美文网首页iOS Swift && Objective-CMore Stronger函数式编程
函数式编程 - 一篇文章概述Functor(函子)、Monad(

函数式编程 - 一篇文章概述Functor(函子)、Monad(

作者: Tangentw | 来源:发表于2017-07-31 03:32 被阅读1112次

    前言

    初步深入函数式编程是在寒假的时候,搞了一本Haskell的书,啃了没多久就因为我突然的项目任务被搁置了,不过在学习的时候也是各种看不懂,里面的概念略微抽象,再加上当时没有适当地实战敲Demo,导致没过多久脑袋就全空了。庆幸的是,Swift是一门高度兼容函数式编程范式的语言,而我又是一只喜欢敲Swift的程序Dog,在后来我使用Swift编码时,有意识或无意识地套用函数式编程范式的一些概念,也渐渐加深我对函数式编程的理解。这篇文章是我对自己所掌握的函数式编程的一个小总结,主要探讨的是函数式编程中的几个概念: FunctorApplicativeMonad以及它们在Swift中的表现形式。由于本人能力有限,一些概念上的不严谨、编码上的不全面希望大家多包涵,欢迎留下各位宝贵的意见或问题。

    本文为纯概念讲述,后期或许会有函数式编程实战的文章推出(我有空写再说吧)

    概念

    Context

    在编码时,我们会遇到各种数据类型,基础的数据类型我们称作,当然这并不是指编程语言中的基本数据类型,比如说整形1它可以称作一个值,一个结构体struct Person { let name: String; let age: Int }的实例也可以成为一个值,那么何为Context(上下文)呢,我们可以将它理解为对值的一个包装,通过这层包装,我们可以得知值此时所处在的一个状态。在Haskell中,这个包装就是typeclass(类型类),而在Swift中,魔性的enum(枚举)可以充当这个角色,一个例子,就是Swift中的Optional(可选类型),它的定义如下(相关继承或协议关系在这里不标出):

    Optional<Wrapped> {
        case none
        case some(Wrapped)
    

    Optional有两种状态,一种是空状态none,也就是和平时我们传入的nil相等价,一种是存在值的状态,泛型Wrapped指代被包入这层上下文的值的类型。通过这个例子,我们可以很直观地理解Context:描述值在某一阶段的状态。当然,在平时开发中,我们会见到各种Context,比如Either:

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

    它代表在某个阶段值可能在left或者right中存在。
    在一些函数式响应式编程框架如ReactiveCocoaRxSwift中,Context无处不在:RACSignalObservable,甚至是Swift的基本类型Array(数组)它本身也可以看作是一个Context。可见,只要你接触了函数式编程,Context即会接触。

    这里,我特别说下这个Context:Result,因为在后面对其他概念以及实战的讲述中我都会以它为基础:

    enum Result<T> {
        case success(T)
        case failure(MyError)
    }
    

    Result上下文存在两种状态,一种是成功的状态,当处于这个状态,Result就会持有一个特定类型的值,另外一种状态是失败状态,在这个状态中,你可以获取到一个错误的实例(这个实例可以是你自己拟定的)。这么这个Context有什么用呢?想象一下,你正在进行一项网络操作,获取到的数据是无法确定的,你或许能如你所愿,从服务器中获取到你期望的值,但是也有可能此时服务器发生一些未知的错误,或者网络延时,又或是一些不可抗力的影响,那么,此时你得到的将会是一个错误的表示,如HTTP Code 500...而Result可以在这种情况下引入来表示你在网络操作中获取到的最终结果,是成功还是失败。除了网络请求,诸如数据库操作、数据解析等等,Result都可以引入来进行更明确的标示。

    何为Functor、Applicative、Monad?

    你可以把FunctorApplicativeMonad想象成Swift中的Protocol(协议),它们可以为某种数据结构的抽象,而这种数据接口正是刚刚我在上面提到的Context,要将某个Context实现成FunctorApplicativeMonad,你必须实现其中特定的函数,所以,要了解什么是FunctorApplicativeMonad,你需要知道它们定义了那些协议函数。接下来我会一一讲解。

    Functor

    我们对一个值的运算操作使用的是函数,比如我要对一个整形的值进行翻倍操作,我们可以定义一个函数:

    func double(_ value: Int) -> Int {
        return 2 * value
    }
    

    然后就可以拿这个函数对特定的值进行操作:

    let a = 2
    let b = double(a)
    

    好,问题来了,如果此时这个值被包在一个Context中呢?
    一个函数只能作用于它声明好的特定类型的值,运算整形的函数不能用来运算一个非整形的Context,所以这时,我们引入了Functor。它要做的,就是使一个只能运算值的函数用来运算一个包有这个值类型的Context,最后返回的一个包有运算结果的Context,为此,我们要实现map这个函数(在Haskell中为fmap),它的伪代码是这样的:
    Context(结果值) = map(Context(初始值), 运算函数)

    现在我们拿Result来实现一下:

    extension Result {
        func map<O>(_ mapper: (T) -> O) -> Result<O> {
            switch self {
            case .failure(let error):
                return .failure(error)
            case .success(let value):
                return .success(mapper(value))
            }
        }
    }
    

    我们可以看到,首先我们对Result进行模式匹配,当此时状态是失败的话,我们也直接返回失败,并把错误的实例传递下去,如果状态是成功的,我们就对初始的值进行运算,最后返回包有结果值的成功状态。
    为了后面表达式简便,我在这里定义了map的运算符<^>

    precedencegroup ChainingPrecedence {
        associativity: left
        higherThan: TernaryPrecedence
    }
    
    // Functor
    infix operator <^> : ChainingPrecedence
    
    // For Result
    func <^><T, O>(lhs: (T) -> O, rhs: Result<T>) -> Result<O> {
        return rhs.map(lhs)
    }
    

    我们现在就可以测试一下:

    let a: Result<Int> = .success(2)
    let b = double <^> a
    

    在上面我提到,Swift的数组也可以当成是Context,它是作为一个包有多个值的状态存在。想必在日常开发中我们经常也用到了Swift数组中的map函数吧:

    let arrA = [1, 2, 3, 4, 5]
    let arrB = arrA.map(double)
    

    RxSwift中我们也经常使用map

    let ob = Observable.just(1).map(double)
    

    Applicative

    Applicative其实就是高级的Functor,我们可以调出上面Functormap伪代码:
    Context(结果值) = map(Context(初始值), 运算函数)
    在函数式编程中,函数也可以作为一个值来看待,若此时这个函数也是被一个Context包裹的,单纯的map是不能接受包裹着函数的Context,所以我们引入了Applicative
    Context(结果值) = apply(Context(初始值), Context(运算函数))

    我们将Result实现Applicative

    extension Result {
        func apply<O>(_ mapper: Result<(T) -> O>) -> Result<O> {
            switch mapper {
            case .failure(let error):
                return .failure(error)
            case .success(let function):
                return self.map(function)
            }
        }
    }
    
    // Applicative
    infix operator <*> : ChainingPrecedence
    
    // For Result
    func <*><T, O>(lhs: Result<(T) -> O>, rhs: Result<T>) -> Result<O> {
        return rhs.apply(lhs)
    }
    

    使用:

    let function: Result<(Int) -> Int> = .success(double)
    let a: Result<Int> = .success(2)
    let b = function <*> a
    

    Applicative在日常开发中其实用的不多,很多时候我们并不会将一个函数塞进一个Context上,但是如果你用了一些略为高阶的函数时,它强劲的能力就能在此时表现出来,这里举一个略为晦涩的例子,你可以花点时间搞懂它:
    这个例子的思路是来自源Swift的函数式JSON解析库Argo的基本用法,若大家有兴趣可以阅读下Argo的源码: thoughtbot/Argo

    假设现在我定义了一个函数,它能够接受一个Any的JSON Object,以及一个值在JSON中对应的Key(键)作为参数,返回一个从JSON数据中解析出来的结果,由于这个结果是不确定的,可能JSON中不存在此键对应的值,所以我们用Result来包装它,这个函数的签名为:

    func parse<T>(jsonObject: Any, key: String) -> Result<T>
    

    当解析成功时,返回的Result处于成功状态,当解析失败时,返回的Result处于失败状态并携带错误的实体,我们能够通过错误实体得知解析失败的原因。

    现在我们有一个结构体,它里面有多个成员,它实现了默认的构造器:

    struct Person {
        let name: String
        let age: Int
        let from: String
    }
    

    我们自己可以编写一套函数柯里化的库,这个库能够对多参数的函数进行柯里化,你也可以从Github中下载: thoughtbot/Curry
    比如,我们有一个函数,它的基本签名是: func haha(a: Int, b: Int, c: Int) -> Int,通过函数柯里化我们可以将其转化为(Int) -> (Int) -> (Int) -> Int类型的函数。
    我们此时将Person的构造器进行函数柯里化:curry(Person.init),此时我们得到的是类型为(String) -> (Int) -> (String) -> Person的值。
    现在奇幻的魔法来了,我定义一个将JSON解析成Person的函数:

    func parseJSONToPerson(json: Any) -> Result<Person> {
        return curry(Person.init)
            <^> parse(jsonObject: json, key: "name")
            <*> parse(jsonObject: json, key: "age")
            <*> parse(jsonObject: json, key: "from")
    }
    

    通过这个函数,我能够将一个JSON数据解析成Person的实例,以一个Result的包装返回,如果解析失败,Result处理失败状态会携带一个错误的实例。

    这个函数为什么可以这么写呢,我们来分解一下:
    首先通过函数的柯里化我们得到了类型为(String) -> (Int) -> (String) -> Person的值,它也是一个函数,然后经过了<^>map的操作,map的右边是一个解析了name返回的Result,它的类型为Result<String>,map将函数(String) -> (Int) -> (String) -> Person应用于Result<String>,此时我们得到的是返回的结果(Int) -> (String) -> Person的Result包装:Result<(Int) -> (String) -> Person>(因为已经消费掉了一个参数),此时,这个函数就被一个Context包裹住了,后面我们不能再用map去将这个函数应用在接下来解析出来的数据了,所以这是我们就借助于Applicative<*>,接下来看第二个参数,parse函数将JSON解析返回了类型为Result<Int>的结果,我们通过<*>Result<(Int) -> (String) -> Person>的函数取出来,应用于Result<Int>,就得到了类型为Result<(String) -> Person>的结果。以此类推,最终我们就获取到了经JSON解析后的结果Result<Person>
    Applicative强大的能力能够让代码变得如此优雅,这就是函数式编程的魅力之所在。

    Monad

    Monad中文称为单子,网上看到挺多人被Monad的概念所搞晕,其实它也是基于上面所讲述的概念而来的。对于使用过函数式响应式编程框架(Rx系列[RxSwift、RxJava]、ReactiveCocoa)的人来说,可能不知道Monad是什么,但是在实战中肯定用过,它所要求实现的函数说白了就是flatMap

    let ob = Observable.just(1).flatMap { num in
        Observable.just("The number is \(num)")
    }
    

    有很多人喜欢用降维来形容flatMap的能力,但是,它能做的,不止如此。
    Monad需要实现的函数我们可以称为bind,在Haskell中它使用符号>>=,在Swift中我们可以定义运算符>>-来表示bind函数,或者直接叫做flatMap。我们先来看看他的伪代码:
    首先我们定义一个函数,他的作用是将一个值进行包装,这里标示出这个函数的签名:
    function :: 值A -> Context(值B)(值A与值B的类型可相同亦可不同)
    我们的bind函数就可以这么写了:
    Context(结果值) = Context(初始值) >>- function
    这里我们实现一下ResultMonad

    extension Result {
        func flatMap<O>(_ mapper: (T) -> Result<O>) -> Result<O> {
            switch self {
            case .failure(let error):
                return .failure(error)
            case .success(let value):
                return mapper(value)
            }
        }
    }
    
    // Monad
    infix operator >>- : ChainingPrecedence
    
    // For Result
    func >>-<T, O>(lhs: Result<T>, rhs: (T) -> Result<O>) -> Result<O> {
        return lhs.flatMap(rhs)
    }
    

    Monad的定义很简单,但是Monad究竟能帮我们解决什么问题呢?它要怎么使用呢?别急,通过以下这个例子,你就能对Monad有更深一层的理解:
    假设现在我有一系列的操作:

    1. 通过特定条件进行本地数据库的查询,找出相关的数据
    2. 利用上面从数据库得到的数据作为参数,向服务器发起请求,获取响应数据
    3. 将从网络获取到的原始数据转换成JSON数据
    4. 将JSON数据进行解析,返回最终解析完成的有特定类型的实体

    对以上操作的分析,我们能得知以上每一个操作它的最终结果都具有不确定性,意思就是说我们无法保证操作百分百完成,能成功返回我们想要的数据,所以我们很容易就会想到利用上面已经定义的Context:Reuslt将获取到的结果进行包裹,若获取结果成功,Result将携带结果值处于成功状态,若获取结果失败,Result将携带错误的信息处于失败状态。
    现在,我们针对以上每种操作进行函数定义:

    // A代表从数据库查找数据的条件的类型
    // B代表期望数据库返回结果的类型
    func fetchFromDatabase(conditions: A) -> Result<B> { ... }
    
    // B类型作为网络请求的参数类型发起网络请求
    // 获取到的数据为C类型,可能是原始字符串或者是二进制
    func requestNetwork(parameters: B) -> Result<C> { ... }
    
    // 将获取到的原始数据类型转换成JSON数据
    func dataToJSON(data: C) -> Result<JSON> { ... }
    
    // 将JSON进行解析输出实体
    func parse(json: JSON) -> Result<Entity> { ... }
    

    现在我们假设所有的操作都是在同一条线程中进行的(非UI线程),如果我们只是纯粹地用基本的方法去调用这些函数,我们可能要这么来:

    var entityResult: Entity?
    if case .success(let b) = fetchFromDatabase(conditions: XXX) {
        if case .success(let c) = requestNetwork(parameters: b) {
            if case .success(let json) = dataToJSON(data: c) {
                if case .success(let entity) = parse(json: json) {
                    entityResult = entity
                }
            }
        }
    }
    

    这代码写起来也好看起来也好真的是一把辛酸泪啊,而且,这里还有一个缺陷,就是我们无法从中获取到错误的信息,如果我们还想要获取到错误的信息,必须再编写多一大串代码了。

    此时,Monad出场了:

    let entityResult = fetchFromDatabase(conditions: XXX) >>- requestNetwork >>- dataToJSON >>- parse
    

    吓到了吧,只需一行代码,即可将所有要做的事情连串起来了,并且,最终我们获取到的是经Result包装的数据,若在操作的过程中发生错误,错误的信息也记录在里面了。
    这就是Monad的威力

    当然,我们可以继续对上面的操作进行优化,比如说现在我需要在网络请求的函数中加多一个参数,表示请求的URL,我们可以这样来定义这个网络请求函数:

    // B类型作为网络请求的参数类型发起网络请求
    // 获取到的数据为C类型,可能是原始字符串或者是二进制
    func requestNetwork(urlString: String) -> (B) -> Result<C> {
        return { parameters in
            return { ... }
        }
    }
    

    调用的时候我们只需要这样调用:

    let entityResult = fetchFromDatabase(conditions: XXX) >>- requestNetwork(urlString: "XXX.com/XXX/XXX") >>- dataToJSON >>- parse
    

    这主要是高阶函数的使用技巧。

    个人对Monad作用的总结有两部分:

    1. 对一系列针对值与Context的操作进行链式结合,代码极其优雅,清晰明了。
    2. 将值与Context之间的转换、Context内部进行的操作对外屏蔽,像上面我用原始的方式进行操作,我们需要手动地分析Context的情况,手动地针对不同的Context状态进行相应的操作,而如果我们使用Monad,整一流程下来我们什么都不需要做,坐享其成,取得最终的结果。

    总结

    Swift是一门高度适配函数式编程范式的语言,你可以在里面到处都能找到函数式编程思想的身影,通过上面对FunctorAppliactiveMonad相关概念的讲述,在巩固我对函数式编程的知识外,希望也能让你对函数式编程的理解有帮助,若文章有概念不严谨的地方或者错误,望见谅,也希望能够向我提出。
    谢谢阅读。

    参考链接

    阮一峰的网络日志 - 图解 Monad

    相关文章

      网友评论

      • SSBun:讲的很好,一直都有疑问,现在一下子清晰了很多
      • 南栀倾寒:可以坚持把hashkell学完 会有非常大得收货
      • 07d93406ec39:那段先 curry 先使用 map 再使用 apply 的地方,講的很清楚,解決了困擾,感謝
      • 丶Runs:写作手法跟阮一峰一样厉害

      本文标题:函数式编程 - 一篇文章概述Functor(函子)、Monad(

      本文链接:https://www.haomeiwen.com/subject/nylllxtx.html