Functional Programming in Swift(三)

原文首發(fā)于我的blog:https://chengwey.com

Chapter 4 Map, Filter, Reduce

本文是《Functional Programming in Swift》中第四章的筆記,如果你感興趣,請購買英文原版。

一個(gè)函數(shù)A使用另一個(gè)函數(shù) B 作為參數(shù),我們可以稱這個(gè) A 函數(shù)是一個(gè)高階函數(shù),本節(jié)我們來探尋一下高階函數(shù)在 Swift standard library 中數(shù)組上面的一些應(yīng)用。為了搞懂這些,首先來介紹一下泛型

<h2 id='TheFilterType'>1. Introducing Generics</h2>

我們有一個(gè)整型數(shù)組,現(xiàn)在需要寫一個(gè) function 返回一個(gè)數(shù)組,使其所有元素都加 1:

func incrementArray(xs: [Int]) -> [Int] { 
    var result: [Int] = [ ]
    for x in xs {
        result.append(x + 1) 
    }
    return result 
}

再寫另外一個(gè) function,返回一個(gè)數(shù)組,使其所有元素都 ×2:

func doubleArray1(xs: [Int]) -> [Int] { 
    var result: [Int] = [ ]
    for x in xs {
        result.append(x * 2) 
    }
    return result
}

這些函數(shù)都共享相同的代碼,我們能否抽象相同和不同的部分,寫出更加通用的 function 呢,比如:

func computeIntArray(xs: [Int]) -> [Int] { 
    var result: [Int] = [ ]
    for x in xs {
        result.append(/* something using x */) 
    }
    return result 
}

為了實(shí)現(xiàn)這一目標(biāo),我們添加一個(gè)函數(shù)作為參數(shù),這個(gè)函數(shù)描述了如何計(jì)算數(shù)組中的每一個(gè)元素:

func computeIntArray(xs: [Int], f: Int -> Int) -> [Int] { 
    var result: [Int] = [ ]
    for x in xs {
        result.append(f(x)) 
    }
    return result 
}

現(xiàn)在我們就能根據(jù)需要傳入不同的參數(shù)(function)來滿足我們的需要,這樣實(shí)現(xiàn) doubleArray 和 incrementArray 只需要一行代碼調(diào)用 computeIntArray: 就能實(shí)現(xiàn):

func doubleArray2(xs: [Int]) -> [Int] { 
    return computeIntArray(xs) { x in x * 2 }
}

注意上面的 function 的第二個(gè)參數(shù)使用了尾隨閉包。雖然改造過的函數(shù)具備了一定的通用性,但還不夠徹底,比如我們想判斷這個(gè)整型數(shù)組所有元素的奇偶性,返回一個(gè)包含 BOOL 類型的結(jié)果數(shù)組,我們可以這么寫:

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

不幸的是上面的函數(shù)會報(bào)錯(cuò),因?yàn)?computeIntArray 函數(shù)接收的第二個(gè)參數(shù)是 Int -> Int 類型,而我們傳進(jìn)來的卻是 Int -> Bool。解決的辦法之一是定義一個(gè)新的 computeBoolArray 函數(shù):

func computeBoolArray(xs: [Int], f: Int -> Bool) -> [Bool] { 
    let result: [Bool] = [ ]
    for x in xs {
        result.append(f(x)) 
    }
    return result 
}

雖然問題解決了,但并不是具有“彈性”,下一次如果要計(jì)算 String 類型呢,難道還要定義一個(gè) Int -> String 函數(shù)么。

幸運(yùn)的是,我們可以使用泛型( generics )。computeBoolArray 和 computeIntArray 僅僅是參數(shù)不同而已,我們下面寫一個(gè)通用版本:

func genericComputeArray<U>(xs: [Int], f: Int -> U) -> [U] { 
    var result: [U] = [ ]
    for x in xs {
        result.append(f(x)) 
    }
    return result 
}

這里的 “U” 是類型簽名,所有的函數(shù)中所有的 U 都是相同類型,我們繼續(xù)深入一步:把整型數(shù)組變的更為通用的一般數(shù)組:

func map<T, U>(xs: [T], f: T -> U) -> [U] { 
    var result: [U] = [ ]
    for x in xs {
        result.append(f(x)) 
    }
    return result 
}

