Swift函數(shù)式編程三(Map、Filter和Reduce)

代碼地址

泛型介紹

需求為寫一個(gè)這樣的函數(shù)惭载,此函數(shù)接收一個(gè)參數(shù)為整型數(shù)組,返回一個(gè)一個(gè)新數(shù)組响巢,新數(shù)組各項(xiàng)為原數(shù)組對應(yīng)的數(shù)據(jù)加一描滔。

func incrementArray(array: [Int]) -> [Int] {
    var result: Array<Int> = []
    for i in array {
        result.append(i + 1)
    }
    
    return result
}

新增需求,再寫一個(gè)函數(shù)踪古,此函數(shù)接收一個(gè)參數(shù)為整型數(shù)組含长,返回一個(gè)一個(gè)新數(shù)組券腔,新數(shù)組各項(xiàng)為原數(shù)組對應(yīng)的數(shù)據(jù)兩倍。

func doubleArray(array: [Int]) -> [Int] {
    var result: [Int] = []
    for i in array {
        result.append(i*2)
    }
    
    return result;
}

至此拘泞,發(fā)現(xiàn)這兩個(gè)函數(shù)有大量的代碼相同得封,能不能寫一個(gè)更通用的函數(shù)髓霞?新增一個(gè)參數(shù)接收一個(gè)函數(shù),這個(gè)參數(shù)根據(jù)各個(gè)數(shù)組項(xiàng)計(jì)算新的值。

func computeIntArray(array: [Int], transform: (Int) -> Int) -> [Int] {
    var result: [Int] = []
    for i in array {
        result.append(transform(i))
    }
    
    return result
}

這樣就可以簡化一點(diǎn)incrementArray项秉、doubleArray這兩個(gè)函數(shù):

func incrementArray1(array: [Int]) -> [Int] {
    return computeIntArray(array: array, transform: { x in x + 1 })
}
func doubleArray1(array: [Int]) -> [Int] {
    return computeIntArray(array: array, transform: { x in x*2 })
}

代碼任然不夠靈活溃卡,如果需要得到一個(gè)布爾型數(shù)組沮焕,用于表示對應(yīng)的數(shù)字是否為偶數(shù)对室。

func isEvenArray(array: [Int] -> [Bool]) {
    return computeIntArray(array: array, transform: { x in x%2 == 0 })
}

不幸的是上面這段代碼無法使用computeIntArray函數(shù),因?yàn)轭愋湾e(cuò)誤强岸。

于是可以定義一個(gè)新的函數(shù)接受一個(gè)Int -> Bool類型的函數(shù)作為參數(shù)锻弓。

但是這個(gè)方案并不好,如果還要計(jì)算String類型蝌箍,還得定義一個(gè)高階函數(shù)來接受一個(gè)Int -> String類型的函數(shù)作為參數(shù)青灼。

幸運(yùn)的是泛型可以解決這個(gè)問題。相同的代碼可以適用任何類型妓盲,寫一個(gè)適用于每種可能類型的泛型函數(shù):

func genericComputeArray<T>(array: [Int], transform: (Int) -> T) -> [T] {
    var result: [T] = []
    for i in array {
        result.append(transform(i))
    }
    
    return result
}

可以進(jìn)一步一般化這個(gè)函數(shù)杂拨,沒有理由僅能對[Int]型的輸入數(shù)組進(jìn)行處理,可以將數(shù)組類型也進(jìn)行抽象:

func map<Element, T>(array: [Element], transform: (Element) -> T) -> [T] {
    var result: [T] = []
    for i in array {
        result.append(transform(i))
    }
    
    return result
}

對于這個(gè)map函數(shù)在兩個(gè)維度是通用的本橙,任何類型的數(shù)組和transform函數(shù)扳躬。

按照Swift的慣例將map函數(shù)定義為Array的擴(kuò)展會比定義為頂層函數(shù)更合適:

extension Array {
    func map<T>(transform: (Element) -> T) -> [T] {
        var result: [T] = []
        for i in self {
            result.append(transform(i))
        }
        
        return result
    }
}

Element源于Swift的Array中對Element所進(jìn)行的泛型定義脆诉。

map函數(shù)已經(jīng)是Swift標(biāo)準(zhǔn)庫中的一部分(基于SequenceType協(xié)議被定義)

頂層函數(shù)和擴(kuò)展

在一開始將創(chuàng)建map函數(shù)時(shí)甚亭,為了簡便起見,選擇了頂層函數(shù)的版本击胜。不過最終將map的泛型版本定義為Array的擴(kuò)展亏狰,這和Swift標(biāo)準(zhǔn)庫的實(shí)現(xiàn)十分相似。

隨著協(xié)議擴(kuò)展(protocol extensions)偶摔,開發(fā)者有了強(qiáng)有力的工具來定義擴(kuò)展--不僅可以在Array這樣的具體類型上上進(jìn)行定義暇唾,還可以在SequenceType這樣的協(xié)議上定義擴(kuò)展。

