Swift函數(shù)式編程教程

前言

本文翻譯自Swift Functional Programming Tutorial
翻譯的不對的地方還請多多包涵指正美莫,謝謝~

Swift函數(shù)式編程教程

當(dāng)從Objective-C(文章其余地方將簡稱OC)編程轉(zhuǎn)移到Swift過程中,將OC中的概念映射到Swfit是非常符合邏輯的慎陵。你知道在OC中如何創(chuàng)建類示血,那在Swift也是一樣。當(dāng)然,Swfit有一些完全新的特性諸如泛型和范圍操作數(shù)祠墅,但仍然還有你已經(jīng)知道一些小的非常精妙的技術(shù)。(OK歌径,可能也不那么谢汆隆!)

但回铛,Swfit不僅僅是為應(yīng)用提供了一個(gè)更好的語法狗准。使用這門新語言,你能有機(jī)會改變解決問題及編碼的思路茵肃。結(jié)合Swift腔长,函數(shù)式編程技術(shù)在你的編程武器中成為了一個(gè)可行的重要的部分。

函數(shù)式編程是一個(gè)理論性強(qiáng)的話題验残,因此這篇教程將通過例子來說明它捞附。你將看到許多的函數(shù)編程的例子,這些例子看起來是熟悉胚膊、命令式的編程方式故俐,之后你可以盡所能在解決相同問題的時(shí)考慮使用函數(shù)式編程技術(shù)。

注意:這篇Swfit教程是假定你已有Swift編程的基礎(chǔ)紊婉。如果你對Swfit很陌生药版,我們建議你先看看其他Swfit的教程

什么是函數(shù)式編程?

簡潔的說喻犁,函數(shù)式編程是是一種編程范式槽片,強(qiáng)調(diào)通過數(shù)學(xué)式的函數(shù)來計(jì)算,函數(shù)具有永恒不可變性及表達(dá)式語法肢础,盡可能少地使用參數(shù)和狀態(tài)位的特性还栓。

因?yàn)闃O少有共享的狀態(tài)且每個(gè)函數(shù)像是應(yīng)用代碼海洋里的一座孤島,所以它更加容易測試传轰。函數(shù)式編程之所以流行是因?yàn)樗沟卯惒胶筒⑿懈雍唵蔚貐f(xié)同工作动猬。為當(dāng)今多核時(shí)代提供了一種提高性能的途徑作箍。

是時(shí)候開始將方法變?yōu)楹瘮?shù)式方式了案狠!

簡單的數(shù)組篩選

以一個(gè)非常簡單的事情作為開始:一個(gè)簡單的數(shù)學(xué)位計(jì)算哈扮。你的第一個(gè)任務(wù)是寫一個(gè)簡單的Swift寫的用于找到1到10中的偶數(shù)的函數(shù)链瓦。雖是一個(gè)非常小的任務(wù),確是對一個(gè)函數(shù)式編程很棒的介紹。

老的篩選方法

創(chuàng)建一個(gè)Swift的工作環(huán)境,保存在你喜歡的任何地方异袄。然后貼下面的代碼到你新創(chuàng)建的Swift文件中:

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

該函數(shù)生成出了我們預(yù)先想要的結(jié)果

[2, 4, 6, 8, 10]

(如果你看不到控制臺的輸出,記得通過View/Assistant Editor/Show Assistant Editor選項(xiàng)打開輔助編輯)

這塊小代碼很簡單玛臂,核心算法是這樣的:

  1. 創(chuàng)建一個(gè)空數(shù)組烤蜕;
  2. 1-10的循環(huán)迭代器(記得是1...10是包含1和10的);
  3. 若符合條件(數(shù)字是偶數(shù))迹冤,則添加到數(shù)組內(nèi)讽营;

上述代碼是實(shí)質(zhì)上是命令式編程。給出明確的使用一些基本的控制命令諸如if,for-in指令告訴計(jì)算機(jī)找出偶數(shù)叁巨。

