函數式編程 - 酷炫Applicative(應用函子) [Swift描述]

Applicative

引言

Applicative functor(應用函子),簡稱Applicative,做爲函數式編程裏面一個比較重要的概念,其具備十分酷炫的特性,在工程上的實用性也很是高。這篇文章將會以工程的角度不斷深刻、層層剖析Applicative,在闡述其概念的同時也會結合小Demo進行實戰演示。編程

不少函數式編程的概念在我以前寫的文章中已經介紹過,一些相關的也會將在這篇文章中被重複說起,以加深認識。json

Functor(函子)

Applicative自己也是Functor,其基於Functor有一套本身額外的概念,用面向對象的角度去理解,能夠認爲Applicative繼承自Functor。在進一步介紹Applicative以前,咱們先來認識一下Functorapi

概念及引入

什麼是Functor?爲了說明,這裏引入Swift的一個結構:Optional:服務器

Optional表明數據可空,其存在兩種狀態,要麼其中具備數據,要麼其爲空(nil)網絡

var name: String? = nil
name = "Tangent"
複製代碼

讓咱們來模擬一個場景,假設如今有一個函數,會在傳入的字符串前拼接"Hello",返回一個新的字符串,它會是這樣:閉包

func sayHello(_ str: String) -> String {
    return "Hello " + str
}
複製代碼

對於裝有String的Optional類型String?(Optional<String>),它的值將不能直接傳入這個函數中,由於String?並不等同於String,並且編譯器並不會在咱們將Optional值傳入非Optional類型變量的時候作自動類型轉換的優化(反過來就能夠)。要獲得結果值,傳統的作法咱們可能會這麼寫:app

let name: String? = "Tangent"

// 使用變量
var result: String?
if let str = name {
    result = sayHello(str)
}

// 直接閉包調用
let result: String? = {
    guard let str = name else { return nil }
    return sayHello(str)
}()
複製代碼

上面的兩種寫法都是分狀況考慮:假如原來的字符串爲空,那麼結果值也理所固然是空,不然將字符串解包後傳入函數中返回結果。比起純粹地處理非Optional數據,這裏咱們須要多作判空這一步。異步

讓咱們來換個角度思考,對於Optional類型,它不只要存儲數據自己,還要去記錄數據是否爲空的標誌,因此咱們對Optional值進行處理時,咱們除了要處理其中的數據,還要考慮它所攜帶的是否爲空的標誌。它就像一個盒子,盒子裏裝有值,自己還具有一些額外的元信息(咱們也能夠稱其爲反作用)。函數式編程

當咱們操做盒子的時候,咱們須要把盒子裏的數據拿出來,而且要考慮到盒子其中所攜帶的額外信息,像上面的代碼所示,咱們作的不只要處理Optional中的數據,還須要對Optional進行判空處理。這裏還有一個很是重要的點:咱們在上面代碼所作的處理中,並無更改到盒子裏的額外信息,若原來數據是空的,那麼結果值也會是空,同理若原來數據非空,結果值也不可能爲空。函數

如今,咱們能夠把上面的操做進行抽象:

  • 存在一種像盒子同樣的數據類型,除了包含內部的數據自己外,可能還攜帶一些額外的元信息
  • 須要對這個盒子數據類型內部的數據進行一些處理轉換
  • 在處理轉換的過程當中並不會改變其中額外的元信息

爲了表示上面的抽象,咱們能夠引入Functor(函子)

實現

爲了方便描述,對於這種盒子數據類型,咱們能夠叫作Context<A>,其中,A表明內部所裝載數據的類型。Functor中存在一種運算,名字能夠叫作mapmap的類型用相似Swift語法的描述能夠理解爲:map: ((A) -> B) -> Context<A> -> Context<B>,你能夠理解爲它將一個做用於盒子內部數據的函數提高爲一個能做用於盒子的函數,也能夠認爲它接收一個做用於盒子內部的函數和一個盒子,先將盒子打開,將函數扔進去做用於盒子內部,而後獲得一個具備新數據的盒子。若這個盒子實現了此運算,咱們能夠認爲這個盒子實現了Functor,就像Swift中的協議實現同樣(對於Functor的實現其實還有一些約定,本篇文章不在此詳述,若是你有興趣能夠去查閱Functor相關概念進行深刻了解)。