把處理確定類型的函數(shù)辰斋,定義為該類型的擴(kuò)展策州。這樣的優(yōu)點(diǎn)是:

  • 自動(dòng)補(bǔ)全更完善
  • 命名更少
  • 代碼結(jié)構(gòu)更清晰

Filter

filter函數(shù)像之前定義的map函數(shù)一樣,接收一個(gè)函數(shù)作為參數(shù)宫仗,這個(gè)參數(shù)類型是(Elemeng) -> Bool--對于數(shù)組中的所有元素够挂,此函數(shù)判定它是否被包含在結(jié)果中:

extension Array {
    func filter(includeElement: (Element) -> BooleanLiteralType) -> [Element] {
        var result: [Element] = []
        for i in self {
            if (includeElement(i)) { result.append(i) }
        }
        
        return result
    }
}

Swift標(biāo)準(zhǔn)庫中的數(shù)組類型已經(jīng)定義好了filter函數(shù)。

有沒有更通用的函數(shù)藕夫,可以用來定義map孽糖,也可以定義filter枯冈?

Reduce

reduce函數(shù)將變量初始化為某個(gè)值,然后對數(shù)組的每一項(xiàng)進(jìn)行遍歷办悟,以某種方式更新結(jié)果尘奏。

extension Array {
    func reduce<T>(initial: T, combine: (T, Element) -> T) -> T {
        var result: T = initial
        for i in self {
            result = combine(result, i)
        }
        
        return result
    }
}

reduce函數(shù)的泛型體現(xiàn)在兩個(gè)方面:

  • 對于任意類型的數(shù)組,它會計(jì)算一個(gè)T類型的返回值病蛉。
  • 需要一個(gè)T類型的初始值炫加,以及一個(gè)用于更新for循環(huán)中變量值的函數(shù)combine: (T, Elemeng) -> T。

使用reduce定義函數(shù)铺然。除了使用閉包琢感,也可以使用操作符作為最后一個(gè)參數(shù),使代碼更簡短探熔。

func sumUsingReduce(xs: [Int]) -> Int {
    return xs.reduce(initial: 0, combine: { result, i in result + i })
}
func productUsingReduce(xs: [Int]) -> Int {
    return xs.reduce(initial: 1, combine: *)
}
func concatUsingReduce(xs: [String]) -> String {
    return xs.reduce(initial: "", combine: +)
}

甚至可以使用reduce重新定義map驹针、filter。

extension Array {
    func mapUsingReduce<T>(transform: (Element) -> T) -> [T] {
        return self.reduce(initial: [T](), combine: { result, i in result + [transform(i)] })
    }
    func filterUsingReduce(includeElement: (Element) -> Bool) -> [Element] {
        return self.reduce(initial: [Element](), combine: { result, i in includeElement(i) ? result + [i] : result })
    }
}

能夠用reduce表示這些函數(shù)诀艰,說明了reduce能夠通過通用的方法來體現(xiàn)一種常見的編程模式:遍歷數(shù)組并計(jì)算結(jié)果柬甥。

注意:
使用reduce來定義一切非常的簡便,但是實(shí)踐中這往往不是一個(gè)好主意其垄。原因是苛蒲,代碼在最終的運(yùn)行期間大量復(fù)制生成的數(shù)組,換句話說绿满,它不得不反復(fù)的分配內(nèi)存釋放內(nèi)存以及復(fù)制內(nèi)存中的內(nèi)容臂外。像之前那樣用一個(gè)可變數(shù)組定義map顯然效率更高。理論上喇颁,編譯器可以優(yōu)化代碼使其速度和可變數(shù)組一樣快漏健,但是Swift2.0并沒有做優(yōu)化。

實(shí)際運(yùn)用

假設(shè)有一個(gè)City結(jié)構(gòu)體橘霎,由城市名稱和人口(萬)組成蔫浆。并定義了一些城市示例。

struct City {
    let name: String
    let population: Int
}
let beijing = City(name: "北京", population: 4000)
let shanghai = City(name: "上海", population: 3500)
let guangzhou = City(name: "廣州", population: 3000)
let shenzhen = City(name: "深圳", population: 2500)

let citys = [beijing, shanghai, guangzhou, shenzhen]

現(xiàn)在賽選出居民數(shù)量至少為3000萬的城市姐叁,并打印一份這些城市名稱及人口數(shù)的列表瓦盛。

extension City {
    func cityByScalingPopulation() -> City { return City(name: self.name, population: self.population*10000) }
}

let table = citys.filter(includeElement: { city in city.population >= 3000 }).map(transform: { city in city.cityByScalingPopulation() }).reduce(initial: "\n城市:人口\n", combine: { result, city in result + "\n\(city.name):\(city.population)\n"})

