函數(shù)式編程-將Monad(單子)融入Swift

前言

近期又開始折騰起Haskell,掉進(jìn)這個(gè)深坑恐怕很難再爬上來(lái)了。在不斷深入了解Haskell的各種概念以及使用它們?nèi)ソ鉀Q實(shí)際問題的時(shí)候,我會(huì)試想著將這些概念移植到Swift中。函數(shù)式編程范式的很多概念在Swift等主打面向?qū)ο蠓妒降恼Z(yǔ)言中就像各種設(shè)計(jì)模式一樣,優(yōu)雅地幫助我們構(gòu)建好整個(gè)項(xiàng)目,促使我們的代碼更加的美觀優(yōu)雅、安全可靠。
本篇文章為"函數(shù)式編程"系列中的第二篇,我主要說(shuō)下Monad的一些小概念,以及試圖將Monad融入Swift中來(lái)讓其為我們的實(shí)際工程項(xiàng)目作出貢獻(xiàn)。

關(guān)于Monad、在Swift中實(shí)現(xiàn)Monad的一些見解

Monad回顧

在上一篇文章《函數(shù)式編程-一篇文章概述Functor(函子)、Monad(單子)、Applicative》中提到過(guò),我們可以將一個(gè)值用Context(上下文)包裹起來(lái),使得它不僅可以純粹地表示自己,還含有一些額外的信息,Monad我理解為參與某種計(jì)算過(guò)程的、被上下文包含起來(lái)的值,說(shuō)到計(jì)算過(guò)程,就需要提及Monad中一個(gè)重要的函數(shù)bind(>>=),它的作用,就是進(jìn)行Monad的計(jì)算過(guò)程,并且,它讓我們?cè)谟?jì)算過(guò)程中只需專注于值的運(yùn)算,而不需要花另外的精力去處理計(jì)算過(guò)程中Context(上下文)的變化轉(zhuǎn)換。說(shuō)白了,就是我們只管值的運(yùn)算,Context(上下文)就放心交給bind的內(nèi)部實(shí)現(xiàn)去處理吧。
這里列舉一個(gè)Swift中的Optional monad:

// 擴(kuò)展Optional,實(shí)現(xiàn)bind方法
extension Optional {
    func bind<O>(_ f: (Wrapped) -> Optional<O>) -> Optional<O> {
        switch self {
        case .none:
            return .none
        case .some(let v):
            return f(v)
        }
    }
}

// 定義bind運(yùn)算符`>>-`
precedencegroup Bind {
    associativity: left
    higherThan: DefaultPrecedence
}

infix operator >>- : Bind

func >>- <L, R>(lhs: L?, rhs: (L) -> R?) -> R? {
    return lhs.bind(rhs)
}

// 除法,若除數(shù)為0,返回nil
// 方法類型:
//    A           B          C
// (Double) -> (Double) -> Double?
// 用B除以A
func divide(_ num: Double) -> (Double) -> Double? {
    return {
        guard num != 0 else { return nil }
        return $0 / num
    }
}

let ret = divide(2)(16) >>- divide(3) >>- divide(2) // 1.33333333...
// 可以寫成
// let ret = Optional.some(16) >>- divide(2) >>- divide(3) >>- divide(2)

let ret2 = Optional.some(16) >>- divide(2) >>- divide(0) >>- divide(2) // nil

如上,我將Swift中的Optional類型實(shí)現(xiàn)為Monad,所以對(duì)于一個(gè)可選的數(shù)據(jù)類型,它的上下文為數(shù)據(jù)是否為空。定義的除法方法divide將兩個(gè)數(shù)相除,如果除數(shù)為0,則返回nil,用于保證運(yùn)算的安全。在最后,我進(jìn)行了兩個(gè)連續(xù)運(yùn)算,結(jié)果為retret2,可以看到,若運(yùn)算過(guò)程中所有除數(shù)都不為0,則最終返回連續(xù)除法運(yùn)算后的結(jié)果,若運(yùn)算過(guò)程中某除數(shù)如果是0,那么返回的結(jié)果就會(huì)是nil。
我們可以發(fā)現(xiàn),整個(gè)運(yùn)算過(guò)程中我們只專注于運(yùn)算的方法以及參與運(yùn)算的數(shù)據(jù),我們并沒有花其他的精力用于檢測(cè)除數(shù)是否為0,并且如果為零則終止運(yùn)算,返回nil,因?yàn)檫@部分關(guān)于上下文的考慮,bind已經(jīng)為我們打理好了。