固然Swift做爲一門支持面向對象的語言,咱們也能夠從面向對象的角度去實現Functor,這裏拿Optional舉個例子:

// Optional在Swift中的定義
enum Optional<Wrapped> {
	case some(Wrapped)
	case none
}

// 爲Optional實現Functor
extension Optional {
    // 使用傳統的模式匹配來實現
    func map<U>(_ f: (Wrapped) -> U) -> U? {
        guard case .some(let value) = self else { return nil }
        return f(value)
    }
    
    // 使用Swift語法糖來實現
    func map2<U>(_ f: (Wrapped) -> U) -> U? {
        guard let value = self else { return nil }
        return f(value)
    }
}
複製代碼

這樣,咱們就可使用map運算來重寫以前的小例子了:

func sayHello(_ str: String) -> String {
    return "Hello " + str
}

let name: String? = "Tangent"

let result = name.map(sayHello)
複製代碼

Swift其實默認已經爲Optional定義了map操做,咱們在開發中也能夠直接拿來使用。

得益於Functor,當咱們在遇到相似的狀況時,能夠只關注於數據處理自己,而不須要花精力於額外的元信息上,代碼的實現更簡潔優雅。

Swift默認實現了Functor的還有Sequence

let arr = [1, 2, 3, 4, 5]
let result = arr.map { 2 * $0 }
複製代碼

運算符

咱們能夠爲map運算定義運算符<^>,以便在後續使用:

precedencegroup FunctorApplicativePrecedence {
    higherThan: AdditionPrecedence
    associativity: left
}

infix operator <^> : FunctorApplicativePrecedence

func <^> <A, B>(lhs: (A) -> B, rhs: A?) -> B? {
    return rhs.map(lhs)
}
複製代碼

這樣,咱們就能夠從更函數式的角度來使用Functor

func sayHello(_ str: String) -> String {
    return "Hello " + str
}

let name: String? = "Tangent"
let result = sayHello <^> name
複製代碼

值得注意的是,這裏<^>運算符左邊的類型爲函數,右邊爲盒子類型,看起來跟面向對象的習慣性寫法有點相反。

雖然說Swift應儘可能避免定義一堆奇奇怪怪的運算符,以避免致使代碼的可讀性下降、增長理解成本,可是<^>運算符其實跟Haskell語言中的<$>十分類似,並且它們功能都是相同的,同理,即將在文章後面定義的<*>運算符在Haskell中你也能找到相同功能的<*>,這些運算符所表達的邏輯能夠說是約定俗成的。

Applicative

Applicative基於Functor。比起FunctorApplicative更爲抽象複雜,爲了能容易理解,本篇接下來將先介紹它的概念以及實現,在最後咱們纔去結合函數式編程的其餘概念來分析它的使用場景,進行項目實戰。

概念

用回在上文提到的盒子模型,Context<A>是一個內部包含A類型數據的盒子,Functormap操做將傳入(A) -> B函數,將盒子打開,做用於裏面的數據,返回新的的盒子Context<B>。在這期間,改變的只是盒子內部的數據,而盒子中具備的額外元信息將不受影響。而對於Applicative而言,其具備apply操做,用Swift語法描述其類型能夠是:apply: Context<(A) -> B> -> Context<A> -> Context<B>,你能夠將它的運算邏輯理解爲如下幾個步驟:

  1. 傳入a盒子Context<A>以及b盒子Context<(A) -> B>,a盒子裏面裝着單純的數據,而b盒子裏面裝有一個處理函數
  2. 將a盒子中的數據取出,將b盒子中的函數取出,而後將函數做用於數據,獲得類型爲B的新值
  3. 將a盒子和b盒子所具備的額外元信息取出,相互做用獲得新的元信息
  4. 把新的值和元信息裝入盒子,獲得結果Context<B>

由上咱們能夠發現,FunctormapApplicativeapply其實十分類似,比起mapapply須要接收的是一個包裝着函數的盒子,而不是純粹的函數類型。另外,map在運做的過程當中不會對額外的元信息產生影響,apply由於其接收的參數都是盒子,它們都具備各自的元信息,因此這裏須要取出這些元信息,讓它們相互做用,以產生新的元信息

