最通俗易懂的 Swift 函數語言程式設計

SwiftCafe發表於2015-12-26

函數語言程式設計(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 平臺對應響應式程式設計的實現。這些新的程式設計方式希望用一種更加優雅簡潔的方式來解決我們開發中的問題。

相關文章