Swift中實(shí)現(xiàn)Monad

Haskell的類型系統(tǒng)強(qiáng)大,加上其對(duì)Monad的高度支持(如提供了do語(yǔ)法糖),我們可以很容易地在里面創(chuàng)造和使用Monad。但是對(duì)于Swift語(yǔ)言,由于其泛型系統(tǒng)以及語(yǔ)法的限制,我們不能夠像Haskell那樣非常優(yōu)雅地實(shí)現(xiàn)Monad,個(gè)人總結(jié)出有兩點(diǎn)原因:

Swift中的協(xié)議無(wú)法定義出Monad

Haskell中,Monad的定義為:

class Applicative m => Monad (m :: * -> *) where
  (>>=) :: m a -> (a -> m b) -> m b
  (>>) :: m a -> m b -> m b
  return :: a -> m a
  fail :: String -> m a

Haskell的類型類與Swift中的協(xié)議類似,我們可以看到第一行聲明了Monad,而m可以看做是需要實(shí)現(xiàn)Monad的類型,下面就是一些需要實(shí)現(xiàn)的函數(shù)。事實(shí)上,m在上面其實(shí)是一個(gè)類型構(gòu)造器,它的類型為(* -> *),我們可以直接把它看成是Swift中具有一個(gè)泛型參數(shù)的泛型,相應(yīng)的,如果是(* -> * -> *)類型的Haskell類型構(gòu)造器,就類型于Swift中具有兩個(gè)泛型參數(shù)的泛型,而(*)類型的類型構(gòu)造器其實(shí)就是一個(gè)具體的類型。
現(xiàn)在問題來(lái)了,對(duì)于Haskell,我們可以讓一個(gè)非具體的類型(具有一個(gè)或多個(gè)類型參數(shù)的類型構(gòu)造器)去實(shí)現(xiàn)某些類型類,但是對(duì)于Swift,若要實(shí)現(xiàn)一個(gè)協(xié)議,我們必須得提供一個(gè)具體的類型。所以在Swift中Monad無(wú)法用協(xié)議來(lái)實(shí)現(xiàn)。

protocol Monad {
    associatedtype MT
    func bind(_ f: (MT) -> Self) -> Self
}

像上面定義的Monad協(xié)議,泛型參數(shù)為MT。這個(gè)Monad協(xié)議的bind函數(shù)是存在問題的,因?yàn)樗邮找粋€(gè)返回Self類型的函數(shù),并且返回一個(gè)Self類型,Self指待現(xiàn)在實(shí)現(xiàn)了這個(gè)協(xié)議的類型,它的泛型參數(shù)依舊是保持不變,這并不滿足Monad的要求。
(以上為個(gè)人觀點(diǎn),個(gè)人嘗試過(guò)是寫不出來(lái),若各位能使用Swift的協(xié)議實(shí)現(xiàn)了Monad,還望教授)
要在Swift實(shí)現(xiàn)Monad,只能由我們自己保證每個(gè)Monad的實(shí)現(xiàn)類中實(shí)現(xiàn)了指定的Monad函數(shù)。

Swift中無(wú)法優(yōu)雅地解決Monad中的lambda嵌套

Haskell的do語(yǔ)法能夠避免多重的lambda嵌套,從而使得Monad的語(yǔ)法更加優(yōu)雅可觀:

main = do
  first <- getLine
  second <- getLine
  putStr $ first ++ second

對(duì)于Swift來(lái)說(shuō),若我們?cè)谑褂?code>Monad的時(shí)候涉及到了lambda的嵌套,可能寫起來(lái)就會(huì)有點(diǎn)憂傷,這里拿上面提到的Optional monad舉例:

let one: Int? = 4
let two: Int? = nil
let three: Int? = 7

let result1 = one >>- { o in two >>- { t in o + t } }
let result2 = one >>- { o in two >>- { t in three >>- { th in o * t * th } } }

如果Swift支持do語(yǔ)法(不是指異常處理的do語(yǔ)法),那么這樣子就會(huì)簡(jiǎn)潔很多:

let result1 = do {
  o <- one
  t <- two
  th <- three
  return o * t * th
}

上面的語(yǔ)法純屬腦補(bǔ)。

所以一般來(lái)說(shuō)應(yīng)該不會(huì)用Swift去實(shí)現(xiàn)某些需要多重嵌套lambda的Monad。

Either Monad

在上一篇函數(shù)式編程的文章中有提到Result Monad,它表示某個(gè)運(yùn)算可能會(huì)存在成功與失敗的情況,若運(yùn)算成功,則能獲取到結(jié)果值,若運(yùn)算失敗,則可以獲取到失敗的原因(錯(cuò)誤信息)。使用Either Monad也可以做這件事。

enum Either<L, R> {
    case left(L)
    case right(R)
}

extension Either {
    static func ret(_ data: R) -> Either<L, R> {
        return .right(data)
    }

    func bind<O>(_ f: (R) -> Either<L, O>) -> Either<L, O> {
        switch self {
        case .left(let l):
            return .left(l)
        case .right(let r):
            return f(r)
        }
    }
}

func >>- <L, R, O> (lhs: Either<L, R>, f: (R) -> Either<L, O>) -> Either<L, O> {
    return lhs.bind(f)
}

Either為枚舉類型,接收兩個(gè)泛型參數(shù),它表示在某個(gè)狀態(tài)時(shí),數(shù)據(jù)要么是在left中,要么是在right中。
由于Monad要求所實(shí)現(xiàn)的類型需要具備一個(gè)泛型參數(shù),因?yàn)樵谶M(jìn)行bind操作時(shí)可能會(huì)對(duì)數(shù)據(jù)類型進(jìn)行轉(zhuǎn)換,但是上下文所包含的數(shù)據(jù)類型是不會(huì)改變的,所以這里我們將泛型參數(shù)L用于上下文所包含的數(shù)據(jù)類型,R則作為值的類型。

什么是上下文所包含的數(shù)據(jù)類型,什么是值的類型?
Result monad中有一個(gè)數(shù)據(jù)泛型,代表里面的數(shù)據(jù)類型。某次運(yùn)算成功是,則返回這個(gè)類型的數(shù)據(jù),若運(yùn)算失敗,則會(huì)返回一個(gè)Error類型。我們可以把Error類型看成是上下文中包含的數(shù)據(jù)類型,它在一系列運(yùn)算中是不可變的,因?yàn)?code>Result需要靠它來(lái)記錄失敗的信息,若某次運(yùn)算這個(gè)類型突然變成Int,那么整個(gè)上下文將失去原本的意義。所以,若Either monad作為Result monad般地工作,我們必須固定好一個(gè)上下文包含的類型,這個(gè)類型在一系列的運(yùn)算中都不會(huì)改變,而值的類型是可以改變的。
運(yùn)算符>>-的簽名可以很清晰地看到這種類型約束:接收的Either參數(shù)跟后面返回的Either它們的左邊泛型參數(shù)都為L,而右邊泛型參數(shù)可以隨著接收的函數(shù)而相應(yīng)進(jìn)行改變(R -> O)。

Either monad來(lái)作為Result monad般工作,可以細(xì)化錯(cuò)誤信息的類型。在Result monad中,錯(cuò)誤信息都是用Error類型的實(shí)例來(lái)攜帶,而我們使用Either monad,可以根據(jù)我們的需要擬定不同的錯(cuò)誤類型。如我們有兩個(gè)模塊,模塊一表示錯(cuò)誤的類型為ErrorOne,模塊二則為ErrorTwo,我們就可以定義兩個(gè)Either monad來(lái)分別作用于兩個(gè)模塊:

typealias EitherOne<T> = Either<ErrorOne, T>
typealias EitherTwo<T> = Either<ErrorTwo, T>

從上面的代碼我們也可以看出,Swift也能像Haskell一樣對(duì)類型構(gòu)造器(泛型類)進(jìn)行柯里化操作,意思是我們?cè)趯?shí)現(xiàn)一個(gè)泛型的時(shí)候無(wú)需把它需要的所有泛型參數(shù)都填滿,可以只填入其中的若干個(gè)。

Writer monad

為了引入Writer monad,我先拋出一個(gè)需求:

  1. 要連續(xù)完成一系列任務(wù)
  2. 在完成每項(xiàng)任務(wù)后,做相關(guān)的記錄存檔(如日志的記錄)
  3. 最終完成所有任務(wù)后,得到最終數(shù)據(jù)以及總體的記錄檔案

對(duì)于這個(gè)需求,傳統(tǒng)的做法可能是在全局中保存著檔案記錄,每當(dāng)任務(wù)完成后,我們就響應(yīng)地修改這個(gè)全局檔案,直到所有任務(wù)完成。

Writer monad針對(duì)這種情況提供了更加優(yōu)雅的解決方案,它的Context中保存著檔案記錄,每次我們對(duì)數(shù)據(jù)進(jìn)行運(yùn)算時(shí),我們不需要再分離一部分精力在檔案的組織和修改上,我們只需關(guān)注其中數(shù)據(jù)的運(yùn)算。

Monoid

在繼續(xù)深入Writer monad前,首先提及一個(gè)概念: Monoid(單位半群),它作為數(shù)學(xué)的概念有著一些特性,但由于我們只是利用它來(lái)完成工程項(xiàng)目上的一些邏輯,所以不深入探討它的數(shù)學(xué)概念。這里只是簡(jiǎn)單提及一下它的需要滿足的特性:

對(duì)于一個(gè)集合,存在一個(gè)二元運(yùn)算:

  1. 取這個(gè)集合中兩個(gè)元素進(jìn)行運(yùn)算,得到的結(jié)果任然是這個(gè)集合中的元素(封閉性)
  2. 這個(gè)運(yùn)算符合結(jié)合律
  3. 存在一個(gè)元素(單位元),用二元運(yùn)算將其與另一個(gè)元素進(jìn)行運(yùn)算,結(jié)果仍然是另外的那個(gè)元素。

舉個(gè)例子:
對(duì)于整數(shù)類型,它有一個(gè)加法運(yùn)算,接收兩個(gè)整數(shù),并且將兩個(gè)整數(shù)相加,得到的無(wú)疑也是一個(gè)整數(shù),而且我們也都知道,加法是滿足結(jié)合律的。對(duì)于整數(shù)0,任何數(shù)與它相加,都是等于原來(lái)的數(shù),所以0是這個(gè)單位半群的單位元。

我們可以在Swift中定義Monoid的協(xié)議:

// 單位半群
protocol Monoid {
    typealias T = Self
    static var mEmpty: T { get }
    func mAppend(_ next: T) -> T
}

其中,mEmpty表示此單位半群的單位元,mAppend表示相應(yīng)的二元運(yùn)算。

上面的例子就可以在Swift中這樣實(shí)現(xiàn):

struct Sum {
    let num: Int
}

extension Sum: Monoid {
    static var mEmpty: Sum {
        return Sum(num: 0)
    }

    func mAppend(_ next: Sum) -> Sum {
        return Sum(num: num + next.num)
    }
}

我們使用Sum來(lái)表示上面例子中的單位半群。為什么不直接使用Int來(lái)實(shí)現(xiàn)Monoid,非要對(duì)其再包裝多一層呢?因?yàn)?code>Int還可以實(shí)現(xiàn)其他的單位半群,比如:

struct Product {
    let num: Int
}

extension Product: Monoid {
    static var mEmpty: Product {
        return Product(num: 1)
    }