Applicative還具備一個操做pure,其接收一個普通值做爲參數,返回一個盒子。咱們能夠理解爲它將一個原始的數據裝在一個盒子裏面。它的類型用Swift語法可描述爲:pure: (A) -> Context<A>。對於經過pure產生的新盒子,其中的元信息應該處於最初始的狀態。

實現

接下來咱們以面向對象的角度來爲Optional實現Applicative

extension Optional {
    static func pure(_ value: Wrapped) -> Wrapped? {
        return value
    }
    
    func apply<U>(_ f: ((Wrapped) -> U)?) -> U? {
        switch (self, f) {
        case let (value?, fun?):
            return fun(value)
        default:
            return nil
        }
    }
}
複製代碼

對於pure,咱們定義了一個static方法,直接將接收到的值返回,Swift編譯器會自動幫咱們用Optional包裝起來。

對於apply,咱們首先看元信息部分,由於Optional所包含的元信息是一個判斷數據是否爲空的標誌,這裏將Optional實例自己與傳入的包含處理函數的Optional參數雙方的元信息進行相互做用,做用的邏輯爲:假如任意一方的元信息表示爲空,那麼apply所返回Optional結果的元信息也同樣是空。再來看數據部分,這裏所作的就是把雙方盒子裏的數據取出來,分別是一個函數以及一個普通的值,再將函數做用於值,獲得新的結果裝入盒子。

請不要疑惑:「爲何Optionalnil時明明已經沒有值了爲何還要從值的角度去考慮?」,由於上面盒子模型中對於元信息和值的描述是基於抽象的角度來進行思考的。

咱們下面就能夠來把玩一下:

typealias Function = (String) -> String
let sayHello: Function? = {
    return "Hello " + $0
}

let name: String? = "Tangent"

let result = name.apply(sayHello)
複製代碼

運算符

咱們使用<*>來做爲apply的運算符,讓代碼編寫起來更函數式:

infix operator <*> : FunctorApplicativePrecedence

func <*> <A, B>(lhs: ((A) -> B)?, rhs: A?) -> B? {
    return rhs.apply(lhs)
}
複製代碼

這裏須要注意的是:FunctorApplicativePrecedence已在文章前面定義,它規定了運算符的結合性是左結合的,因此<^><*>都具備左結合的特性。

下面就來使用一下:

typealias Function = (String) -> String
let sayHello: Function? = {
    return "Hello " + $0
}

let name: String? = "Tangent"

let result = sayHello <*> name
複製代碼

Curry(柯里化)

Applicative的使用場景離不開函數式編程中另外一個重要的概念:Curry(函數柯里化)Curry就是將一個接收多個參數的函數轉變爲只接收單一參數的高階函數。像類型爲(A, B) -> C的函數,通過Curry後,它的類型就變成了(A) -> (B) -> C。舉個例子,咱們有函數add,可以接收兩個Int類型的參數,並返回兩個參數相加的結果:

func add(_ a: Int, _ b: Int) -> Int {
    return a + b
}

let three = add(1, 2)
複製代碼

Curry後的add只需接收一個參數,返回的是一個閉包,這裏閉包也須要接收一個參數,最終返回結果值:

func add(_ a: Int) -> (Int) -> Int {
    return { b in a + b }
}

// 連續調用
let three = add(1)(2)

// 將返回的閉包保存起來,後續再調用
let inc = add2(1)
let three2 = inc(2)
複製代碼

爲了方便,咱們能夠構造若干個幫助咱們進行Curry的函數,這些函數也叫作curry

func curry<A, B, C>(_ fun: @escaping (A, B) -> C) -> (A) -> (B) -> C {
    return { a in { b in fun(a, b) } }
}

func curry<A, B, C, D>(_ fun: @escaping (A, B, C) -> D) -> (A) -> (B) -> (C) -> D {
    return { a in { b in { c in fun(a, b, c) } } }
}

func curry<A, B, C, D, E>(_ fun: @escaping (A, B, C, D) -> E) -> (A) -> (B) -> (C) -> (D) -> E {
    return { a in { b in { c in { d in fun(a, b, c, d) } } } }
}

// 更多參數的狀況 ...
複製代碼

如今咱們能夠用一個例子來使用這些curry函數:

struct User {
    let name: String
    let age: Int
    let bio: String
}

let createUser = curry(User.init)
let tangent = createUser("Tangent")(22)("I'm Tangent!")
複製代碼

