原文首發(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)換,而這里的泛型消除了二者的歧義。