Swift 函數(shù)式編程實(shí)踐

原文連接:a practical introduction to functional programming
原文日期:2015/08/10

譯者:shanks
校對(duì):numbbbbb
定稿:小鍋

介紹

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),使用mapreduce

任何一個(gè)深入研究函數(shù)式編程的程序員都會(huì)很快接觸到map、reducefilter函數(shù)。這幾個(gè)函數(shù)非常強(qiáng)大,用于處理集合(collections)類型。下面我們來(lái)看mapreduce函數(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 中,類似于mapreduce的函數(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è)元素包含namecountry。我們想要對(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)注意canadacapitalize函數(shù);他們只是接受BandProperty(字符串)然后返回一個(gè)BandProperty(字符串)。

請(qǐng)注意查看setCountryAsCanadacapitalizeName函數(shù);他們簡(jiǎn)單的接收BandPropertyTransform函數(shù)(比如canadacapitalize)并把它們應(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 的mapreduce方案。

結(jié)論

這篇文章可以用 Cook 的精彩總結(jié)來(lái)結(jié)尾:

函數(shù)式代碼可以很好的與其他風(fēng)格的代碼共存……把列表的循環(huán)換成mapreduce。這點(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è)!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容