代碼運(yùn)行的很好斑匪,只是有一點(diǎn)驗(yàn)證數(shù)字是否是偶數(shù)是隱藏在循環(huán)當(dāng)中的。而且存在一些嚴(yán)重耦合锋勺,將偶數(shù)添加到數(shù)組的動作包含在條件當(dāng)中。如果你希望在App的其他地方打印偶數(shù)數(shù)組狡蝶,只能通過拷貝粘貼的方式來重用代碼庶橱。

讓我們來改成函數(shù)式吧~

函數(shù)式篩選

將以下代碼貼到你的文件中:

func isEven(number: Int) -> Bool {
  return number % 2 == 0
}
evens = Array(1...10).filter(isEven)
println(evens)

可以看到,函數(shù)式編程的結(jié)果跟命令式編程一樣:

[2, 4, 6, 8, 10]

讓我們看的更仔細(xì)點(diǎn)贪惹。它由兩部分組成:

  1. 數(shù)組代碼段是一個(gè)簡單方便生成包含1到10的數(shù)組方式苏章。范圍操作數(shù)...創(chuàng)建一個(gè)包含兩端點(diǎn)的范圍;
  2. 篩選聲明處是函數(shù)式編程模仿所在奏瞬。這個(gè)篩選方法枫绅,對數(shù)組是顯示的(基礎(chǔ)方法),創(chuàng)建并返回了一個(gè)新的數(shù)組硼端,該數(shù)組包含了只有通過篩選器里的函數(shù)返回True的元素并淋。在這個(gè)例子中,isEven提供給了filter珍昨。

isEven函數(shù)作為參數(shù)傳遞給了filter函數(shù)县耽,但記住函數(shù)僅僅是有名字的閉包。試試添加以下更加簡明的代碼到你的文件中:

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

再一次镣典,確認(rèn)三個(gè)方法的返回結(jié)果是一致的兔毙。以上代碼可以說明編譯器從使用上下文推斷出參數(shù)number的類型并且返回閉包的類型。

如果你想讓你的代碼更加簡明兄春,再做一步這么來寫:

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

上述代碼使用了參數(shù)簡寫澎剥,隱式返回,類型推薦...且成功了赶舆!

使用簡寫的參數(shù)表達(dá)是一種習(xí)慣或者偏好哑姚。個(gè)人來說趾唱,覺得像上面簡單的例子,簡寫參數(shù)很好蜻懦。但是甜癞,對于更加復(fù)雜的情況我更傾向于顯示的參數(shù)表達(dá)。編譯器雖然不關(guān)心變量名字宛乃,但它們可以創(chuàng)建一個(gè)與人類不同的世界S圃邸(原文連續(xù)的意思是說,不寫參數(shù)名計(jì)算機(jī)可以任意翻譯人們寫的代碼征炼,構(gòu)建出不同的含義析既,這并不是我們想要的。)

函數(shù)式的寫法顯然比命令式的更加簡潔谆奥。這個(gè)簡單的例子展示出了所有函數(shù)式語言所具有的一些有趣的特性:

  1. 高階函數(shù):這種函數(shù)的參數(shù)一個(gè)函數(shù)眼坏,或者返回值是一個(gè)函數(shù)。在這個(gè)例子中酸些,filter就是一個(gè)高階函數(shù)宰译,它可以接收一個(gè)函數(shù)作為參數(shù);
  2. 一級函數(shù):你可以將函數(shù)當(dāng)做是任意變量魄懂,可以將它們賦值給變量沿侈,也可以將它們作為參數(shù)傳給其他函數(shù);
  3. 閉包:實(shí)際上就是匿名函數(shù)市栗;

你可能注意到OC中的block也有一些類似的特性缀拭。但swfit在函數(shù)編程上走的更遠(yuǎn),它通過混合使用更加簡明的語法和內(nèi)建的函數(shù)諸如filter填帽。

Filter篩選器背后的魔法