    func mAppend(_ next: Product) -> Product {
        return Product(num: num * next.num)
    }
}

上面這個(gè)單位半群的二元運(yùn)算就是乘法運(yùn)算,所以單位元為1,1與任何數(shù)相乘都為原本的數(shù)。

像布爾類型,可以引出兩種Monoid:

struct All {
    let bool: Bool
}

extension All: Monoid {
    static var mEmpty: All {
        return All(bool: true)
    }

    func mAppend(_ next: All) -> All {
        return All(bool: bool && next.bool)
    }
}

struct `Any` {
    let bool: Bool
}

extension `Any`: Monoid {
    static var mEmpty: `Any` {
        return `Any`(bool: true)
    }

    func mAppend(_ next: `Any`) -> `Any` {
        return `Any`(bool: bool || next.bool)
    }
}

當(dāng)我們要判斷一組布爾值是否都為或者是否存在時(shí),我們就可以利用AllAny monoid的特性:

let values = [true, false, true, false]

let result1 = values.map(`Any`.init)
    .reduce(`Any`.mEmpty) { $0.mAppend($1) }.bool // true

let result2 = values.map(All.init)
    .reduce(All.mEmpty) { $0.mAppend($1) }.bool // false

實(shí)現(xiàn)Writer monad

下面繼續(xù)來(lái)深入Writer monad,首先給出它在Swift中的實(shí)現(xiàn):

// Writer
struct Writer<W, T> where W: Monoid {
    let data: T
    let record: W
}

extension Writer{
    static func ret(_ data: T) -> Writer<W, T> {
        return Writer(data: data, record: W.mEmpty)
    }

    func bind<O>(_ f: (T) -> Writer<W, O>) -> Writer<W, O> {
        let newM = f(data)
        let newData = newM.data
        let newW = newM.record
        return Writer<W, O>(data: newData, record: record.mAppend(newW))
    }
}

func >>- <L, R, W>(lhs: Writer<W, L>, rhs: (L) -> Writer<W, R>) -> Writer<W, R> where W: Monoid {
    return lhs.bind(rhs)
}

分析下實(shí)現(xiàn)的源碼:

  • 泛型參數(shù)M要求為一個(gè)Monoid,它就是表示一系列操作用所記錄的檔案的類型;泛型參數(shù)T表示被包裹在Writer monad上下文中數(shù)據(jù)的類型。
  • ret方法作用跟Haskell中的return函數(shù)一樣,將一個(gè)值包裹在某個(gè)Monad的最小上下文中。對(duì)于Writer monad,我們?cè)?code>ret函數(shù)中返回一個(gè)Writer,其中數(shù)據(jù)為傳入的參數(shù),記錄檔案則為指定Monoid的單位元,這樣就能將一個(gè)數(shù)據(jù)包裹進(jìn)Writer monad的最小上下文中。
  • bind的實(shí)現(xiàn)中,我們可以看到,里面會(huì)自動(dòng)將兩個(gè)Writer monad的記錄進(jìn)行mAppend操作,返回一個(gè)包裹著新數(shù)據(jù)和新記錄的Writer monad。前面關(guān)于Monad概念中提到:Monadbind操作是讓我們專注于數(shù)據(jù)的運(yùn)算,對(duì)于上下文的處理,我們無(wú)需關(guān)心,這個(gè)是自動(dòng)進(jìn)行的。所以對(duì)于Writer monad,bind操作自動(dòng)幫我們把記錄mAppend起來(lái),我們也無(wú)需把其他的精力花在對(duì)記錄的操作中。
  • 為了讓代碼更加美觀優(yōu)雅,我定義了運(yùn)算符>>-,它在Haskell中的樣子是>>=。

Demo

接下來(lái)我們用Writer monad做一個(gè)小Demo。
就像前面引入的需求一樣,這里我打算做一個(gè)關(guān)于Double的一系列簡(jiǎn)單運(yùn)算,包括加、減、乘、除,每次運(yùn)算后,我們需要用字符串來(lái)對(duì)運(yùn)算的過(guò)程進(jìn)行記錄,比如x * 3會(huì)記錄成乘以3,并將之前的記錄與新運(yùn)算創(chuàng)建的記錄進(jìn)行合并,最終一系列運(yùn)算完成后,我們會(huì)得到運(yùn)算結(jié)果以及整個(gè)運(yùn)算過(guò)程的記錄。

