第七章 函數編程

第七章:Functional Programming - 函數編程

在前面的章節中,你已經瞭解了在Swift中泛型,類,枚舉,範圍操作符以及一些其他有趣的語法特點。整體看下這些特點,很明顯,Swift比oc的表達要簡潔很多。

在oc過渡到Swift的過程中,你知道可以將一些oc的邏輯概念映射到Swift中。你知道了怎麼在oc中創建類,也知道了在Swift中如何創建。當然,Swift中有一些完全不同的新東西,如泛型,範圍操作,或者其他你已經知道其他的不同的玩意。

然而,Swift爲你的應用程序提供了更好的語法。用這種新的語言,你有機會去用代碼改變解決問題的方式。在Swift中,函數式編程技術成爲了編程工具包中可行且十分重要的一部分。

這本書學習Swift語言非常注重實踐。函數編程是一個比較大的話題,所以這章我們就通過例子來學習下。你將通過大量的編程例子來熟悉命令式風格,然後嘗試着用函數技術解決同樣的問題。

簡單地說,函數式編程是一種編程範式,強調通過數學計算風格的函數,突出不變性和表現力,最小化的使用變量和狀態。

每個函數在應用程序中就像一個島嶼獨自在海洋中。用最少的共享狀態,這會讓測試變得更容易些。還有一件事是能提高你多核設備的性能。

是時候把時間用到函數式編程中了。

Simple array filtering - 簡單的數組過濾

你開始只需要做一些簡單的事:一個簡單的數學計算。你的第一個任務是創建一個函數用來查找1到10之間的所有偶數。不是個非常重要的函數,但是可以將你很好的引入到函數編程當中!

Filtering the old way - 老的過濾方式

創建一個新的playground,並保存以下代碼:

var evens = [Int]() 
for i in 1...10 { 
    if i % 2 == 0 { 
        evens.append(i) 
    } 
} 
print(evens) 

這個結果輸出:

[2, 4, 6, 8, 10] 

這個腳本方法非常簡單,核心算法如下:
1.你創建了一個空的可變數組。
2.這個for循環迭代從1到10(牢記“…”是閉區間包含的意思)
3.如果碰到了數值是偶數這個條件,你需要將這個值添加到數組中。

上面的幾行代碼是必須的。使用了if和for兩個基本的控制結構,告訴了計算機如何定位到偶數。

這個代碼運行很正常,但重要的關於判斷數值是否是偶數的內容嵌套在for循環中。相對的耦合有些過於緊密,需要將偶數數組結構添加到條件判斷中。如果你想在app的別處打印每個偶數,只能通過代碼的複製粘貼重用了。

來來來,函數編程時間~~

Functional filtering - 函數過濾

在playground後面繼續添加代碼:

func isEven(number: Int) -> Bool { 
    return number % 2 == 0 
}

evens = Array(1...10).filter(isEven) 
print(evens) 

你會發現上面的,代碼創建了完全相同結果的函數:

[2, 4, 6, 8, 10] 

讓我們來進一步瞭解這個版本的函數。他包括兩個部分:
1.數組Array(1…10)部分簡單的遍歷創建了一個包含了數字1到10的值的數組.範圍操作符1…10創建了Range來完成數組的初始化。

2.filter語句塊是這個函數編程中發生神奇事情的地方。在filter方法中,通過暴露數組的值,創建並返回一個僅包含返回true的值的新數組。在這個例子中,isEven提供了過濾功能。

你將isEven作爲一個傳入參數傳遞到過濾中,正如你在第五章“函數和閉包”中瞭解的,函數就是有名字的閉包。嘗試下面的修改,讓代碼變得更簡潔:

evens = Array(1...10).filter { 
        (number) in number % 2 == 0 
    } 
print(evens) 

再次確認上面三種方式的返回結果都是相同的。上面的例子表明編譯器能夠從上下文推斷參數的數量和類型以及返回的類型。

如果你想讓你的代碼更簡潔些,試試下面的吧:

evens = Array(1...10).filter { $0 % 2 == 0 } 
print(evens) 

上面使用了參數簡化符號,執行了隱式返回,類型的推斷等等!