上面咱們定義了一個結構體User,其具備三個成員。這裏Swift編譯器默認已經幫咱們建立了一個User的構造方法:User.init,方法的類型爲(String, Int, String) -> User。經過把這個構造方法傳入curry函數,咱們獲得一個高價的函數(閉包)(String) -> (Int) -> (String) -> User

經過結合CurryApplicative將能發揮強大的做用。

使用場景

你們可能從上面的概念中還摸不清Applicative到底能用來作什麼,下面就來揭露Applicative的實用範圍:

假設如今有一個Dictionary,裏面可能裝有與User相關的信息,咱們想在裏面找尋能構造User的字段信息,從而構造出實例:

struct User {
    let name: String
    let age: Int
    let bio: String
}

let dic: [String: Any] = [
    "name": "Tangent",
    "age": 22,
    "bio": "Hello, I'm Tangent!"
]
複製代碼

在運行時中,dic裏面是否具有構造User的所有字段信息咱們是不知道的,因此最終的結果爲一個被Optional包起來的User,也就是User?,傳統的作法能夠這樣寫:

// 使用變量
var tangent: User?
if let name = dic["name"] as? String,
    let age = dic["age"] as? Int,
    let bio = dic["bio"] as? String {
    tangent = User(name: name, age: age, bio: bio)
}

// 直接閉包調用
let tangent: User? = {
    guard
        let name = dic["name"] as? String,
        let age = dic["age"] as? Int,
        let bio = dic["bio"] as? String
    else { return nil }
    return User(name: name, age: age, bio: bio)
}()
複製代碼

在平常的開發中咱們是否是也常常會寫出跟上面類似的代碼呢?這樣寫沒毛病,可是總感受有點繁雜了...

這時候Applicative粉墨登場了:

let tangent = curry(User.init)
    <^> (dic["name"] as? String)
    <*> (dic["age"] as? Int)
    <*> (dic["bio"] as? String)
複製代碼

等等,這上面發生了什麼?讓咱們來一步步分析:

  1. curry(User.init)生成了一個類型爲(String) -> (Int) -> (String) -> User的高階函數(閉包)

    let createUser = curry(User.init)
    複製代碼
  2. 咱們將這個閉包與dic["name"] as? String經過<^>運算符鏈接:

    let step1 = createUser <^> (dic["name"] as? String)
    複製代碼

    step1的類型是什麼?回憶一下<^>,它來源於Functormap操做,左邊接收一個函數(A) -> B,右邊則是一個盒子Context<A>,返回盒子Context<B>。如今咱們把實際的類型代入:盒子是OptionalAString,由於<^>左邊傳入的函數類型爲(String) -> (Int) -> (String) -> User,咱們能夠理解爲(String) -> ((Int) -> (String) -> User),因此這裏B就是(Int) -> (String) -> User,因而,<^>運算結果step1的類型就是Optional<(Int) -> (String) -> User>step1: Optional<(Int) -> (String) -> User>

  3. <*>運算應用於step1dic["age"] as? Int

    let step2 = step1 <*> (dic["age"] as? Int)
    複製代碼

    <*>來源於Applicativeapply操做,左邊接收一個裝有函數的盒子Context<(A) -> B>,右邊接收一個盒子Context<A>,返回盒子Context<B>。把實際的類型代入:盒子是OptionalAInt,由於咱們把step1應用於<*>的左邊,step1是一個裝有(Int) -> (String) -> User函數的Optional盒子,(Int) -> (String) -> User能夠理解爲(Int) -> ((String) -> User),其做用於A(Int),因此B就是(String) -> User。因而,step2的類型就是Optional<(String) -> User>step2: Optional<(String) -> User>

  4. <*>運算應用於step2dic["String"] as? String,獲得結果:

    let tangent = step2 <*> (dic["bio"] as? String)
    複製代碼

    和上面同理,<*>左邊接收的類型爲Context<(A) -> B>,右邊爲Context<A>,返回Context<B>,代入實際類型:盒子是OptionalAStringstep2做爲一個Optional盒子,裝有類型爲(String) -> User的函數,因此B就是User。因而tangent的類型就是Optional<User>tangent: Optional

這就是上方Applicative例子運做的整個過程。比起傳統的寫法,使用Applicative能讓代碼更加簡潔優雅。