首先我們先讓String實(shí)現(xiàn)Monoid

extension String: Monoid {
    static var mEmpty: String {
        return ""
    }

    func mAppend(_ next: String) -> String {
        return self + next
    }
}

這個(gè)針對(duì)String的單位半群,其二元運(yùn)算為+,表示將兩個(gè)字符串拼接起來(lái),所以其單位元為一個(gè)空字符串。

這里我為DoubleWriter monad類型擬一個(gè)別名,記錄類型為String,數(shù)據(jù)類型為Double

typealias MWriter = Writer<String, Double>

然后定義加、減、乘、除運(yùn)算:

func add(_ num: Double) -> (Double) -> MWriter {
    return { MWriter(data: $0 + num, record: "加上\(num) ") }
}

func subtract(_ num: Double) -> (Double) -> MWriter {
    return { MWriter(data: $0 - num, record: "減去\(num) ") }
}

func multiply(_ num: Double) -> (Double) -> MWriter {
    return { MWriter(data: $0 * num, record: "乘以\(num) ") }
}

func divide(_ num: Double) -> (Double) -> MWriter {
    return { MWriter(data: $0 / num, record: "除以\(num) ") }
}

注意,這些函數(shù)都是高階函數(shù),若他們的形參跟返回值看成是(a) -> (b) -> c,則這些函數(shù)的作用是進(jìn)行運(yùn)算b X a (X為加、減、乘、除運(yùn)算),然后把結(jié)果c返回。
每次運(yùn)算后都會(huì)記錄此次運(yùn)算的相關(guān)信息,比如加上X、除以X。

現(xiàn)在我們來(lái)測(cè)試一下:

let resultW = MWriter.ret(1) >>- add(3) >>- multiply(5) >>- subtract(6) >>- divide(7)

let resultD = resultW.data // 2.0

let resultRecord = resultW.record // "加上3.0 乘以5.0 減去6.0 除以7.0"

可見,我們得到了多次連續(xù)運(yùn)算后的結(jié)果2.0,還有被自動(dòng)拼接起來(lái)的記錄"加上3.0 乘以5.0 減去6.0 除以7.0"。


當(dāng)然,Writer monad的玩法還有很多種,比如現(xiàn)在再出一個(gè)需求:
規(guī)定成績(jī)分?jǐn)?shù)為整數(shù),分?jǐn)?shù)大于等于60分能拿到及格,現(xiàn)需要統(tǒng)計(jì)一個(gè)班同學(xué)的成績(jī),并且判斷:整個(gè)班的同學(xué)是否都及格/是否存在至少一個(gè)同學(xué)及格。
我們可以利用上面已經(jīng)介紹的All monoid以及Any monoid來(lái)創(chuàng)建分?jǐn)?shù)的Writer monad

typealias ScoreWriter = Writer<All, Int>

func append(_ score: Int) -> (Int) -> ScoreWriter {
    return { ScoreWriter(data: $0 + score, record: All(bool: score >= 60)) }
}

let allScores = [45, 60, 98, 77, 65, 59, 60, 86, 93]

let result = allScores.reduce(ScoreWriter.ret(0)) { $0 >>- append($1) }
let resultBool = result.record.bool // false
let resultScore = result.data // 643

append為一個(gè)高階函數(shù),我們可以把它看成是一個(gè)接收兩個(gè)參數(shù)的函數(shù)的柯里化形式,我們會(huì)判斷傳入的第一個(gè)參數(shù)是否滿足合格的要求,并且將兩個(gè)參數(shù)相加,創(chuàng)建一個(gè)ScoreWriter
在這個(gè)ScoreWriter monad中,我將記錄類型設(shè)為All,所以返回的結(jié)果中,布爾類型表明整個(gè)班同學(xué)們的成績(jī)是否都及格了。傳入的數(shù)據(jù)中顯然有低于60的,所以最終的布爾結(jié)果為false