注意,使用速記參數符號純屬個人愛好。就我個人而言,我認爲像上面這樣簡單的例子,速記符號就非常好用。然而,在更復雜的地方我會選擇顯示參數名稱,編譯器不關心變量名,但是人類世界需要這個來更好的讀懂代碼不是~~

這個版本的功能代碼肯定是比最前面的命令式代碼要簡潔。這個簡單的例子展示裏一些常見函數語言共同的有趣特點:
1.高階函數:這些函數可以作爲參數傳遞給其他函數。在這個簡單的例子中,filter要求你傳入一個高階函數。

2.一流類函數:你可以把函數當做任何其它的變量來處理,可以將他們分配給變量並將他們作爲參數傳遞給其他函數。

3.閉包:就地創建有效的匿名函數。

你可能已經注意到,oc也有block來實現相同的功能。然而,Swift在促進更簡潔的函數式編程語法以及內置過濾等功能的能力遠超oc。

The magic behind filter - 神奇的filter背後實現

Swift的數組有着很多的函數方法,比如map,join和reduce。這些方法背後是怎麼實現的呢?

是時候看看神奇的filter是怎麼完成過濾的了。

在playground中添加函數:

func myFilter<T>(source: [T], predicate:(T) -> Bool) -> [T] { 
    var result = [T]()

    for i in source { 
            if predicate(i) { 
            result.append(i) 
        } 
    } 
    return result 
    } 

上面這個泛型函數將一個函數作爲參數傳入,函數中需要一個source(T類型的數組),前置條件predicate,並返回一個bool。

看上去myFilter的實現是要比我們前面開始做的函數要高精尖些。主要的區別在於檢查的條件現在是一個函數而不是一段寫死的代碼。

添加下面的代碼來試試新添加的過濾實現方法。

evens = myFilter(Array(1...10)) { $0 % 2 == 0 } 
print(evens) 

再次查看,輸出結果和剛剛相同!

挑戰:上面的filter函數是全局的,爲什麼不試試看改爲數組的方法。 一點小提示: 你可以在類的擴展中將myFilter添加到數組中。
你可以擴展數組,而不是用Array[T].也就是說當你執行時,數組會自己迭代自己包含的內容。

Reducing - 過濾添加

前面的示例是個簡單的例子,就用了一個函數方法。在本節中,你將進一步提升,實現功能技術更爲複雜的邏輯。

創建一個新的playground並準備好開始吧!

Manual reduction - 手動實現

在本節中的任務會稍微複雜一點:取出1到10之間的全部偶數並計算他們的和。就試試所謂的reduce函數,傳入一組輸入返回一個輸出。

我相信你有能力用更多的方法實現這個功能,但是這裏就暫且當是練習了,添加代碼:

var evens = [Int]() 
for i in 1...10 { 
    if i % 2 == 0 { 
        evens.append(i) 
    } 
} 
var evenSum = 0 
    for i in evens { 
        evenSum += i 
    } 
print(evenSum) 

控制檯輸出:30

這代碼在偶數的篩選上和前面是一樣的,只是後面多添加了一個for循環條件用於加值。

讓我們來看看等價於這個函數的方法!

Functional reduce - 函數reduce

在playground中添加代碼:

evenSum = Array(1...10)

.filter { (number) in number % 2 == 0 } 
.reduce(0) { (total, number) in total + number } 
print(evenSum) 

同樣輸出:30

前一部分使用了一個數組結構並使用了filter。這兩步操作返回了一個有着5個數字的數組(2,4,6,8,10)。新的一步是添加了reduce方法。

reduce是個非常強大的數組方法,可以爲每個參數執行一次函數,用於疊加值。

爲了理解reduce是怎麼工作的,下面的解析希望能幫助你理解:

func reduce<U>(initial: U, combine: (U, T) -> U) -> U

第一個參數initial是初始值 ,類型是U。在當前這個代碼下,initial的值是0並且類型是Int(因此在這裏U指代Int)。第二個參數是結合函數對數組的每一個元素都執行一次。

結和了兩個參數:第一個是U類型,是之前調用的結果。第二個值是數組元素的總和。通過reduce返回每一次執行結果的總和。

執行的步驟很多,讓我們來一步一步的分解。

在代碼裏,reduce執行的第一次結果如下:
這裏寫圖片描述

