函数式编程 - 酷炫Applicative(应用函子) [Swift描述]

栏目: Swift · 发布时间: 5年前

内容简介:Applicative functor(应用函子),简称很多函数式编程的概念在我之前写的文章中已经介绍过,一些相关的也会将在这篇文章中被重复提及,以加深认识。
函数式编程 - 酷炫Applicative(应用函子) [Swift描述]

引言

Applicative functor(应用函子),简称 Applicative ,作为函数式编程里面一个比较重要的概念,其具有十分酷炫的特性,在工程上的实用性也非常高。这篇文章将会以工程的角度不断深入、层层剖析 Applicative ,在阐述其概念的同时也会结合小Demo进行实战演示。

很多函数式编程的概念在我之前写的文章中已经介绍过,一些相关的也会将在这篇文章中被重复提及,以加深认识。

Functor(函子)

Applicative 本身也是 Functor ,其基于 Functor 有一套自己额外的概念,用面向对象的角度去理解,可以认为 Applicative 继承自 Functor 。在进一步介绍 Applicative 之前,我们先来认识一下 Functor

概念及引入

什么是 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类型变量的时候做自动类型转换的优化(反过来就可以)。要得到结果值,传统的做法我们可能会这么写:

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 电话号码格式有误


以上所述就是小编给大家介绍的《函数式编程 - 酷炫Applicative(应用函子) [Swift描述]》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

编程卓越之道

编程卓越之道

海德 / 张菲 / 电子工业出版社 / 2007-4 / 69.00元

《编程卓越之道第二卷:运用底层语言思想编写高级语言代码》是《编程卓越之道》系列书的第二卷,将探讨怎样用高级语言(而非汇编语言)编程得到高效率机器代码。在书中,您可以学到如何分析编译器的输出,以便检验代码的所作所为,从而得到高质量的机器码;了解编译器为常见控制结构生成的典型机器指令,以便在编写高级语言程序时选用恰当的语句;掌握编译器将各种常量和变量类型转换成机器数据的方法,以便于使用这些数据写出又快......一起来看看 《编程卓越之道》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具