如果你把All改成Any,最終的布爾結(jié)果就為true,表明整個(gè)班至少有一位同學(xué)是及格的:

// 這里我用反單引號(hào)(`)將Any包裹住,因?yàn)锳ny為Swift中的關(guān)鍵字
typealias ScoreWriter = Writer<`Any`, Int>

func append(_ score: Int) -> (Int) -> ScoreWriter {
    return { ScoreWriter(data: $0 + score, record: `Any`(bool: score >= 60)) }
}

let allScores = [45, 60, 98, 77, 65, 59, 60, 86, 93]

let result = allScores.reduce(ScoreWriter.ret(0)) { $0 >>- append($1) }
let resultBool = result.record.bool // true

State Monad

對(duì)于Swift來(lái)說(shuō),由于其不是純函數(shù)式編程語(yǔ)言,所以也不會(huì)存在數(shù)據(jù)不可變的情況,我們可以隨時(shí)用var創(chuàng)建變量。而Haskell由于其特性規(guī)定了所有數(shù)據(jù)都是不可變的,所以對(duì)于某些涉及狀態(tài)的運(yùn)算而言,需要另辟蹊徑。State monad(狀態(tài)Monad)可以用來(lái)解決這種需求。不過(guò)在Swift中,如果你不喜歡總是定義一些變量,或者說(shuō)出現(xiàn)變量混雜的情況,你也可以使用這種方法。

State MonadHaskelldo語(yǔ)法中能發(fā)揮強(qiáng)勁的作用,但是在Swift中如要實(shí)現(xiàn)這種效果,我們需要編寫多重的lambda嵌套(閉包嵌套),這樣寫既麻煩,可觀性又不高,與函數(shù)式編程簡(jiǎn)潔的特點(diǎn)相違背。所以,這里只探討用>>- (bind)鏈?zhǔn)秸{(diào)用State monad的相關(guān)情況。
State Monad有一定的難度,并且它可能很少會(huì)在日常的工程項(xiàng)目中被需要到,但是通過(guò)對(duì)它的學(xué)習(xí)把玩,可以很好地提高我們對(duì)函數(shù)式編程的熟悉掌握。以下對(duì)Stata Monad的講解較為粗略,以供了解,若有興趣,可查閱有關(guān)State Monad的更多信息。

首先我們來(lái)實(shí)現(xiàn)State Monad:

struct State<S, T> {
    let f: (S) -> (T, S)
}

extension State {
    static func ret(_ data: T) -> State<S, T> {
        return State { s in (data, s) }
    }

    func bind<O>(_ function: @escaping (T) -> State<S, O>) -> State<S, O> {
        let funct = f
        return State<S, O> { s in
            let (oldData, oldState) = funct(s)
            return function(oldData).f(oldState)
        }
    }
}

func >>- <S, T, O>(lhs: State<S, T>, f: @escaping (T) -> State<S, O>) -> State<S, O> {
    return lhs.bind(f)
}

如果某項(xiàng)操作需要狀態(tài),我們不想在作用域中創(chuàng)建一個(gè)新的變量來(lái)記錄某些臨時(shí)的狀態(tài),并隨著操作的進(jìn)行而改變,可以在每次進(jìn)行操作完后把新的狀態(tài)返回,這樣,我們下一次操作就可以利用新的狀態(tài)進(jìn)行,以此類推。
State具有一個(gè)成員,它的類型為一個(gè)函數(shù),這個(gè)函數(shù)可以看作是一種操作,接受某個(gè)狀態(tài)作為參數(shù),返回操作后的結(jié)果數(shù)據(jù)以及一個(gè)新的狀態(tài)組成的元組。State Monadret函數(shù)接收一個(gè)任意類型的值,返回State本身。因?yàn)?code>ret函數(shù)是將數(shù)據(jù)包裹在Monad的最小上下文中,所以此時(shí)State中的成員函數(shù)不對(duì)數(shù)據(jù)和狀態(tài)做任何的處理。
對(duì)于bind函數(shù),它的作用就是自動(dòng)幫我們將上一個(gè)操作返回的新狀態(tài)傳入到下一個(gè)操作中,所以我們調(diào)用bind函數(shù)進(jìn)行一系列操作的時(shí)候,我們無(wú)需花精力于狀態(tài)的傳遞。