輸入的總和值是初始值initial0.在輸入數組中第一個值是2.算出總和值是2.

第二步迭代實例如下:
這裏寫圖片描述

在第二步迭代中,將輸入從前面迭代獲取的結果以及數組中下一個內容。結合他們的結果就是:2+4=6.

繼續這個過程直到將數組中的元素全部執行完一次提供以下輸入輸出:
這裏寫圖片描述

突出顯示右下角的數量的總體結果。這是個非常簡單的例子。在實踐中,你可以用reduce執行各種有趣的轉換。

在下面是幾個例子,添加到playground中:

let maxNumber = Array(1...10).reduce(0) { 
    (total, number) in max(total, number) 
} 
print(maxNumber) 

這代碼是使用reduce來找到整數數組中的最大值。在這個例子裏,結果已經很明顯!記住total一直是reduce上一個迭代返回的最大值。

如果你努力的想要知道這行代碼是怎麼工作的,爲什麼不像上面那樣創建一個表,計算你每個迭代的輸入和輸出?

到目前爲止你看到的所有reduce示例都是將整型數組轉爲單一的一個整數值。reduce有兩個類型的參數U和T,可以不同,也可以不是整數。也就是說你可在reduce中使用完全不同的類型。在playground中添加代碼:

let numbers = Array(1...10).reduce("numbers: ") {(total, number) in total + "\(number) "} 
print(numbers) 

這個代碼輸出:numbers: 1 2 3 4 5 6 7 8 9 10

上面這個例子使用reduce將整數轉爲字符串數組。

在這些練習中,你會在使用reduce時發現各種有趣且具有創造性的地方!你可能還記得他在這本書的其他地方出現過。第四章“Generics”,你使用他來找到一個地圖上的邊界矩形。

挑戰:試試能不能使用reduce將一個數字數組轉換爲一個整數,給定的數組: let digits = [“3”, “1”, “4”,“1”]
你的reduce方法應該能返回一個3141的整數值。

在本章的後面會有解決的方法,但試試看自己能不能實現。

The magic behind reduce - reduce的幕後操作

在前面一小節,你用filter實現了需要的方法,非常的簡單。現在,你將看到reduce也同樣可以實現這種方法。

在playground中添加代碼:

extension Array {
    func myReduce<T, U>(seed:U, combiner:(U, T) -> U) -> U {
        var current = seed
        for item in self {
            current = combiner(current, item as! T)
        }
        return current
    } 
} 

上面添加了一個myReduce方法模仿系統自帶的reduce,此方法只是簡單的將數組每個元素都迭代調用一次。

將上面的代碼用來替換掉你playground上的reduce,測試下。

這裏你可能會想,既然系統有了這個方法爲什麼我還要自己實現filter或reduce?answer是:你可能不知道怎麼實現!

你可能很想在Swift中擴展你現在使用的一些功能例子,實現自己的功能方法,所以,理解別人是怎麼實現的非常重要,比如說reduce。

Building an index - 建立索引

是時候去解決一個更難的問題了,也就是說需要新建一個playground來演練下了,你知道怎麼建一個新的playground吧( ⊙ o ⊙ )啊!

在這一節中,你將使用函數編程技術基於每個單詞的第一個字母建立一個列表組。

在playground中添加如下代碼:

import Foundation 
let words = ["Cat", "Chicken", "fish", "Dog","Mouse", "Guinea Pig", "monkey"] 

要完成這個任務,第一步是將他們單詞的第一個字母不區分大小寫的組成一組。

添加代碼:

typealias Entry = (Character, [String]) 
func buildIndex(words: [String]) -> [Entry] { 
    return [Entry]() 
} 
print(buildIndex(words)) 

Entry別名爲每個索引條目定義了一個元組類型。這個例子之所以使用typealias是爲了代碼更易讀,讓你不用反覆的指定一個這樣的元組類型。在buildindex中建立代碼。

Building an index imperatively - 勢在必行的索引建立

從更新下方法開始,更新buidIndex如下:

func buildIndex(words: [String]) -> [Entry] {

    var result = [Entry]()
    var letters = [Character]()
    for word in words {
        let firstLetter = Character(word.substringToIndex( word.startIndex.advancedBy(1)).uppercaseString)
        if !letters.contains(firstLetter){
            letters.append(firstLetter)
        } }
    for letter in letters {
        var wordsForLetter = [String]()
        for word in words {
            let firstLetter = Character(word.substringToIndex( word.startIndex.advancedBy(1)).uppercaseString)
            if firstLetter == letter { wordsForLetter.append(word)
            }
        } 
        result.append((letter, wordsForLetter)) 
    } 
    return result
}

這個函數包含兩個部分,每個部分都有自己的循環。上半部分遍歷每個單詞建立字符數組。第二部分遍歷這些字符,找到對應的單詞,建立返回的數組。

實現了之後,可以看到輸出:

[(C, [Cat, Chicken]),
(F, [fish]),
(D, [Dog]),

(M, [Mouse, monkey]), 
(G, [Guinea Pig])] 

上面的輸出格式還算是比較清晰。

這個功能的實現使用了很多的步驟和嵌套循環,致使其變得難以理解。讓我們來看看其他的實現方法。

Building an index the functional way - 建立一個有index索引額函數方法

創建新的playground並初始化相同的代碼:

import Foundation
let 
words = ["Cat", "Chicken", "fish", "Dog", "Mouse", "Guinea Pig", "monkey"] 
typealias Entry = (Character, [String]) 
func buildIndex(words: [String]) -> [Entry] { 
    return [Entry](); 
} 
print(buildIndex(words)) 

輸出語句輸出一個空的數組:[]

建立一個索引的第一步是將單詞轉換爲只包含了他們的首字母的數組,更新buildIndex:

func buildIndex(words: [String]) -> [Entry] {
    let letters = words.map {
        (word) -> Character in
     Character(word.substringToIndex(word.startIndex.advancedBy(1)).uppercaseString)
    }
    print(letters)
    return [Entry]()
}

輸出一個大寫字母的數組。

在前面的章節中,你碰到過filter和reduce。上面的代碼介紹了map,數組的另一種功能。

map生成一個新的數組用來提供數組中每個元素的調用結果。你使用map來進行轉換。在這個例子中,map將一個[Sting]類型的數組轉換爲[Character]類型的數組。

當前字母的數組中包含了一些重複的內容,而你需要的索引只能有一個單字母。不巧的是,Swift的數組中沒有執行去重的方法,所以只能自己動手豐衣足食了!

在前面的章節中,你已經看到了重新實現reduce和filter是非常簡單的。所以毫無疑問,你要實現一個去重的方法應該也不會太棘手。

在buildIndex函數簽名繼續添加代碼:

func distinct<T: Equatable>(source: [T]) -> [T] { 
    var unique = [T]()
 for item in source { 
    if !contains(unique, item) { 
        unique.append(item) 
    } 
} 
    return unique 
} 

distinct遍歷數組中的所有項,並建立一個只包含唯一項目的數組。

更新buildIndex:

func buildIndex(words: [String]) -> [Entry] {
    let letters = words.map {
        (word) -> Character in
        Character(word.substringToIndex(word.startIndex.advancedBy(1)).uppercaseString)
    }
    let distinctLetters = distinct(letters)
    print(distinctLetters)
    return [Entry]()
}

現在輸出:[C, F, D, M, G]

現在你有了一個擁有不同字母的數組,接下來的任務是建立一個你的index索引將每個字母都轉換成一個Entry實例。聽起來像是個轉換吧?這算是給map的另一個任務。

更新buildindex:

func buildIndex(words: [String]) -> [Entry] {
    let letters = words.map {
        (word) -> Character in
        Character(word.substringToIndex(word.startIndex.advancedBy(1)).uppercaseString)
    }
    let distinctLetters = distinct(letters)

    return distinctLetters.map {
        (letter) -> Entry in return (letter, [])
    } 

}

第二個調用map的數組是字符數組,輸出一個Entry的實例:

[(C, []),
 (F, []),
 (D, []),
 (M, []),
 (G, [])]

開發基本要完成了,最後一個任務是用一個給定的字母開頭的單詞來填充每一個Entry實例。更新函數並添加一個嵌套的filter,如下所示:

func buildIndex(words: [String]) -> [Entry] {
    let letters = words.map {
        (word) -> Character in
        Character(word.substringToIndex(word.startIndex.advancedBy(1)).uppercaseString)
    }
    let distinctLetters = distinct(letters)

    return distinctLetters.map {
        (letter) -> Entry in return (letter, words.filter({ (word) -> Bool in
            Character(word.substringToIndex(word.startIndex.advancedBy(1)).uppercaseString) == letter
        }))
    } 

}

上面的代碼輸出:

[(C, [Cat, Chicken]), 
(F, [fish]),
(D, [Dog]),

(M, [Mouse, monkey]), 
(G, [Guinea Pig])] 

在函數的第二部分,在map中嵌套了filter函數,將過濾每一個不同字母的單詞列表,從而確定以給定字母開始的單詞。

上面的實現已經相當的簡潔清晰了,但仍然還可以再次優化:此代碼多次提取並使用第一個單詞,消除重複的調用明顯更好些。

如果在oc中,你可以有多種不同的解決方案:你可以創建一個實用的方法來執行這個功能,你也可以直接將方法添加到NSString類中。然而如果你只是要在buildIndex中實現此功能,卻在別的類中添加新的方法明顯有些矯枉過正。

好在,在Swift中還有一個更好的解決方法!

func buildIndex(words: [String]) -> [Entry] {
    func firstLetter(str: String) -> Character {
        return Character(str.substringToIndex( str.startIndex.advancedBy(1)).uppercaseString)
    }
    let letters = words.map {
        (word) -> Character in firstLetter(word)
    }
    let distinctLetters = distinct(letters)
    return distinctLetters.map {
        (letter) -> Entry in
        return (letter, words.filter {
            (word) -> Bool in
            firstLetter(word) == letter }) 
    } 
}

你會看到和剛剛一樣完全相同的輸出結果。

上面的代碼添加了一個firstLetter的函數,嵌套在buildIndex中,並將第一個字符作爲結果返回,一個完全的本地外部函數。這裏使用了Swift的一流功能,你可以在你允許的任務和範圍外使用變量。

新的代碼中移除了重複的邏輯,但其實buildIndex還有更多可以移除的。

構造數組的第一個map步驟需要一個閉包,該閉包是(Sting)-> Character.你可能會注意到這個和你剛剛添加的firstLetter函數的類型完全一樣,也就是說你可以直接映射結果。

利用這方便的知識,你可以重寫一下函數:

func buildIndex(words: [String]) -> [Entry] { 
    func firstLetter(str: String) -> Character { 
        return Character(str.substringToIndex( advance(str.startIndex, 1)).uppercaseString) 
    } 
        return distinct(words.map(firstLetter)) .map { 
        (letter) -> Entry inreturn (letter, words.filter { 
            (word) -> Bool in 
            firstLetter(word) == letter }) 
        } 
} 

最終版的代碼非常簡潔,但卻非常有表現力。

也許你也注意到了到目前爲止你使用的函數技術實現的一個副作用。雖然你有時需要依賴變量(就像用var關鍵字定義),但你已經在函數中定義了所有的變量爲常數(用let)

你應該儘量保持不可變性,因爲不可變類型更容易測試維護。函數編程和不可變類型通常會一起使用。因此,你的代碼會更簡潔,也更不容易出錯。是不是看其來很酷!

挑戰:現在buildIndex返回的是一個沒有排序的index索引;Entry實例的順序,用輸入參數單詞數組的順序來排。你現在可以試試按照字母的順序重新排序呢。對於字符串數組,實現時將會輸出以下結果:
[(C, [Cat, Chicken]),
(D, [Dog]),(F, [fish]),
(G, [Guinea Pig]),
(M, [Mouse, monkey])]

提示:Swift的數組中有一個排序方法,但這種方法是修改數組值而不是返回一個新的排序後的數組,只有可變數組纔可以操作。一般情況下,處理不變類型的數據更安全些,所以我不建議用那個方法。作爲替代方法,使用返回第二排序數組的排序方法。

當然,如果你實在ko不了這個問題,資源代碼裏有,可自行查看。

Partial application and currying - partial和currying