咱們也能夠在其中使用Applicativepure

let tangent = .pure(curry(User.init))
    <*> (dic["name"] as? String)
    <*> (dic["age"] as? Int)
    <*> (dic["bio"] as? String)
複製代碼

若使用了pure,咱們就不須要Functor<^>了,由於pure已經將函數用盒子裝了起來,後面就須要所有用<*>運算進行操做。不過這樣寫就須要多調用了一個函數。

可能有人會疑惑:「使用Applicative的代碼其實也就是比起傳統的寫法優雅一點點而已,差異不大,爲何這裏還要大費周章去引入一個新的概念去完成這一件小事?」

由於這裏的例子只是爲了方便理解而從Optional的角度去講解,Swift已經爲Optional定義了一套語法糖,因此以傳統的寫法來使用Optional已足夠簡潔。可是Applicative並不僅侷限於Optional,它足夠強大,能完成更多的事情。

下面將引入其餘功能更增強大的Applicative,它們的實用性也很是高。

Result

Result這個概念對於Swifter們來講應該不會陌生,Swift也計劃將它歸入到標準庫中了。Result表示了一個可能失敗的操做結果:若操做成功,Result中將裝有結果數據,若失敗,Result中也會裝有表示失敗緣由的錯誤信息。

enum Result<Value, Err> {
    case success(Value)
    case failure(Err)
}
複製代碼

得益於Swift對代數數據類型的支持,這裏Result將做爲一個枚舉,包含兩種狀態(成功和失敗),每一個狀態都具備一個關聯數據,對於成功的狀態,其關聯着一個結果值,對於失敗,其關聯了一個錯誤信息。這裏對Result的實現中,咱們也爲錯誤信息配有泛型參數,而不單純是一個實現了Error協議的任意類型。Result以一種非錯誤拋出的形式來向操做調用方反饋錯誤信息,在一些不能使用錯誤拋出的地方(異步回調)中起到很是重要的做用。

引入

讓咱們來編寫一個小型的JSON解析函數,它經過一個特定的key將數據從JSON Object(以Dictionary的形式呈現)中取出來,並將其轉換成一個特定的類型:

enum JSONError {
    case keyNotFound(key: String)
    case valueNotFound(key: String)
    case typeMismatch(type: Any.Type, value: Any)
}

extension Dictionary where Key == String, Value == Any {
    func parse<T>(_ key: String) -> Result<T, JSONError> {
        guard let value = self[key] else {
            return .failure(.keyNotFound(key: key))
        }
        guard !(value is NSNull) else {
            return .failure(.valueNotFound(key: key))
        }
        guard let result = value as? T else {
            return .failure(.typeMismatch(type: T.self, value: value))
        }
        return .success(result)
    }
}
複製代碼

parse方法返回一個Result做爲解析的結果,若解析失敗,Result處於failure狀態幷包含JSONError類型的錯誤信息。

下面來使用看看:

let jsonObj: [String: Any] = [
    "name": NSNull(),
    "age": "error value",
    "bio": "Hello",
]

typealias JSONResult<T> = Result<T, JSONError>
// valueNotFound
let name: JSONResult<String> = jsonObj.parse("name")
// typeMismatch
let age: JSONResult<Int> = jsonObj.parse("age")
// keyNotFound
let gender: JSONResult<String> = jsonObj.parse("gender")
// success!
let bio: JSONResult<String> = jsonObj.parse("bio")
複製代碼

假設咱們要經過一個JSON Object來構造User實例,按照User中聲明的順序來依次解析每一個字段,當解析到某個字段發生錯誤的時候,咱們返回裝有錯誤信息的Result,若是所有字段解析成功,咱們獲得一個包含User實例的Result。按照傳統的作法,咱們須要這樣編寫代碼:

typealias JSONResult<T> = Result<T, JSONError>

func createUser(jsonObj: [String: Any]) -> JSONResult<User> {
    // name
    let nameResult: JSONResult<String> = jsonObj.parse("name")
    switch nameResult {
    case .success(let name):
        
        // age
        let ageResult: JSONResult<Int> = jsonObj.parse("age")
        switch ageResult {
        case .success(let age):
            
            // bio
            let bioResult: JSONResult<String> = jsonObj.parse("bio")
            switch bioResult {
            case .success(let bio):
                return .success(User(name: name, age: age, bio: bio))
                
            case .failure(let error):
                return .failure(error)
            }
            
        case .failure(let error):
            return .failure(error)
        }
        
    case .failure(let error):
        return .failure(error)
    }
}
複製代碼