這里我們寫了一個(gè) map 函數(shù),他帶兩個(gè)參數(shù):元素類型為 T 的數(shù)組,和類型為 T -> U 的 function ,這個(gè) map 函數(shù)最終返回一個(gè)元素類型為 U 的數(shù)組。這個(gè)map函數(shù)比之前的 genericComputeArray 更為通用,我們可以這樣定義 genericComputeArray:

func computeIntArray<T>(xs: [Int], f: Int -> T) -> [T] { 
    return map(xs, f)
}

實(shí)際上,swift 的標(biāo)準(zhǔn)庫已經(jīng)定義了 map 方法,我們可以直接通過 xs.map(f) 來調(diào)用,比如:

func doubleArray3(xs: [Int]) -> [Int] { 
    return xs.map { x in 2 * x }
}

<h2 id='Filter'>2. Filter </h2>

map 方法不是 swift 基本庫中唯一用到泛型的函數(shù),下面介紹其他一些方法。假設(shè)有一個(gè)數(shù)組如下:

let exampleFiles = ["README.md", "HelloWorld.swift", "HelloSwift.swift", "FlappyBird.swift"]

但我們想要一個(gè)只包含 swift 文件的數(shù)組:

func getSwiftFiles(files: [String]) -> [String] { 
    var result: [String] = [ ]
    for file in files {
        if file.hasSuffix(".swift") { 
            result.append(file)
        }
    }
    return result 
}

// 調(diào)用
getSwiftFiles(exampleFiles)

> [HelloWorld.swift, HelloSwift.swift, FlappyBird.swift]

我們用更通用的方式來改寫,首先定義一個(gè) check 類型:T -> Bool,來 check 數(shù)組中的每一個(gè)元素,并輸出:

func filter<T>(xs: [T], check: T -> Bool) -> [T] { 
    var result: [T] = [ ]
    for x in xs {
        if check(x) { 
            result.append(x)
        } 
    }
    return result 
}

定義一個(gè)過濾 swift 文件的方法:

func getSwiftFiles2(files: [String]) -> [String] {
    return filter(files) { 
        file in file.hasSuffix(".swift") 
    }
}

其實(shí)和 map 方法一樣,swift 基本庫也提供了原生的 filter 方法,我們可以直接調(diào)用:

exampleFiles.filter { file in file.hasSuffix(".swift") } 

> [HelloWorld.swift, HelloSwift.swift, FlappyBird.swift]

<h2 id='Reduce'>3. Reduce </h2>

本節(jié)我們先來看幾個(gè)簡單的函數(shù),首先是對一個(gè)整型數(shù)組的所有元素求和:

func sum(xs: [Int]) -> Int { 
    var result: Int = 0
    for x in xs {
        result += x 
    }
    return result 
}

let xs = [1, 2, 3, 4] sum(xs)
> 10

對整型數(shù)組所有元素求乘積:

func product(xs: [Int]) -> Int { 
    var result: Int = 1
    for x in xs {
        result = x * result 
    }
    return result 
}

組合一個(gè)字符串:

func concatenate(xs: [String]) -> String { 
    var result: String = ""
    for x in xs {
        result += x 
    }
    return result 
}

我們還可以提供一個(gè)header line:

func prettyPrintArray(xs: [String]) -> String {
    var result: String = "Entries in the array xs:\n" 
    for x in xs {
        result=" "+result+x+"\n" 
    }
    return result 
}

以上這些方法有兩個(gè)部分可抽象,result的初始值,在循環(huán)中用來更新result的 function,我們按照這個(gè)思想來定義一個(gè) reduce function:

// 一個(gè)任意類型的輸入數(shù)組 [A],將要計(jì)算出結(jié)果類型 R,一個(gè)用來更新結(jié)果的 function :(R,A) -> R
func reduce<A, R>(arr: [A], 
                    initialValue: R,
                    combine: (R, A) -> R) -> R {
    var result = initialValue 
    for i in arr {
        result = combine(result, i) 
    }
    return result 
}

我們現(xiàn)在可以用 reduce fucntion 來重新實(shí)現(xiàn)之前那些 simple function:

// 求和
func sumUsingReduce(xs: [Int]) -> Int {
    return reduce(xs, 0) { result, x in result + x }
}
// 乘積
func productUsingReduce(xs: [Int]) -> Int { 
    return reduce(xs, 1, *)
}
// 字符串拼接
func concatUsingReduce(xs: [String]) -> String { 
    return reduce(xs, "", +)
}