Swift有很多功能化方法蛛淋,例如map, join, reduce...。那么在這些方法的背后是怎么實(shí)現(xiàn)的呢篡腌?

我們來看看filter背后的魔法并加上我們自己的實(shí)現(xiàn)褐荷。

在相同的文件中,添加以下代碼:

func myFilter<T>(source: [T], predicate:(T) -> Bool) -> [T] {
  var result = [T]()
  for i in source {
    if predicate(i) {
      result.append(i)
    }
  }
  return result
}

上述代碼是一個(gè)泛型函數(shù)哀蘑,它接受一個(gè)包含類型T的數(shù)組的源(source)和一個(gè)以T類型實(shí)例為入?yún)⒉⒎祷豣ool值得判定函數(shù)(predicate)诚卸。

myFilter函數(shù)看起來更像是我們剛開始寫的命令式函數(shù)。主要區(qū)別在于你可以提供一個(gè)檢查條件的函數(shù)而不是硬編碼在函數(shù)中绘迁。

試試新添加的實(shí)現(xiàn)合溺,添加以下代碼:

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

再一次說明,輸出結(jié)果是一樣的缀台!

挑戰(zhàn):上述方法是全局的棠赛,看看你是否能讓它變成數(shù)組的一個(gè)方法。

  1. 可以通過擴(kuò)展Array添加myFilter方法;
  2. 可以擴(kuò)展Array睛约,但不是Array[T](泛型擴(kuò)展)鼎俘。這意味著需要通過self遍歷數(shù)組且需要強(qiáng)制轉(zhuǎn)換類型。

Reducing

之前的是一個(gè)很簡單的例子辩涝,只用了一個(gè)函數(shù)式方法贸伐。接下來,在上面的基礎(chǔ)上怔揩,運(yùn)用函數(shù)式編程技術(shù)實(shí)現(xiàn)更加復(fù)雜的邏輯捉邢。

創(chuàng)建一個(gè)新的工作文件,準(zhǔn)備下一個(gè)任務(wù)吧~

Manual reduction

本階段你的任務(wù)會復(fù)雜一點(diǎn)點(diǎn):取出1到10的偶數(shù)并求和商膊。這就是熟知的reduce函數(shù)伏伐,接收一組輸入且返回一個(gè)輸出。

我相信你有能力自己寫完這個(gè)邏輯晕拆,但我已經(jīng)寫好了~ 添加以下代碼到工作文件:

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

結(jié)果如下:

30

上面的命令行代碼還是和之前的例子一樣藐翎,在for-in循環(huán)語句中做加法。

讓我們來看看函數(shù)式會如何編寫吧~

函數(shù)式Reduce(Functional Reduce)

添加如下代碼到你的工作區(qū):

evenSum = Array(1...10)
    .filter { (number) in number % 2 == 0 }
    .reduce(0) { (total, number) in total + number }
 
println(evenSum)

你會看到結(jié)果是:

30

上述代碼段包含了數(shù)組的創(chuàng)建和filter的使用实幕。這兩個(gè)操作的結(jié)果是五個(gè)數(shù)字的數(shù)組[2, 4, 6, 8, 10]吝镣。最后一步使用的是reduce

reduce是功能極其豐富的數(shù)組方法茬缩,可以為數(shù)組的每一個(gè)元素執(zhí)行一個(gè)方法赤惊,并累加結(jié)果。

為了理解reduce是如何工作的凰锡,看看它的簽名是有幫助的。

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

第一個(gè)參數(shù)是類型為U的初始值圈暗。在目前的代碼中掂为,初始值是0且是Int型的(這里的U就一直是Int了)。第二個(gè)參數(shù)是combine函數(shù)员串,這個(gè)函數(shù)會對每個(gè)數(shù)組元素執(zhí)行一遍勇哗。

combine接收連個(gè)參數(shù),第一個(gè)是類型是U寸齐,且是上一個(gè)combine函數(shù)的調(diào)用結(jié)果欲诺;第二個(gè)參數(shù)是正在執(zhí)行combine函數(shù)的數(shù)組元素。reduce執(zhí)行的返回結(jié)果就是最后一個(gè)combine函數(shù)的返回結(jié)果渺鹦。