上面的代碼層層嵌套、很是繁雜,每個字段解析完畢後咱們還要分狀況作考慮:當解析成功,繼續解析下一個字段,當解析失敗,返回失敗值。若是後期User須要添加或修改字段,這裏的代碼改起來就很是麻煩。

使用Applicative就可以更加優雅地實現上面的需求。

實現

如今爲Result實現Applicative。由於Applicative基於Functor,這裏首先讓Result成爲一個Functor

// Functor
extension Result {
    func map<U>(_ f: (Value) -> U) -> Result<U, Err> {
        switch self {
        case .success(let value):
            return .success(f(value))
        case .failure(let error):
            return .failure(error)
        }
    }
}

func <^> <T, U, E>(lhs: (T) -> U, rhs: Result<T, E>) -> Result<U, E> {
    return rhs.map(lhs)
}

func testFunctor() {
    let value: Result<String, Never> = .success("Hello")
    let result = value.map { $0 + " World" }
}
複製代碼

Result盒子的元信息代表了操做過程當中可能產生的錯誤信息,由於map不會影響到盒子的元信息,因此若是原來的Result是失敗的,那麼獲得的結果也處於失敗的狀態。整個過程就如文章以前所述,將盒子內的數據拿出來應用於函數中,再將獲得的結果裝回盒子。

接着就可讓Result成爲一個Applicative,首先咱們先來看下面的代碼,下面的代碼是徹底按照Applicative的規定來編寫的,可是存在一個很是有趣的問題

// Applicative
extension Result {
    static func pure(_ value: Value) -> Result {
        return .success(value)
    }
    
    func apply<U>(_ f: Result<(Value) -> U, Err>) -> Result<U, Err> {
        switch f {
        case .success(let fun):
            switch self {
            case .success(let value):
                return .success(fun(value))
            case .failure(let error):
                return .failure(error)
            }
        case .failure(let error):
            return .failure(error)
        }
    }
}

func <*> <T, U, E>(lhs: Result<(T) -> U, E>, rhs: Result<T, E>) -> Result<U, E> {
    return rhs.apply(lhs)
}

func testApplicative() {
    let function: Result<(String) -> String, Never> = .success { $0 + " World!" }
    let value: Result<String, Never> = .success("Hello")
    let result = value.apply(function)
}
複製代碼

apply方法中,咱們依次判斷裝有函數和裝有值的Result是否處於失敗狀態,若是是,那麼直接返回失敗結果,不然繼續進行。

上面的代碼問題在哪裏呢?試想咱們設計Result的初衷:咱們但願可以依次按照User中每一個字段的順序去解析JSON,當遇到其中一個字段解析失敗時,直接把錯誤信息封裝在Result返回,並中止後續的解析操做。能夠說,這是一種「短路」的邏輯,可是由於Swift並非一門原生支持惰性求值的語言,而若是咱們按照上面的寫法來爲Result實現Applicative,程序將會把全部的解析邏輯都執行一遍,這樣就違背了咱們的初衷。因此這裏咱們就須要對其進行修改:

// Applicative
extension Result {
    static func pure(_ value: Value) -> Result {
        return .success(value)
    }
}

func <*> <T, U, E>(lhs: Result<(T) -> U, E>, rhs: @autoclosure () -> Result<T, E>) -> Result<U, E> {
    switch lhs {
    case .success(let fun):
        switch rhs() {
        case .success(let value):
            return .success(fun(value))
        case .failure(let error):
            return .failure(error)
        }
    case .failure(let error):
        return .failure(error)
    }
}
複製代碼

爲了實現惰性求值,咱們把Applicative基於面向對象角度編寫的apply方法去掉,把所有實現都放在了<*>運算符的定義中,而後把運算符右邊本來接收的類型Result<T, E>改爲了以Autoclosure形式存在的閉包類型() -> Result<T, E>。這樣,當前一個解析操做失敗時,下面的操做將不會進行,實現了短路的效果。

使用