前面三節內容都涉及了數組操作。有了三件套的map,reduce和filter,你幾乎可以執行任何類型的數據處理任務。此外,如果你還想要執行一個這三件套中沒有的功能方法,自己添加也很容易(如mdistinct)。
集合操作是個非常棒的函數編程技術,在Swift中還能實現更強大的功能。這節主要來看下局部應用,以及他的好朋友currying。

Partial application - 局部應用

新建一個playground並更新裏面的內容如下:

import Foundation

let data = "5,7;3,4;55,6" 

這個常數有點像是個結構化的數據:以分號分隔的數字組,每組由逗號分隔數字。這組數據可能是一個csv文件或者一個表中加價減價的內容。

處理分隔的數據通常要用到拆分,更新代碼看看:

print(data.componentsSeparatedByString(";"))

輸出:[“5,7”, “3,4”, “55,6”]
componentsSeparatedbystring是定義在NSString中的方法,這也是爲什麼要在playground中導入Foundation的原因。該方法根據提供的分隔符來分隔字符串。在這個代碼中,用分號返回了一個包含有三個字符的數組。

在處理分隔的數據時,你經常會發現在同一個分隔符上會多次分隔。有沒有更好的分流功能來指定你的數據分隔符的類型?

你可以創建一個分號的分隔方法,但部分應用程序提供了更通用的解決方法,如下:

func createSplitter(separator:String) -> (String -> [String]) { 
    func split(source:String) -> [String] { 
        return source.componentsSeparatedByString(separator) 
    } 
    return split 
} 

這是一個看上去很古怪的代碼,讓我們來分步拆解。

createSplitter需要一個字符串參數並返回一個類型(String->[String])的值。這是一個需要字符串參數並返回一個字符串數組的函數類型。

在createSplitter的內容,你創建了一個叫split的內部函數作爲所需要的返回類型,你通過source.componentsSeparatedByString實現這個內部函數。

零零碎碎的總結理解,createSplitter就像一個新的函數工廠。當用一個分隔符調用時,將返回一個基於給定分隔符的字符串的函數。

這使用了first—class以及閉包,你在閉包中捕獲分隔符參數的值。

是時候來用用這個方法了,在playground中添加代碼:

let commaSplitter = createSplitter(",") 
print(commaSplitter(data)) 
let semiColonSplitter = createSplitter(";") 
print(semiColonSplitter(data)) 

你將看到輸出:

[“5”, “7;3”, “4;55”, “6”]
[“5,7”, “3,4”, “55,6”] 

控制檯輸出的第一行使用逗號作爲分隔符進行的拆分,第二行用的分號進行拆分。當然你也可以反覆使用逗號或分號進行分割!(幹嘛不試試呢?)

你的代碼用了一個叫局部應用的概念,這是一個用一個或多個參數函數來生成新的函數的的“固定”過程。在這個例子中,createSplitter利用componentsseparatedbystring創建了一個新的函數。

這是個非常簡單的例子,你的部分應用了一個有一個單一參數的方法,用以創建一個沒有任何參數的函數。你會在這一節的後面看到更復雜的例子,但在這之前,讓我們來看看currying!

curry在新版本的Swift中移除了,所以就不再講解了

在當前實現的createSplitter中,你手動創建了一個返回外部結果的一個內部函數。消除不需要手動構建的內部函數。

替換createSplitter 如下:

func createSplitter(separator:String)(source:String) -> [String] { 
    return source.componentsSeparatedByString(separator) 
} 

更新下createSplitter的用法並編譯:

let commaSplitter = createSplitter(",") 
print(commaSplitter(source: data)) 
let semiColonSplitter = createSplitter(";") 
print(semiColonSplitter(source: data)) 

這個由createSplitter產生的函數需要外部參數名稱,因此添加source:上面的代碼名稱。有了這個更新的代碼,你將會在控制檯看到完全輸出結果。

[“5”, “7;3”, “4;55”, “6”] 
[“5,7”, “3,4”, “55,6”] 

更新的createSplitter更簡潔了,但它是如何工作的呢。你已經在函數的簽名標記處做了些微妙的變化。之前的樣子:

(separator:String) -> (String -> [String]) 

現在:

(separator:String)(source:String) -> [String] 