這里面繁盛了很多事情扰法,我們可以一步步拆解它。

在代碼中毅厚,第一個(gè)reduce后的結(jié)果如下:

first
first

combine第一個(gè)入?yún)⒌某跏贾凳?塞颁,入?yún)⒌牡诙€(gè)參數(shù)是數(shù)組的第一個(gè)值2。combine函數(shù)將它們兩想加,返回2祠锣。

第二個(gè)循環(huán)如下圖介紹:

在第二個(gè)循環(huán)酷窥,combine的入?yún)⑹堑谝粋€(gè)combine的返回值和下一個(gè)數(shù)組元素值。將他們兩想加即2 + 4 = 6伴网。

繼續(xù)循環(huán)處理所有的數(shù)組元素可以得到下面這張表:

加粗且邊上有星號的值就是最終的執(zhí)行結(jié)果蓬推。

這是一個(gè)簡單例子;事實(shí)上澡腾,使用reduce你可以執(zhí)行各種各樣有趣的功能強(qiáng)大的轉(zhuǎn)換沸伏。下面是幾個(gè)運(yùn)用例子。

添加以下代碼到你的工作區(qū):

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

這段代碼用于找出數(shù)組中的最大值蛋铆。這個(gè)例子中馋评,返回結(jié)果顯而易見~ 在這里要記住,結(jié)果就是最終reduce執(zhí)行的計(jì)算max數(shù)值的循環(huán)結(jié)果刺啦。

如果你還在努力思索為何是這么運(yùn)行的留特,為什么創(chuàng)建類似于上述表格來記錄每次combine的入?yún)⒑头祷刂的亍?/p>

至今為止你所見到的reduce都是將一個(gè)整型數(shù)組轉(zhuǎn)換為一個(gè)整型數(shù)字。顯然玛瘸,reduce有兩個(gè)參數(shù)蜕青,U和T,是不同的糊渊,而且必須得他們沒必要是整型右核。這意味著你能將包含一個(gè)類型的數(shù)組轉(zhuǎn)換成另一種類型的數(shù)。

添加以下代碼到你的工作文件中:

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

執(zhí)行結(jié)果如下:

numbers: 1 2 3 4 5 6 7 8 9 10

這個(gè)例子展示了reduce將一個(gè)整型數(shù)組轉(zhuǎn)換成一個(gè)String數(shù)字渺绒。
通過一些練習(xí)贺喝,你會發(fā)現(xiàn)可以用各種各樣的有趣且好玩的方式使用reduce

挑戰(zhàn):看看你是否能將一個(gè)包含位的數(shù)組轉(zhuǎn)換成一個(gè)整型值宗兼。輸入如下:

let digits = ["3", "1", "4", "1"]

你的reduce結(jié)果需要返回3141的整形值躏鱼。

Reduce背后的魔法

在之前的段落中,你實(shí)現(xiàn)了自己的簡單的filter的方法∫笊埽現(xiàn)在我們來實(shí)現(xiàn)自己的reduce方法染苛。

添加如下代碼到工作區(qū):

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方法到數(shù)組中,模仿了內(nèi)建的reduce函數(shù)主到。這個(gè)函數(shù)簡單的遍歷每個(gè)數(shù)組元素茶行,每一次遍歷都會調(diào)用combiner函數(shù)。

為測試上述代碼登钥,可以用myReduce替換掉當(dāng)前工作區(qū)內(nèi)的reduce函數(shù)畔师。

此時(shí),你也許會想怔鳖,“我為什么會想要自己實(shí)現(xiàn)filterreduce方法茉唉?”固蛾。回答是度陆,“和很可能不會~”

