函數式編程(Functional Programming)是相對於我們常用的面向對象和面向過程編程的另外一種開發思維方式,它更加強調以函數為中心。善用函數式編程思路,可以對我們的開發工作有很大的幫助和啟發,今天我們就來討論一下吧。
什么是函數式編程
我們用一個簡單的例子為大家說明什么是函數式編程。 比如我們有這樣一個結構:
struct Staff {
var firstname: String
var lastname: String
var age: Int
var salary: Float
}
Staff
結構定義了員工的基本信息,比如姓名,年齡,薪水等等。 我們再聲明一個數組,里面存放我們的員工信息:
let staffs = [
Staff(firstname:"Ming", lastname:"Zhang", age: 24, salary: 12000.0),
Staff(firstname:"Yong", lastname:"Zhang", age: 29, salary: 17000.0),
Staff(firstname:"TianCi", lastname:"Wang", age: 44, salary: 30000.0),
Staff(firstname:"Mingyu", lastname:"Hu", age: 30, salary: 15000.0),
Staff(firstname:"TianYun", lastname:"Zhang", age: 25, salary: 12000.0),
Staff(firstname:"Wang", lastname:"Meng", age: 24, salary: 14000.0)
]
行為式思維
現在,我們需要找到所有姓張的員工信息(lastname 等於 Zhang),如果用我們慣用的開發思路,可以這樣解決:
var staffOfZhang = [Staff]()
for staff in staffs {
if staff.lastname == "Zhang" {
staffOfZhang.append(staff)
}
}
print(staffOfZhang)
這段代碼首先聲明了一個數組 staffOfZhang
用於存放符合條件的 Staff 實例,然后我們開始遍歷我們的 staffs 數組,對於其中 lastname
等於 "Zhang" 的實例,將他們添加到 staffOfZhang
這個數組中。 這就完成了我們這個查找需求。
這種開發思路我們可以稱作行為式思路。它側重於告訴程序如何解決問題。比如我們定義了查詢結果存放在哪里,以及如何遍歷每一個實例,然后將符合條件的實例讀取出來。
聲明式思維
解決問題肯定不止一種方法,我們還可以換一種思維方式來解決這個問題,這種思維也可以稱為聲明式思維。我們可以使用 Array
的 filter
方法:
let staffOfZhang = staffs.filter { staff in
return staff.lastname == "Zhang"
}
這就是聲明式思維的一個例子,這里 staffs.filter
函數接受一個閉包類型的參數,filter 方法會對 staffs 中的每一個元素都用傳入 filter 的閉包調用一遍,根據這個閉包的返回值決定是否將這個元素作為符合條件(閉包的返回值如果為 true
則表示符合條件)的元素加入我們的查找結果中。
簡單來說我們只需要在傳入的閉包中聲明好查找的規則,也就是 return staff.lastname == "Zhang"
這個表達式。這樣我們就完成整個查找操作的處理了。
我們這里並沒有告訴程序應該怎么去查找滿足條件的元素的方法,而只是聲明了一個規則。 這樣做最大的好處就是能夠減少我們的代碼量,讓我們的代碼看起來非常的簡潔,而且易理解。 這種方式就是函數式編程的一個例子。
First-Class function
談到函數式編程就會提到 First Class Function。 這是個什么鬼呢~
簡單來說 First Class Function 這樣的函數不但可以進行簡單的調用,還可以賦值給變量,也可以作為參數傳遞給另外一個函數,還可以作為函數的返回值。關於更詳細的描述,可以參看 Wikipedia 上面的這篇文章:https://en.wikipedia.org/wiki/First-class_function
Swift 中的閉包就屬於 First Class, 所以我們可以將閉包賦值給變量,傳遞給函數,作為返回值等等。對於我們上面的那個例子來說,我們就可以這樣改寫:
func filterZhang(staff:Staff) -> Bool {
return staff.lastname == "Zhang"
}
staffOfZhang = staffs.filter(filterZhang)
這正好和咱們剛才說的 First Class 對應上了。首先定義了一個 filterZhang 函數,接着將這個函數作為參數傳遞給 filter。
就這么簡單,這也是 Swift 函數式編程的一個體現。
curry
那么,聰明的各位可能又想了,雖然我們可以定義這樣一個函數,然后作為參數傳遞給 filter,但又有什么好處呢? 代碼量並不比之前的少。
沒錯,這個問題問到關鍵之處了。且聽繼續分解。函數式編程的另一大特性就是 curry。這也是函數式編程的一個核心概念。
說白了 curry 就是用函數生成另一個函數。關於 curry 的詳細探討,還可以參考我之前的幾篇文章:
神奇的 Currying
Swift 中 curry 特性的高級應用
下面咱們就用最簡單的例子說明 curry 特性。我們可以將剛才定義的 filterZhang
方法改寫一下:
func filterGenerator(lastnameCondition: String) -> (Staff) -> (Bool) {
return {staff in
return staff.lastname == lastnameCondition
}
}
那么我們來看一下 filterGenerator 的聲明,首先它接收一個 String 類型的參數,然后它會返回另一個函數,這個函數接受一個 Staff 類型的參數,並且返回一個布爾值。
詳細各位也都看出來了,我們定義的這個新函數 filterGenerator,其實就是對我們之前定義的 filterZhang 函數做了一個更高層的抽象。filterZhang 會過濾處所有 lastname 等於 Zhang 的員工實例。而使用 filterGenerator 可以生成任意條件的過濾函數:
let filterWang = filterGenerator("Wang")
let filterHu = filterGenerator("Hu")
大家看到了吧,我們對 filterGenerator 傳入不同的參數,它就會根據這個參數生成一個新的過濾函數。然后我們就可以直接使用:
staffs.filter(filterHu)
這個調用會查詢出所有 lastname 等於 Hu 的員工。這就是 curry 特性的精髓。比如,你在開發一個照片處理 APP,會對照片應用很多濾鏡,並且這些濾鏡還可以疊加,那么使用 curry 方式就可以讓你的開發的效率和代碼的健壯性提高很多。
更多應用
我們再來多看一些例子。假如我們現在想把所有員工的名字保存到另外一個數組中:
var names = [String]();
for staff in staffs {
names.append("\(staff.lastname) \(staff.firstname)")
}
print(names)
再來看看我們使用函數式方式如何完成:
names = staffs.map{ staff in
return "\(staff.lastname) \(staff.firstname)"
}
print(names)
這次我們使用的是 map 方法,它會對數組中所有的元素依照我們指定的規則進行變換,然后生成一個新的數組。這樣我們只需要在閉包中聲明變換規則就完成了。又是聲明式思維的一種體現。
再比如,我們想計算出所有員工的平均工資:
var totalSalary = Float(0.0)
for staff in staffs {
totalSalary += staff.salary
}
print(totalSalary / Float(staffs.count))
我們再看看如何用函數式的思路來完成:
let averageSalary = staffs.reduce(0) { total, staff in
return total + staff.salary / Float(staffs.count)
}
print(averageSalary)
這次我們使用 Array 的 reduce 函數,這個函數接受兩個參數,第一個參數是初始值,然后 reduce 會依次讓每一個元素和這個值進行操作,然后將計算結果傳遞給下一個元素的調用。
對於我們這里,就是依次計算每個員工對於平均工資的基數,然后將他們相加到一起就是整體的平均工資了。我們這里依然只聲明了一個規則 return total + staff.salary / Float(staffs.count)
。這樣代碼讀起來非常的清晰,很明確的說明了我們要干什么。
結語
到這里,函數式編程的基本思路就都給大家介紹完了。總之呢函數式編程的主要特性就是聲明式思維以及 curry 傳遞思維。它能夠讓我們用很優雅的語法實現在以前看來比較繁雜的邏輯,這也是它的最大優勢。並且它還衍生除了一些分支,比如響應式編程,非常流行的 ReactiveCocoa 庫正式 Cocoa 平台對應響應式編程的實現。這些新的編程方式希望用一種更加優雅簡潔的方式來解決我們開發中的問題。