如今,咱們來使用已經實現ApplicativeResult來重寫上面的JSON解析代碼:

typealias JSONResult<T> = Result<T, JSONError>

func createUser(jsonObj: [String: Any]) -> JSONResult<User> {
    return curry(User.init)
        <^> jsonObj.parse("name")
        <*> jsonObj.parse("age")
        <*> jsonObj.parse("bio")
}
複製代碼

怎麼樣,如今是否是瞬間感受代碼優雅了不少!這裏也多虧了Swift的類型自動推導機制,讓咱們少寫了類型的聲明代碼。

Validation

引入

Validation用於表示某種驗證操做的結果,跟上面提到的Result很是類似,它也是擁有兩種狀態,分別表明驗證成功和驗證失敗。當結果驗證成功,則包含結果數據,當驗證失敗,則包含錯誤信息。ValidationResult不一樣的地方在於對錯誤的處理上。

對於Result,當咱們進行一系列可能產生錯誤的操做時,若前一個操做產生了錯誤,那麼接下來後面全部的操做將不可以被執行,程序直接將錯誤再向上返回,這是一種「短路」的邏輯。可是有些時候咱們想讓所有操做都可以被執行,最終再將各個操做中產生的所有錯誤信息彙總。Validation就是用於解決這種問題。

實現

enum Validation<T, Err: Monoid> {
    case valid(T)
    case invalid(Err)
}
複製代碼

仔細看Validation的定義,咱們會發現其中表示錯誤信息的泛型Err具備Monoid協議的約束,這就說明Validation中的錯誤信息是Monoid(單位半羣)Monoid在個人上一篇文章《函數式編程 - 有趣的Monoid(單位半羣)》中已進行很是詳細的說明,若你們對Monoid的認識比較模糊,能夠查看此文章或者翻閱其餘資料,Monoid的概念在這裏就再也不展開說明。

下面是Monoid的定義:

infix operator <> : AdditionPrecedence

protocol Semigroup {
    static func <> (lhs: Self, rhs: Self) -> Self
}

protocol Monoid: Semigroup {
    static var empty: Self { get }
}

// 爲String實現Monoid
extension String: Semigroup {
    static func <> (lhs: String, rhs: String) -> String {
        return lhs + rhs
    }
}

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

// 爲Array實現Monoid
extension Array: Semigroup {
    static func <> (lhs: [Element], rhs: [Element]) -> [Element] {
        return lhs + rhs
    }
}

extension Array: Monoid {
    static var empty: Array<Element> {
        return []
    }
}
複製代碼

Functor的實現上,ValidationResult並沒有太大區別,咱們能夠以Result的角度去理解:

// Functor
extension Validation {
    func map<U>(_ f: (T) -> U) -> Validation<U, Err> {
        switch self {
        case .valid(let value):
            return .valid(f(value))
        case .invalid(let error):
            return .invalid(error)
        }
    }
}

func <^> <T, U, E: Monoid>(lhs: (T) -> U, rhs: Validation<T, E>) -> Validation<U, E> {
    return rhs.map(lhs)
}
複製代碼

ValidationApplicative的實現上則比起Result大有不一樣。文章上面提到:Functormap不會對盒子元信息產生影響,而Applicativeapply須要將雙方盒子的元信息進行相互做用,以產生新的元信息。而ValidationResult的區別是在於錯誤信息的處理,這屬於的元信息範疇,因此對於map操做ResultValidation無區別,可是apply操做則有所不一樣。

// Applicative
extension Validation {
    static func pure(_ value: T) -> Validation<T, Err> {
        return .valid(value)
    }
    
    func apply<U>(_ f: Validation<(T) -> U, Err>) -> Validation<U, Err> {
        switch (self, f) {
        case (.valid(let value), .valid(let fun)):
            return .valid(fun(value))
        case (.invalid(let errorA), .invalid(let errorB)):
            return .invalid(errorA <> errorB)
        case (.invalid(let error), _), (_, .invalid(let error)):
            return .invalid(error)
        }
    }
}

func <*> <T, U, E: Monoid>(lhs: Validation<(T) -> U, E>, rhs: Validation<T, E>) -> Validation<U, E> {
    return rhs.apply(lhs)
}
複製代碼