但是艾凯,你可能會想要在Swfit中擴(kuò)展當(dāng)前的函數(shù)范式,實(shí)現(xiàn)你自己想要的函數(shù)式方法懂傀≈菏看明白并且理解函數(shù)式多么容易實(shí)現(xiàn)對于你自己去實(shí)現(xiàn)一些強(qiáng)大的諸如reduce樣的函數(shù)是非常重要且有激勵性的。

建立索引

是時(shí)候解決一些更復(fù)雜的問題了蹬蚁,意味著你要新建另一個(gè)新的工作區(qū)啦恃泪。你明白你想這么做~

在本段中,你將使用函數(shù)式編程技術(shù)把一組單詞通過他們的首字母分成不同的組犀斋。
在你新創(chuàng)建的工作區(qū)內(nèi)添加如下代碼:

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

為了完成本階段的任務(wù)贝乎,你將通過首字母將他們分組(大小寫不敏感)。添加以下代碼做準(zhǔn)備:

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

Entry為每個(gè)entry定義了元祖類型叽粹。這個(gè)例子中使用類型重命名是代碼更易讀览效,全篇不用重復(fù)的說明元祖類型。你將會在buildIndex函數(shù)內(nèi)添加建立索引的代碼虫几。

命令式建立索引

命令式索引方法如下:

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

函數(shù)分為兩個(gè)部分锤灿,每一個(gè)部分都是for循環(huán)。前一部分是根據(jù)單詞數(shù)組的第一個(gè)字母建立首字母數(shù)組辆脸;后半部分遍歷首字母數(shù)組但校,把找到以首字母開頭的單詞加入到對應(yīng)的數(shù)組。

這個(gè)實(shí)現(xiàn)得到我們期待的結(jié)果如下:

[(C, [Cat, Chicken]),
 (F, [fish]),
 (D, [Dog]),
 (M, [Mouse, monkey]),
 (G, [Guinea Pig])]

(上面的輸出稍微做了整理)

該命令式實(shí)現(xiàn)花了很多的步數(shù)和嵌套循環(huán)啡氢,這使得它很難理解状囱。讓我們來看看函數(shù)式編程是如何怎樣的。

函數(shù)式編程建立索引

創(chuàng)建一個(gè)新的工作區(qū)間并添加如下代碼:

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

這時(shí)輸出是個(gè)空數(shù)組:

[]

第一步是建立索引倘是,將單詞數(shù)組轉(zhuǎn)換成包含單詞首字母的數(shù)組浪箭。更新buildIndex函數(shù)如下:

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

工作區(qū)會輸出大寫字母的數(shù)組,每一個(gè)都對應(yīng)輸入單次數(shù)組的單次的每個(gè)元素辨绊。

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

在之前的段落中,我們遇到了filter, reduce等函數(shù)匹表。上面的代碼中门坷,我們來介紹map,另一個(gè)數(shù)組內(nèi)建API袍镀。

map函數(shù)對給定的數(shù)組的每個(gè)元素調(diào)用提供的閉包默蚌,并用每次調(diào)用的結(jié)果生成一個(gè)新的數(shù)組。你可以使用map來做轉(zhuǎn)換苇羡;在這個(gè)例子中绸吸,mapString數(shù)組轉(zhuǎn)換成Character數(shù)組。

目前首字母數(shù)組包含了一些重復(fù)元素,但你期望的數(shù)組是沒有重復(fù)元素的锦茁。不幸的是攘轩,swift數(shù)組并沒有提供內(nèi)建函數(shù)去重。意味著你將要自己寫一個(gè)~

在之前的段落中码俩,你發(fā)現(xiàn)重新實(shí)現(xiàn)filter, reduce還是很簡單的度帮。所以毫無疑問添加一個(gè)去重函數(shù)也是小菜一碟~

添加如下代碼在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遍歷數(shù)組,建立一個(gè)元素唯一的新數(shù)組稿存。
distinct函數(shù)運(yùn)用到buildIndex中:

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

你的工作區(qū)將會輸出去重后的字母:

[C, F, D, M, G]

