原文連接:a practical introduction to functional programming
原文日期:2015/08/10
介紹
Swift 為 iOS 編程世界引入了一個(gè)新的范式:函數(shù)式范式。大多數(shù) iOS 開(kāi)發(fā)者之前都習(xí)慣了用 Objective-C 或者其他面向?qū)ο缶幊陶Z(yǔ)言,函數(shù)式的編碼和思考會(huì)變得有點(diǎn)燒腦(brain-addling)。
應(yīng)該從那里開(kāi)始學(xué)習(xí)呢?我找到了一些非常容易理解的例子 - 在Mary Rose Cook的 blog 中找到了一篇非常好的文章:A practical introduction to functional programming,這篇文章很好,足夠解答我的疑惑,而且這篇文章包含了很多示例代碼,我們可以在此基礎(chǔ)上加上函數(shù)式的補(bǔ)充。
我們的這篇文章會(huì)重新審視 Cook 的例子,并把它們用 Swift 實(shí)現(xiàn)。所以在你閱讀本篇文章之前,請(qǐng)首先閱讀她的文章。這篇文章不僅創(chuàng)造了很多例子,對(duì)于新手來(lái)講,它還清晰地解釋了什么是函數(shù)式編程,我不會(huì)再重復(fù)這些概念。
當(dāng)程序員們談?wù)摵瘮?shù)式編程時(shí),他們會(huì)提到很多叼炸天的"函數(shù)式"特性。。。但是請(qǐng)大家無(wú)視這些點(diǎn)。函數(shù)式代碼只描述了一件事情:沒(méi)有副作用。當(dāng)前函數(shù)不依賴函數(shù)之外的數(shù)據(jù),當(dāng)前函數(shù)也不會(huì)改變函數(shù)之外的數(shù)據(jù)。所有其他的"函數(shù)式"特性都是從此特性擴(kuò)展開(kāi)的。請(qǐng)把此特性作為你學(xué)習(xí)函數(shù)式編程的指導(dǎo)思想。
-Mary Rose Cook關(guān)于如何學(xué)習(xí)函數(shù)式編程的經(jīng)驗(yàn)
案例 #1 - Increment
/* 函數(shù)式代碼只描述了一件事情:沒(méi)有副作用。 */
/// 非函數(shù)式的 ///
var a = 0
func incrementUnfunctional() -> () {
a += 1
}
incrementUnfunctional()
print(a) // a = 1
/// 函數(shù)式的 ///
a = 0
func incrementFunctional(num: Int) -> Int {
return num + 1
}
a = incrementFunctional(a)
print(a) // a = 1
這兩個(gè)函數(shù)的區(qū)別在于如何增加變量 a - 函數(shù)incrementUnfunctional修改的是一個(gè)全局變量,而函數(shù)incrementFunctional是一個(gè)常規(guī)函數(shù):獲得一個(gè)數(shù)字,然后返回增加后的數(shù)字。這就是 Cook 提到的"沒(méi)有副作用":函數(shù)incrementFunctional沒(méi)有影響自身之外的變量的狀態(tài)。
cook 的課程 #1:不要在列表中使用循環(huán),使用map和reduce
任何一個(gè)深入研究函數(shù)式編程的程序員都會(huì)很快接觸到map、reduce和filter函數(shù)。這幾個(gè)函數(shù)非常強(qiáng)大,用于處理集合(collections)類型。下面我們來(lái)看map和reduce函數(shù)。
例子 #2 - Map 1
Map函數(shù)接收一個(gè)函數(shù)和一個(gè)集合。Map會(huì)生成一個(gè)新的空集合,使用傳入的函數(shù)處理集合中的每個(gè)元素并把返回值插入新集合,最后返回這個(gè)新集合。
/* 不要在列表中使用循環(huán),使用 map 和 reduce。 */
// Map 例子 #1
let languages = ["Objective-C", "Java", "Smalltalk"]
let languageLengths = languages.map { language in count(language) }
print(languageLengths) // [11, 4, 9]
let squares = [0, 1, 2, 3, 4].map { x in x * x }
print(squares) // [0, 1, 4, 9, 16]
正如例子展示的那樣,map的確返回了一個(gè)新集合——在這個(gè)例子中是數(shù)組(array)——使用一個(gè)匿名函數(shù)處理原始集合中的每個(gè)元素,同時(shí)原始集合保持不變。
例子 #3 - Map 2
/* 不要在列表中使用循環(huán),使用 map 和 reduce。 */
// Map 例子 #2
var languages = ["Objective-C", "Java", "Smalltalk"]
let newLanguages = ["Swift", "Haskell", "Erlang"]
/// 非函數(shù)式的 ///
for index in 0..<languages.count {
languages[index] = randomElement(newLanguages)
}
print(languages) // e.g. ["Haskell", "Haskell", "Swift"]
/// 函數(shù)式的 ///
let randomLanguages = languages.map { _ in randomElement(newLanguages) }
print(randomLanguages) // e.g. ["Haskell", "Haskell", "Swift"]
// 輔助方法
func randomElement(array: [String]) -> String {
let randomIndex = randomPositiveNumberUpTo(array.count)
return array[randomIndex]
}
func randomPositiveNumberUpTo(upperBound: Int) -> Int {
return Int(arc4random_uniform(UInt32(uppderBound)))
}
這里我們可以看到如何使用熟悉的方式得到一個(gè)隨機(jī)的編程語(yǔ)言數(shù)組 - 還有我們的函數(shù)式的補(bǔ)充實(shí)現(xiàn)。
例子 #4 - Reduce 1
Reduce函數(shù)接收一個(gè)函數(shù)和一個(gè)集合。返回一個(gè)合并了元素后創(chuàng)建的值。
/* 不要在列表中使用循環(huán),使用 map 和 reduce。 */
// Reduce 例子 #1
let sum = [0, 1, 2, 3, 4].reduce(0, combine: { $0 + $1 })
print(sum) // 10
Reduce理解起來(lái)比map難一些。Reduce從一個(gè)初始值(上面的例子中,初始值是 0)開(kāi)始積累一個(gè)值——在每一個(gè)集合元素上調(diào)用combine閉包并返回最后一個(gè)結(jié)果。
上面的例子使用了參數(shù)名稱縮寫(xiě)——$0和$1——這可能不太好懂。下面是另外一種表達(dá)方式,功能相同:
/* 不要在列表中使用循環(huán),使用map和reduce。 */
// Reduce 例子 #1 - 沒(méi)有參數(shù)名稱縮寫(xiě)
let numbers = [0, 1, 2, 3, 4]
let startingWith = 0
let sum = numbers.reduce(startingWith) {
(runningSum, currentNumber) in
runningSum + currentNumber
}
print(sum) // 10
我們從 0 開(kāi)始,使用加法連接runningSum和我們數(shù)字集合中的當(dāng)前值——最終完成求和。
例子 #5 - Reduce 2
Reduce不僅能用于數(shù)字集合——我們看看如何用它處理字符串集合。以下的函數(shù)會(huì)告訴我們,有多少個(gè)包含"hello"單詞的短語(yǔ):
/* 不要在列表中使用循環(huán),使用 map 和 reduce。 */
// Reduce 例子 #2
let greetings = ["Hello, World", "Hello, Swift", "Later, Objective-C"]
/// 非函數(shù)式的 ///
var helloCount = 0
for greeting in greetings {
if(string(greeting, contains:"hello")) {
helloCount += 1
}
}
print(helloCount) // 2
/// 函數(shù)式的 ///
let helloCountFunctional = greetings.reduce(0, combine: { $0 + ((string($1, contains:"hello")) ? 1 : 0) })
print(helloCountFunctional) // 2
// 協(xié)助代碼
func string(str: String, #contains: String) -> Bool {
return str.lowercaseString.rangeOfString(contains.lowercaseString) != nil
}
Swift 筆記:Map 和 Reduce
在 Swift 1.2 中,類似于map和reduce的函數(shù)在 Swift 庫(kù)中是全局函數(shù)——所以你得使用這樣的語(yǔ)法:map([0, 1, 2, 3, 4], { x in x * x })。Swift 2 提供了更加直觀的語(yǔ)法,正如你在上面的例子看到的那樣,可以在集合上直接調(diào)用map。這兩種方法的功能相同!
cook 的課程 #2 - 使用聲明式(Imperative)編程,不要使用命令式(Declarative)
一個(gè)函數(shù)式版本的命令式代碼將會(huì)是聲明式的。這類代碼描述的是要做什么,而不是如何做。。。把一段代碼打包成一些函數(shù),將會(huì)提高代碼的聲明性質(zhì)。
Objective-C 開(kāi)發(fā)者習(xí)慣使用命令式編程——這是一個(gè)編程范式,一系列的語(yǔ)句被用來(lái)修改狀態(tài)。函數(shù)式編程是聲明式編程中的一種形式——函數(shù)式編程的特征是使用函數(shù)來(lái)描述要做什么。
例子 #6 - Imperative 1
讓我們看看 Cook 的例子,下面這段代碼模擬了三個(gè)汽車的競(jìng)賽:
/*** 聲明式編程,不是命令式 ***/
// Imperative vs. Declarative - 實(shí)例 #1
/// Imperative - 第一次嘗試 ///
var time = 5
var carPositions = [1, 1, 1]
while(time > 0) {
time -= 1
print("\n")
for index in 0..<carPositions.count {
if(randomPositiveNumberUpTo(10) > 3) {
carPositions[index] += 1
}
for _ in 0..<carPositions[index] {
print("-")
}
print("\n")
}
}
// Output:
-
--
--
--
--
---
---
--
---
----
---
----
----
----
-----
例子 #7 - Imperative 2
一個(gè)有經(jīng)驗(yàn)的 Objective-C 開(kāi)發(fā)者看到上面那些代碼時(shí),會(huì)馬上意識(shí)到應(yīng)該把上面的代碼分解成更小的片段。
/*** 聲明式的編碼,而不是命令式的 ***/
// Imperative vs. Declarative - 例子 #2
/// Imperative - 第二次嘗試 ///
var time = 5
var carPositions = [1, 1, 1]
while(time > 0) {
runStepOfRace()
draw()
}
// Helpers
func runStepOfRace() -> () {
time -= 1
moveCars()
}
func draw() {
print("\n")
for carPosition in carPositions {
drawCar(carPosition)
}
}
func moveCars() -> () {
for index in 0..<carPositions.count {
if(randomPositiveNumberUpTo(10) > 3) {
carPositions[index] += 1
}
}
}
func drawCar(carPosition: Int) -> () {
for _ in 0..<carPosition {
print("-")
}
print("\n")
}
代碼更加簡(jiǎn)潔了,但仍然不是函數(shù)式的——其中的每一個(gè)函數(shù)沒(méi)有按照 Cook 教導(dǎo)我們的函數(shù)式實(shí)現(xiàn)的方式去做:"[函數(shù)不能]依賴當(dāng)前函數(shù)之外的數(shù)據(jù),并且[不能]改變當(dāng)前函數(shù)之外的數(shù)據(jù)"。
例子 #8 - Declarative
以下是一個(gè)函數(shù)式實(shí)現(xiàn)版本:
/*** 聲明式編程,不是命令式 ***/
// Imperative vs. Declarative - 例子 #3
/// 聲明式的 ///
typealias Time = Int
typealias Positions = [Int]
typealias State = (time: Time, positions: Positions)
let state: State = (time: 5, positions: [1, 1, 1])
race(state)
// 輔助函數(shù)
func race(state: State) -> () {
draw(state)
if(state.time > 1) {
print("\n\n")
race(runStepOfRace(state))
}
}
func draw(state: State) -> () {
let outputs = state.positions.map { position in outputCar(position) }
print(join("\n", outputs))
}
func runStepOfRace(state: State) -> State {
let newTime = state.time - 1
let newPositions = moveCars(state.positions)
return (newTime, newPositions)
}
func outputCar(carPosition: Int) -> String {
let output = (0..<carPosition).map { _ in "-" }
return join("", output)
}
func moveCars(positions: [Int]) -> [Int] {
return positions.map { position in (randomPositiveNumberUpTo(10) >
3) ? position + 1 : position }
}
哇哦,好多代碼啊。我建議你仔細(xì)研究下例子中的每個(gè)函數(shù),然后你就會(huì)意識(shí)到到這就是我們想要的函數(shù):既不依賴外部數(shù)據(jù),也不會(huì)修改內(nèi)部數(shù)據(jù)。
例子中另外一個(gè)保持整潔的細(xì)節(jié)是使用了自定義類型——通過(guò)typealias關(guān)鍵字——在編寫(xiě)代碼時(shí)候會(huì)變得更加自然。我個(gè)人比較喜歡函數(shù)式編程的原因之一是,它會(huì)強(qiáng)行讓我們仔細(xì)思考代碼中使用的類型以及如何在函數(shù)里面操作這些類型。
cook的課程 #3 - 使用管道。
在前面的章節(jié)中,一些命令式的循環(huán)重寫(xiě)為調(diào)用輔助函數(shù)的循環(huán)。在這個(gè)章節(jié)中,另外一種命令式的循環(huán)使用一種名叫管道的技術(shù)來(lái)重寫(xiě)。
例子 #9 - 不使用管道
我們先來(lái)一個(gè)典型的例子,對(duì)一組數(shù)據(jù)進(jìn)行變換。在這個(gè)例子里面,有一個(gè)bands的數(shù)組,其中每個(gè)元素包含name和country。我們想要對(duì)這個(gè)bands集合進(jìn)行兩次變換:1、把country設(shè)置為Canada 2、把name改成大寫(xiě)。以下是我們第一次的實(shí)現(xiàn):
/*** 使用管道 ***/
/// 非函數(shù)式的 ///
var bands: [ [String : String] ] = [
["name" : "sunset rubdown", "country" : "UK"],
["name" : "women", "country" : "Germany"],
["name" : "a silver mt. zion", "country" : "Spain"]
]
func formatBands(inout bands: [ [String : String] ]) -> () {
var newBands: [ [String : String] ] = []
for band in bands {
var newBand: [String : String] = band
newBand["country"] = "Canada"
newBand["name"] = newBand["name"]!.capitalizedString
newBands.append(newBand)
}
bands = newBands
}
formatBands(&bands)
print(bands) // [[country: Canada, name: Sunset Rubdown], [country: Canada, name: Women], [country: Canada, name: A Silver Mt. Zion]]
使用inout關(guān)鍵字已經(jīng)說(shuō)明我們沒(méi)有使用函數(shù)式編程。
下面是另一種更具有表現(xiàn)力和靈活性的實(shí)現(xiàn)方式,同樣能實(shí)現(xiàn)數(shù)據(jù)變換,并且不會(huì)把所有代碼都塞到formatBands函數(shù)中:
print(formattedBands(bands, [setCanadaAsCountry, capitalizeName]))
我們應(yīng)該如何實(shí)現(xiàn)呢?
例子 #10 - 函數(shù)式管道
/*** 使用管道 ***/
/// Functional - Example #1 ///
let bands: [ [String : String] ] = [
["name" : "sunset rubdown", "country" : "UK"],
["name" : "women", "country" : "Germany"],
["name" : "a silver mt. zion", "country" : "Spain"]
]
typealias BandProperty = String
typealias Band = [String : BandProperty]
typealias BandTransform = Band -> Band
typealias BandPropertyTransform = BandProperty -> BandProperty
let canada: BandPropertyTransform = { _ in return "Canada" }
let capitalize: BandPropertyTransform = { return $0.capitalizedString }
let setCanadaAsCountry: BandTransform = call(function: canada, onValueForKey: "country")
let capitalizeName: BandTransform = call(function: capitalize, onValueForKey: "name")
func formattedBands(bands: [Band], functions: [BandTransform]) -> [Band] {
return bands.map {
band in
functions.reduce(band) {
(currentBand, function) in
function(currentBand)
}
}
}
print(formattedBands(bands, [setCanadaAsCountry, capitalizeName])) // [[country: Canada, name: Sunset Rubdown], [country: Canada, name: Women], [country: Canada, name: A Silver Mt. Zion]]
// 輔助函數(shù)
func call(#function: BandPropertyTransform, onValueForKey key: String) -> BandTransform {
return {
band in
var newBand = band
newBand[key] = function(band[key]!)
return newBand
}
}
請(qǐng)注意canada和capitalize函數(shù);他們只是接受BandProperty(字符串)然后返回一個(gè)BandProperty(字符串)。
請(qǐng)注意查看setCountryAsCanada和capitalizeName函數(shù);他們簡(jiǎn)單的接收BandPropertyTransform函數(shù)(比如canada和capitalize)并把它們應(yīng)用到Band(字典)中的一個(gè)鍵值上(這里分別是"country"和"name")。
使用上述所有函數(shù)時(shí),都可以獨(dú)立思考它們的作用。函數(shù)formattedBands在最后調(diào)用BandTransform數(shù)組中的函數(shù)來(lái)處理傳入的bands數(shù)組。我們可以寫(xiě)任何數(shù)量的變換,然后把它們傳入formattedBands,同時(shí)保持其他函數(shù)不變——這是一個(gè)非常強(qiáng)大的東東!
課外內(nèi)容 - 函數(shù)組合
如果你已經(jīng)看到這里了——那就繼續(xù)來(lái)看另外一種合并轉(zhuǎn)換的方法,Cook 的文章中沒(méi)有介紹這種方法。
這種方法的靈感來(lái)自一本我最喜歡的關(guān)于 Swift 的圖書(shū):Functional Programming in Swift,作者是來(lái)自objc.io的 Chris Eidhof、Florian Kugler 和 Wouter Swierstra。這本書(shū)的一個(gè)章節(jié)談到了如何構(gòu)建函數(shù)——我們可以把這個(gè)概念用到之前的代碼中。
例子 #11 - 函數(shù)組合
/*** 使用管道 ***/
/// 函數(shù)式的 - 例子 #2 ///
let canada: BandPropertyTransform = { _ in return "Canada" }
let capitalize: BandPropertyTransform = { return $0.capitalizedString }
let setCanadaAsCountry: BandTransform = call(function: canada, onValueForKey: "country")
let capitalizeName: BandTransform = call(function: capitalize, onValueForKey: "name")
let myBandTransform = composeBandTransforms(setCanadaAsCountry, capitalizeName)
let formattedBands = bands.map { band in myBandTransform(band) }
print(formattedBands) // [[country: Canada, name: Sunset Rubdown], [country: Canada, name: Women], [country: Canada, name: A Silver Mt. Zion]]
// 輔助函數(shù)
func call(#function: BandPropertyTransform, onValueForKey key: String) -> BandTransform {
return {
band in
var newBand = band
newBand[key] = function(band[key]!)
return newBand
}
}
func composeBandTransforms(transform1: BandTransform, transform2: BandTransform) -> BandTransform {
return {
band in
transform2(transform1(band))
}
}
這和之前的代碼看起來(lái)很像——但是使用了函數(shù)構(gòu)建的概念來(lái)構(gòu)造我們的變換,替換掉 Cook 的map和reduce方案。
結(jié)論
這篇文章可以用 Cook 的精彩總結(jié)來(lái)結(jié)尾:
函數(shù)式代碼可以很好的與其他風(fēng)格的代碼共存……把列表的循環(huán)換成
map和reduce。這點(diǎn)請(qǐng)參考車輛競(jìng)賽的例子。把代碼拆分到函數(shù)中,使得這些函數(shù)更加函數(shù)式化。把一個(gè)過(guò)程的循環(huán)變成遞歸。這點(diǎn)請(qǐng)參考bands的例子。把一系列的操作變成管道。
Swift 不是一個(gè)純函數(shù)式語(yǔ)言——你的函數(shù)式代碼可以很好的與非函數(shù)式代碼共存。
這篇文章的重點(diǎn)是教你如何把現(xiàn)有代碼轉(zhuǎn)換成函數(shù)式風(fēng)格,并讓你見(jiàn)識(shí)到函數(shù)式編程的威力。
本文所有例子的代碼都放在了 Gists(譯者注:請(qǐng)點(diǎn)擊原文中例子的Gists鏈接)中,同時(shí)也放到了 GitHub 上:https://github.com/hkellaway/swift-functional-intro
編碼快樂(lè)!