上面對於apply的實現分了三種狀況:

  • 若裝有函數和裝有值的Validation盒子都處於成功狀態,那麼將函數應用於值後的結果封裝到一個成功狀態的Validation中。
  • 若兩個Validation其中有一個處於成功狀態,一個處於失敗狀態,那麼將錯誤信息封裝到一個失敗狀態的Validation中。
  • 若兩個Validation都處於失敗狀態,由於Validation中的錯誤信息是Monoid,因此此時將它們的錯誤信息經過<>組合,再將組合結果封裝到一個失敗狀態的Validation中。

使用

假設如今咱們須要完成一個用戶註冊界面的邏輯,用戶須要輸入的內容以及對應的規則限制爲:

  • 用戶名 | 不能爲空
  • 電話號碼 | 長度爲11的數字
  • 密碼 | 長度大於6

若是用戶輸入的內容所有合規,點擊註冊按鈕則能夠向服務器發起提交請求,若用戶輸入的內容存在不合規,則須要把所有不合規的緣由彙總起來並提醒用戶。

首先編寫模型類和按鈕點擊的觸發方法:

struct Info {
    let name: String
    let phone: Int
    let password: String
}

func signIn(name: String?, phone: String?, password: String?) {
    // TODO ...
}
複製代碼

Info模型用於保存合規的用戶輸入內容,最終做爲服務器請求的參數。

當按鈕點擊後,signIn方法將會被調用,咱們從UITextField中分別取出用戶輸入的內容name、phone、password,傳入,它們的類型都是String?。這個方法剩下的邏輯將會在後面補上。

此時咱們就要針對不一樣的內容編寫規則判斷以及轉換邏輯,這裏咱們就能夠用到Validation

func validate(name: String?) -> Validation<String, String> {
    guard let name = name, !name.isEmpty else {
        return .invalid(" 用戶名不能爲空 ")
    }
    return .valid(name)
}

func validate(phone: String?) -> Validation<Int, String> {
    guard let phone = phone, !phone.isEmpty else {
        return .invalid(" 電話號碼不能爲空 ")
    }
    guard phone.count == 11, let num = Int(phone) else {
        return .invalid(" 電話號碼格式有誤 ")
    }
    return .valid(num)
}

func validate(password: String?) -> Validation<String, String> {
    guard let password = password, !password.isEmpty else {
        return .invalid(" 密碼不能爲空 ")
    }
    guard password.count >= 6 else {
        return .invalid(" 密碼長度需大於6 ")
    }
    return .valid(password)
}

複製代碼

在這裏,咱們用String類型來表示Validation中的錯誤信息,文章上面已經爲String實現了Monoid,它的append操做就是將兩個字符串相鏈接,empty則是一個空字符串。

對於每種輸入內容,咱們會進行不一樣的合規判斷,若是輸入不合規,那麼將返回裝有錯誤信息的失敗Validation,不然將返回裝有結果的成功Validation

如今,咱們就能夠經過Validation來將用戶輸入的內容進行合規檢查和數據換行了:

let info = curry(Info.init)
    <^> validate(name: name)
    <*> validate(phone: phone)
    <*> validate(password: password)
複製代碼

info的類型爲Validation<Info>,咱們將經過它來判斷究竟須要提醒用戶輸入不合規仍是直接發起服務器請求。

最終signIn方法的代碼爲:

func signIn(name: String?, phone: String?, password: String?) {
    let info = curry(Info.init)
        <^> validate(name: name)
        <*> validate(phone: phone)
        <*> validate(password: password)

    switch info {
    case .invalid(let error):
        print("Error: \(error)")
        // TODO: 向用戶展現錯誤信息(可經過UILabel)
    case .valid(let info):
        print(info)
        // TODO: 發起網絡請求
    }
}
複製代碼

下面就來測試一下這個方法:

signIn(name: "Tangent", phone: "123", password: "123")
複製代碼

上面的執行最終會在控制檯打印出結果:Error: 密碼長度需大於6 電話號碼格式有誤

除了上文談到的ResultValidationApplicative還有其餘不少實現,咱們甚至能夠將它用於構造響應式的小型工具上。因爲篇幅問題,文章在此就再也不細講,有興趣的小夥伴能夠查閱相關資料進行了解,而說不定將來我也會再出一篇文章進行介紹。

若你們對文章有疑惑,歡迎在評論區留言。

相關文章
相關標籤/搜索