現(xiàn)在你有了首字母去重后的數(shù)組笨篷,下一步就是將字母轉(zhuǎn)變成Entry元祖了。聽起來是不是很像一個(gè)轉(zhuǎn)換瓣履?這將是map的另一個(gè)工作啦~

更新buildIndex如下:

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

第二次調(diào)用map的目的是將字符數(shù)組裝換成元祖數(shù)組率翅。目前輸出:

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

(再次,上面也是整理過啦~)

就快完成了袖迎。最后一步工作用給定字符開頭的單詞生成對應(yīng)的Entry冕臭。更新函數(shù)并添加嵌套的filter如下:

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

輸出結(jié)果如下:

[(C, [Cat, Chicken]),
 (F, [fish]),
 (D, [Dog]),
 (M, [Mouse, monkey]),
 (G, [Guinea Pig])]

在第二部分,使用的嵌套調(diào)用是filter而不是map瓢棒。filter能位不同的字符篩選出對應(yīng)的單次數(shù)組浴韭,并且是根據(jù)首字母來定位的。

以上實(shí)現(xiàn)已經(jīng)比命令式更加簡潔明了脯宿,但是仍有提高空間念颈;上述代碼取出操作和大寫轉(zhuǎn)換操作太多次了。移除這些重復(fù)性是非常好的连霉。

如果這是OC代碼榴芳,你可能會有幾種方式來優(yōu)化:你可以創(chuàng)建公共方法,將方法直接通過category添加到NSString類中跺撼。但是窟感,如果你僅僅是希望在buildIndex使用,一個(gè)公共方法顯示不夠清晰且有些過度歉井。

幸運(yùn)的是柿祈,使用swift,有更好的方法~

更新代碼如下:

func buildIndex(words: [String]) -> [Entry] {
  func firstLetter(str: String) -> Character {
    return Character(str.substringToIndex(
            advance(str.startIndex, 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函數(shù)哩至,該函數(shù)嵌套在buildIndex中躏嚎,而且對于外部函數(shù)是隱藏的(本地函數(shù))。利用swift一階函數(shù)的特性菩貌,你可以想使用變量一樣使用它卢佣。

新的代碼合并了重復(fù)邏輯,但還有很多可做的事來整理buildIndex箭阶。

第一步map是使用(String) 轉(zhuǎn) Character簽名閉包生成字母數(shù)組虚茶。你可能會發(fā)現(xiàn)這個(gè)函數(shù)跟添加的firstLetter是一樣的戈鲁,這意味著你可以直接將它傳給map

使用這個(gè)知識點(diǎn)后嘹叫,可以將函數(shù)寫成如下:

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 in
      return (letter, words.filter {
        (word) -> Bool in
        firstLetter(word) == letter
      })
    }
}

最終結(jié)果是很簡潔的婆殿,表達(dá)清晰的。
也許現(xiàn)在你注意到函數(shù)式編程有趣的一面~ 命令式解決方案需要依賴于變量(變量關(guān)鍵詞var)待笑,而對應(yīng)的在函數(shù)式里定義所有值都是常量(通過let)鸣皂。

你應(yīng)該更多積極地使用常量,常量易于測試且方便并行暮蹂。函數(shù)式編程和不可變類型往往是緊密相連的寞缝。結(jié)果,你的代碼會更加簡明同時(shí)也不易出錯仰泻。而且代碼看起來很酷荆陆,使你的朋友刮目相看。

挑戰(zhàn):目前集侯,buildIndex返回了一個(gè)未排序的索引被啼;Entry的順序取決于輸入單詞數(shù)組的元素順序。你的任務(wù)是將Entry數(shù)組按照它的字母排序棠枉。對于上面的例子浓体,你需要輸入如下:

[(C, [Cat, Chicken]),
 (D, [Dog]),
 (F, [fish]),
 (G, [Guinea Pig]),
 (M, [Mouse, monkey])]

答案:

swift數(shù)組有個(gè)sort函數(shù),但是這個(gè)方法會改變操作的數(shù)組而不是返回一個(gè)新的排序好的實(shí)例辈讶,并且它需要操作的數(shù)組是一個(gè)可變數(shù)組命浴。總之贱除,處理不可變數(shù)據(jù)更加安全生闲,因此建議你不要使用該函數(shù)!作為替代月幌,使用sorted方法會返回一個(gè)新的數(shù)組碍讯。

何去何從

這里是函數(shù)式編程教程所有完整的代碼

恭喜~ 你已經(jīng)有swift函數(shù)式編程的實(shí)戰(zhàn)經(jīng)驗(yàn)了扯躺。你不僅學(xué)會了如何使用函數(shù)式方法諸如:map, reduce等捉兴,還知道如何自己實(shí)現(xiàn)這些方法,而且學(xué)會了用函數(shù)式思考录语。

如果你希望學(xué)到更多函數(shù)式編程的知識轴术,可以查看整個(gè)章節(jié),那里會講的更加深入也包括部分應(yīng)用功能钦无。

希望看到你自己的App中使用了函數(shù)式編程技術(shù)~~~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市盖袭,隨后出現(xiàn)的幾起案子失暂,更是在濱河造成了極大的恐慌彼宠,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,542評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件弟塞,死亡現(xiàn)場離奇詭異凭峡,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)决记,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,596評論 3 385
  • 文/潘曉璐 我一進(jìn)店門摧冀,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人系宫,你說我怎么就攤上這事索昂。” “怎么了扩借?”我有些...
    開封第一講書人閱讀 158,021評論 0 348
  • 文/不壞的土叔 我叫張陵椒惨,是天一觀的道長。 經(jīng)常有香客問我潮罪,道長康谆,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,682評論 1 284
  • 正文 為了忘掉前任嫉到,我火速辦了婚禮沃暗,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘何恶。我一直安慰自己孽锥,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,792評論 6 386
  • 文/花漫 我一把揭開白布导而。 她就那樣靜靜地躺著忱叭,像睡著了一般。 火紅的嫁衣襯著肌膚如雪今艺。 梳的紋絲不亂的頭發(fā)上韵丑,一...
    開封第一講書人閱讀 49,985評論 1 291
  • 那天,我揣著相機(jī)與錄音虚缎,去河邊找鬼撵彻。 笑死,一個(gè)胖子當(dāng)著我的面吹牛实牡,可吹牛的內(nèi)容都是我干的陌僵。 我是一名探鬼主播,決...
    沈念sama閱讀 39,107評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼创坞,長吁一口氣:“原來是場噩夢啊……” “哼碗短!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起题涨,我...
    開封第一講書人閱讀 37,845評論 0 268
  • 序言:老撾萬榮一對情侶失蹤偎谁,失蹤者是張志新(化名)和其女友劉穎总滩,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體巡雨,經(jīng)...
    沈念sama閱讀 44,299評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡闰渔,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,612評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了铐望。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片冈涧。...
    茶點(diǎn)故事閱讀 38,747評論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖正蛙,靈堂內(nèi)的尸體忽然破棺而出督弓,到底是詐尸還是另有隱情,我是刑警寧澤跟畅,帶...
    沈念sama閱讀 34,441評論 4 333
  • 正文 年R本政府宣布咽筋,位于F島的核電站,受9級特大地震影響徊件,放射性物質(zhì)發(fā)生泄漏奸攻。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,072評論 3 317
  • 文/蒙蒙 一虱痕、第九天 我趴在偏房一處隱蔽的房頂上張望睹耐。 院中可真熱鬧,春花似錦部翘、人聲如沸硝训。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,828評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽窖梁。三九已至,卻和暖如春夹囚,著一層夾襖步出監(jiān)牢的瞬間纵刘,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,069評論 1 267
  • 我被黑心中介騙來泰國打工荸哟, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留假哎,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,545評論 2 362
  • 正文 我出身青樓鞍历,卻偏偏與公主長得像舵抹,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子劣砍,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,658評論 2 350

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