reduce 也是 swift 基本庫原生實(shí)現(xiàn)的,我們可以直接通過 xs.reduce(initialValue, combine) 來使用。我們也可以使用 reduce 來構(gòu)造新的泛型函數(shù),比如有一個(gè)包含數(shù)組的數(shù)組,我們將所有元素導(dǎo)出到一個(gè)數(shù)組中。首先我們用普通的方法來寫:

func flatten<T>(xss: [[T]]) -> [T] { 
    var result : [T] = [ ]
    for xs in xss {
        result += xs 
    }
    return result 
}

改用 reduce :

func flattenUsingReduce<T>(xss: [[T]]) -> [T] {
    return xss.reduce([ ]) { result, xs in result + xs }
}

事實(shí)上,我們還可以用 reduce 來重新定義 map 和 filter:

// map
func mapUsingReduce<T, U>(xs: [T], f: T -> U) -> [U] { 
    return xs.reduce([ ]) { result, x in result + [f(x)] }
}
// filter
func filterUsingReduce<T>(xs: [T], check: T -> Bool) -> [T] { 
    return xs.reduce([ ]) { result, x in
        return check(x) ? result + [x] : result 
    }
}

以上例子展示了 reduce 的本質(zhì):就是遍歷數(shù)組中的元素來計(jì)算最終結(jié)果。

<h2 id='PuttingItAllTogether'>4. Putting It All Together</h2>

現(xiàn)在我們來看一個(gè)實(shí)際的例子,假定我們有一個(gè) struct City 定義如下:

struct City {
    let name: String 
    let population: Int
}

然后我們定義一些城市,并將他們放到一個(gè)數(shù)組中:

// 人口(單位:千)
let paris = City(name: "Paris", population: 2243)
let madrid = City(name: "Madrid", population: 3216)
let amsterdam = City(name: "Amsterdam", population: 811) 
let berlin = City(name: "Berlin", population: 3397)

let cities = [paris, madrid, amsterdam, berlin]

假定我們需要打印常住人口至少一百萬的城市,我們先定義一個(gè) scale 函數(shù)來轉(zhuǎn)換人口單位:

func scale(city: City) -> City {
    return City(name: city.name, population: city.population * 1000)
}

接著,我們就用本章所接觸的函數(shù)來實(shí)現(xiàn)這個(gè)方法

cities.filter({ city in city.population > 1000 }) 
       .map(scale)
       .reduce("City: Population") { result, c in
            return result + "\n" + "\(c.name) : \(c.population)"
        }
        
> City: Population 
> Paris : 2243000 
> Madrid : 3216000 
> Berlin : 3397000

首先,我們用 filter 篩選出人口不小于一百萬的城市,然后用 map 遍歷所有城市轉(zhuǎn)換人口單位,最后用 reduce 將字符串拼接起來。

<h2 id='GenericsVs.theAnyType'>5. Generics vs. the Any Type</h2>

除了泛型,swift 還提供了 Any type 來表示任意類型,表面上和泛型類似,他們都可以用在函數(shù)上表示接受不同的類型的參數(shù),但是他們很重要的區(qū)別就是:泛型通常用來定義 flexible fucntion,編譯器仍然可以檢查類型。但是 Any type 躲開了 swift 的類型檢查。

// 返回類型是T,編譯器可以判斷
func noOp<T>(x: T) -> T { 
    return x
}
// 返回類型不確定,編譯器不知道,可能呢引起運(yùn)行時(shí)錯(cuò)誤
func noOpAny(x: Any) -> Any { 
    return x
}

最后,泛型中的類型可以提供很多信息,比如我們可以定義 >>> 的泛型版本:

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

同樣的方式,我們可以泛型一個(gè)柯理化:

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

這樣我們就定義了一個(gè)轉(zhuǎn)換函數(shù),將一個(gè)非柯理化的函數(shù)轉(zhuǎn)換成柯理化的函數(shù)。

使用泛型,我們可以寫出靈活性和擴(kuò)展性都很強(qiáng)的函數(shù)而不必受制于類型安全。

<h2 id='4Notes'>6. Notes</h2>

泛型的歷史可以追溯到很久之前( Strachey (2000), Girard’s System F (1972), and Reynolds (1974))不過這些作者提到的泛型指多態(tài),現(xiàn)在很多面向?qū)ο笳Z言都在使用多態(tài)做來自與子類的隱式轉(zhuǎn)換,而這里的泛型消除了二者的歧義。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,983評論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內(nèi)容