泛型和Any類型

Any類型和泛型都能定義接收不同類型的參數(shù)的函數(shù)。然而兩者之間的重要區(qū)別是:

  • 泛型可以用于定義靈活的函數(shù)外潜,類型檢查仍由編譯器負(fù)責(zé)原环。
  • Any類型直接避開了Swift的類型系統(tǒng)(盡可能避免使用)。

用泛型和Any類型分別構(gòu)造一個(gè)函數(shù)处窥,除了返回它的參數(shù)嘱吗,其他什么也不做。

func noOp<T>(x: T) -> T { return x }
func noOpAny(x: Any) -> Any { return x }

noOp和noOpAny函數(shù)都接收任意參數(shù)碧库,關(guān)鍵區(qū)別在于返回值柜与,noOp的返回值類型必須跟參數(shù)一樣巧勤,而noOpAny的返回值可以為任何類型,甚至可以和參數(shù)的類型不同弄匕。如下函數(shù)noOpWrong會導(dǎo)致類型錯(cuò)誤:

func noOpWrong<T>(x: T) -> T { return 0 }
func noOpAnyWrong(x: Any) -> Any { return 0 }

泛型函數(shù)的類型十分豐富颅悉,考慮把上一篇Swift函數(shù)式編程二(封裝Core Image)中的函數(shù)組合運(yùn)算符>>>定義為泛型版本:

precedencegroup ComposeFunctionPrecedence {
    associativity: left
}
infix operator >>>: ComposeFunctionPrecedence
func >>><A, B, C>(f: @escaping (A) -> B, g: @escaping (B) -> C) -> (A) -> C {
    return { x in g(f(x)) }
}

最后用相同的方式定義一個(gè)泛型函數(shù),這個(gè)函數(shù)的作用是將接受兩個(gè)參數(shù)作為輸入的函數(shù)進(jìn)行柯里化處理迁匠,生成相應(yīng)的柯里化版本:

func curry<A, B, C>(f: @escaping (A, B) -> C) -> (A) -> (B) -> C {
    return { x in return { y in f(x, y) } }
}

使用泛型剩瓶,能夠在不犧牲類型安全的情況下寫出靈活的函數(shù);而使用Any類型城丧,則無法辦到延曙。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市亡哄,隨后出現(xiàn)的幾起案子枝缔,更是在濱河造成了極大的恐慌,老刑警劉巖蚊惯,帶你破解...
    沈念sama閱讀 211,194評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件愿卸,死亡現(xiàn)場離奇詭異,居然都是意外死亡截型,警方通過查閱死者的電腦和手機(jī)趴荸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來宦焦,“玉大人发钝,你說我怎么就攤上這事〔郑” “怎么了酝豪?”我有些...
    開封第一講書人閱讀 156,780評論 0 346
  • 文/不壞的土叔 我叫張陵,是天一觀的道長舔痪。 經(jīng)常有香客問我寓调,道長,這世上最難降的妖魔是什么锄码? 我笑而不...
    開封第一講書人閱讀 56,388評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮晌涕,結(jié)果婚禮上滋捶,老公的妹妹穿的比我還像新娘。我一直安慰自己余黎,他們只是感情好重窟,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,430評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著惧财,像睡著了一般巡扇。 火紅的嫁衣襯著肌膚如雪扭仁。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,764評論 1 290
  • 那天厅翔,我揣著相機(jī)與錄音乖坠,去河邊找鬼。 笑死刀闷,一個(gè)胖子當(dāng)著我的面吹牛熊泵,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播甸昏,決...
    沈念sama閱讀 38,907評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼顽分,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了施蜜?” 一聲冷哼從身側(cè)響起卒蘸,我...
    開封第一講書人閱讀 37,679評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎翻默,沒想到半個(gè)月后悬秉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,122評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡冰蘑,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,459評論 2 325
  • 正文 我和宋清朗相戀三年和泌,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片祠肥。...
    茶點(diǎn)故事閱讀 38,605評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡武氓,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出仇箱,到底是詐尸還是另有隱情县恕,我是刑警寧澤,帶...
    沈念sama閱讀 34,270評論 4 329
  • 正文 年R本政府宣布剂桥,位于F島的核電站忠烛,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏权逗。R本人自食惡果不足惜美尸,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,867評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望斟薇。 院中可真熱鬧师坎,春花似錦、人聲如沸堪滨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至遏乔,卻和暖如春义矛,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背盟萨。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評論 1 265
  • 我被黑心中介騙來泰國打工凉翻, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人鸯旁。 一個(gè)月前我還...
    沈念sama閱讀 46,297評論 2 360
  • 正文 我出身青樓噪矛,卻偏偏與公主長得像,于是被迫代替她去往敵國和親铺罢。 傳聞我的和親對象是個(gè)殘疾皇子艇挨,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,472評論 2 348