這個看上去是個非常奇怪的函數。到目前爲止你看到的所有函數都遵循一個相同的模式:在括號裏是參數,後面緊跟返回的類型。上面的標記中在括號裏有兩個參數。

當你調用這個函數createSplitter(“,”)返回一個由給定分隔符綁定的值的新的函數。這和簽名的例子有很大的不同,但可將你從手動構造一個內部函數中解放了出來。
curried函數肯定需要花費一些時間來適應。也許一些例子可以幫助你理解。

A hotter curry - 進一步瞭解curry

在playground中添加代碼:

func addNumbers(one:Int, two:Int, three:Int) -> Int { 
    return one + two + three 
} 
let sum = addNumbers(2, 5, 4) 
print(sum) 

這裏的代碼可能也需要解釋下…嗯,這定義了一個函數,嗯,這個函數添加三個整數。
現在用等效代碼curried:

func curryAddNumbers(one:Int)(two:Int)(three:Int) -> Int { 
    return one + two + three 
} 

再次注意簽名中的一些微妙差異,將代碼添加到playground上測試下

let stepOne = curryAddNumbers(2) 
let stepTwo = stepOne(two: 5) 
let result = stepTwo(three: 4);
print(result) // 11 

從上面可以看到,爲了添加三個數字,你執行了一系列的功能,每一此返回都是一個提供有綁定了參數值的新的函數,最後的函數返回結果。

當然,你可以按照函數的順序執行,而不在每個中間步驟分配變量和常數。

let result2 = curryAddNumbers(2)(two: 5)(three: 4) 
print(result2) // 11 

正如你看到那樣,curried函數只是簡單的給每個給定的參數生成一個新的函數的功能。

最後一個例子,添加到playground中:

func curryAddNumbers2(one:Int, two:Int)(three:Int) -> Int { 
    return one + two + three 
}
let result3 = curryAddNumbers2(2, 5)(three: 4) 
print(result3) // 11 

這演示了每個步驟中是一個參數或多個參數的函數序列。

Practical currying - 實際使用的currying

前面的例子可能有些誇張-你不太可能會創建一個curried函數只是爲了添加三個數字!在這一節中,你會建立一些更實際的東西。

添加代碼:

let text = "Swift"let paddedText = text.stringByPaddingToLength(10, withString: ".", startingAtIndex: 0) 
print(paddedText) 

上面的代碼塊在Swift中迅速生成一個長度爲10個點的字符,並輸出:

Swift..... 

stringbypaddingtolength 是非常靈活的,可以指定填充字符串的長度以及在哪開始填充字符串。大多數時候,你不需要全部的靈活性,讓我們用currying去提供一個簡單的字符串填充方法。

添加代碼:

func curriedPadding(startingAtIndex: Int, withString: String) (source: String, length: Int) -> String { 
    return source.stringByPaddingToLength(length,withString: withString, startingAtIndex:           startingAtIndex); 
} 

上面的curried函數需要startingIndex和withString參數。返回一個你填充到指定長度的字符串。

在playground上創建填充函數

let dotPadding = curriedPadding(0, ".") 

這個調用方法提供了起始索引和填充的字符。dotPadding常數是個函數且類型爲(String,Int)-> String.

接着來測試下新的函數:

let dotPadded = dotPadding(source: "Curry!", length: 10) 
print(dotPadded) 

現在輸出:Curry!....

當然你可以使用dotPadding來不斷填充。

爲什麼不用curriedPadding去創建一個新的填充函數,或嘗試着創建一個自己的curried函數。

Where to go from here? - 接着幹什麼?

我希望這一章讓你感受到了一點函數式的編程的味道,並希望你能在應用程序中使用它。正如在引言中提到的。函數式編程在Swift開發中並不是強制使用的。然而一旦你掌握了這一章的技術,你就會發現你的應用程序可以更簡潔,更富有表現力和趣味性。

函數概念貫穿本書:回憶下,看看我們現在有多少地方使用到了。例如在第四章的“泛型”中你會用到reduce去收集地圖的pin位置。在第九章“Swift vs oc”中你將用函數式編程實現訪問者模式。

這一章目前還遠不夠詳盡;你可以在各式各樣的環境中應用到函數概念。發揮你的想象力和創造力!## 標題 ##

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章