下面我舉一個(gè)使用State Monad的小例子,這個(gè)例子可能比較牽強(qiáng),如果以后我想到更好的可能會(huì)重新修改下這部分。

現(xiàn)假設(shè)現(xiàn)在服務(wù)器提供API,通過(guò)用戶的ID可以獲取到用戶的名字,我們想要獲取連續(xù)ID的n個(gè)用戶的名字,并將這些名字包裹在一個(gè)數(shù)組中。
我們首先來(lái)模擬服務(wù)器數(shù)據(jù)庫(kù)的數(shù)據(jù)以及API函數(shù):

struct Person {
    let id: Int
    let name: String
}

let data = ["Hello", "My", "Name", "Is", "Tangent", "Haha"].enumerated().map(Person.init)

func fetchNameWith(id: Int) -> String? {
    return data.filter { $0.id == id }.first?.name
}

服務(wù)器提供fetchNameWith方法用于通過(guò)ID獲取到指定用戶的名字,若不存在此ID的用戶,則返回nil。

我們定義用于解決此問題的State Monad類型,并創(chuàng)建請(qǐng)求函數(shù):

typealias MState = State<Int, [String]>

func fetch(names: [String]) -> MState {
    return MState { id in
        guard let name = fetchNameWith(id: id) else { return (names, id) }
        return (names + [name], id + 1)
    }
}

fetch函數(shù)的類型為([String]) -> MState,參數(shù)為前面所請(qǐng)求到的所有用戶名字所組成的數(shù)組,返回的MState中操作函數(shù)做的事情有兩件:

  1. 調(diào)用服務(wù)器API,獲取到指定的用戶名字,并把用戶的名字添加到數(shù)組中
  2. 將原本的用戶ID加一,以便在后面的操作中能夠獲取到下一個(gè)用戶的名字

這里需考慮一個(gè)邊界情況,當(dāng)服務(wù)器找不到指定的用戶時(shí),返回nil,我們的操作函數(shù)就不做任何的事情了,返回原來(lái)的數(shù)據(jù),表明后面我們?cè)僭趺蠢^續(xù)調(diào)用請(qǐng)求函數(shù),結(jié)果都不會(huì)改變。

下面來(lái)測(cè)試一下:

let fetchFunc = MState.ret([]) >>- fetch >>- fetch >>- fetch >>- fetch
let namesAndNextID = fetchFunc.f(1)
let names = namesAndNextID.0 // ["My", "Name", "Is", "Tangent"]
let nextID = namesAndNextID.1 // 5

我們一開始把一個(gè)空的數(shù)組包裹到State Monad的最小上下文中,然后進(jìn)行了四次請(qǐng)求,bind自動(dòng)完成有關(guān)狀態(tài)的操作,最后返回結(jié)果State Monad,這個(gè)結(jié)果State Monad中的操作函數(shù)已經(jīng)是將前面所有的操作合并了,所以我們可以直接調(diào)用此操作函數(shù),最中獲取我們想要的數(shù)據(jù)。

總結(jié)

本文概述了有關(guān)Monad(單子)的概念,探討了在Swift中實(shí)現(xiàn)Monad的一些缺陷點(diǎn),并引入了Either MonadWriter Monad、State Monad,嘗試在Swift中去實(shí)現(xiàn)它們。雖然在平時(shí)的開發(fā)中我們一般都使用面向?qū)ο蟮木幊谭妒?,但是靈活地在你的代碼中融入一些函數(shù)式編程的概念及思想將會(huì)產(chǎn)生意想不到效果。
不過(guò)坑有點(diǎn)深??

最后編輯于
?著作權(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)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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