详解函数式编程中的函子

作者: 前端辉羽 | 来源:发表于2020-09-23 16:57 被阅读0次

    本文目录:
    1.什么是函子
    2.MayBe函子
    3.Either函子
    4.Pointed函子
    5.IO函子
    6.IO函子存在的问题
    7.Monad函子(单子)

    1.什么是函子

    函子是一个包含值和值的变形关系(这个变形关系就是函数)的特殊容器,函子通过一个普通的对象来实现,该对象具有map方法,map可以运行一个函数对值进行处理(变形关系),并最终返回一个包含了新值的新函子。
    看下面这段代码

    class Container {
        static of (value) {
            return new Container(value)
        }
        constructor(value) {
            this._value = value
        }
        map(fn) {
            return Container.of(fn(this._value))
        }
    }
    let r = Container.of(5)
        .map(x => x + 2)
        .map(x => x * x)
    console.log(r)
    

    在node下运行代码,在控制台上输出
    Container { _value: 49 }

    代码解析:
    首先明确一点,该代码返回的是一个名字叫Container的函子,这个名字是我们自己定义的。
    函子的里面维护了一个值,这个值不对外公布,以_开头代表私有,另外对外公布一个map方法,map方法接收一个处理值的函数,当我们调用map方法的时候,去调用fn去处理值,并且把处理后的结果传递给新的函子进行保存。
    of是Container函子的静态方法,目的就是为了让Container在使用的时候不用再频繁的写new,因为new的出现会让代码显得非常的面向对象。

    2.MayBe函子

    上面的Container函子如果在调用of,也就是new的时候传入的value是null,会造成函数执行异常,这是一个典型的副作用,我们要想办法去规避。
    MayBe函子的作用就是在内部增加了一个处理机制,对异常的数据进行判定,从而控制副作用在允许的范围内。
    下面是一个MayBe函子的代码,注意,这个MayBe的名字同样是可以自定义的。

    class MayBe {
        static of (value) {
            return new MayBe(value)
        }
        constructor(value) {
            this._value = value
        }
        map(fn) {
            return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value))
        }
        isNothing() {
            return this._value === null || this._value === undefined
        }
    }
    let r = MayBe.of('Hello World').map(x => x.toUpperCase())
    console.log(r)
    

    运行代码,输出结果为
    MayBe { _value: 'HELLO WORLD' }
    前面有说过,map方法最终返回的是一个包含新值的新函子,所以可以进行链式调用

    let r = MayBe.of('Hello World').map(x => x.toUpperCase()).map(x => null).map(x => x.split(' '))
    

    输出结果为
    MayBe { _value: null }
    虽然其中某一个环节的传参有误,代码也并没有报错,实际情况的传参当然不会想演示代码这么简单,这导致了我们却没法去准确的判断是哪个环节出了错。

    3.Either函子

    Either的意思就是指两者中的任何一个,类似于if...else的处理。
    异常会让函数变得不纯,所以我们可以用Either函子去进行异常的处理。
    首选我们封装一个Left函子和一个Right函子

    class Left {
        static of (value) {
            return new Left(value)
        }
        constructor(value) {
            this._value = value
        }
        map(fn) {
            return this
        }
    }
    class Right {
        static of (value) {
            return new Right(value)
        }
        constructor(value) {
            this._value = value
        }
        map(fn) {
            return Right.of(fn(this._value))
        }
    }
    

    我们再封装一个函数parseJSON用来进行异常的捕获

    function parseJSON(str) {
        try {
            return Right.of(JSON.parse(str))
        } catch (e) {
            return Left.of({
                error: e.message
            })
        }
    }
    

    接下来我们调用parseJSON,并其中一个故意传入错误的json数据,另外一个传入正确的

    let l = parseJSON(`{name:zs}`)
    console.log(l)
    let r = parseJSON('{"name":"zs"}')
    console.log(r)
    

    打印结果如下

    Left {
      _value: { error: 'Unexpected token n in JSON at position 1' } }
    Right { _value: { name: 'zs' } }
    

    对于r,我们当然还可以调用map,对传入的数据进行处理。

    let r = parseJSON('{"name":"zs"}').map(x => x.name.toUpperCase())
    

    打印结果为
    Right { _value: 'ZS' }
    l因为在parseJSON的时候,异常就被捕获了,并且将错误信息进行了返回,所以后面自然不会再进行任何操作。

    4.Pointed函子

    Pointed函子是实现了of静态方法的函子。
    of方法是为了避免使用new来创建对象,更深层的含义是of方法用来把值放到上下文context中(把值放到容器中,使用map来处理值)

    5.IO函子

    IO函子的_value是一个函数,这里是把函数作为值来处理。
    IO函子可以把不纯的动作存储到_value中,并延迟执行这个不纯的操作(惰性执行),把不纯的操作交由调用者来处理。

    class IO {
        static of (x) {
            return new IO(function () {
                return x
            })
        }
        constructor(fn) {
            this._value = fn
        }
        map(fn) {
            return new IO(fp.flowRight(fn, this._value))
        }
    }
    

    调用这个函子
    let r = IO.of(process).map(p => p.execPath)
    此时打印结果,r也是一个函子,但区别是_value里面存储的是一个函数,我们需要进一步调用才能获得_value的值。

    console.log(r)  //IO { _value: [Function] }
    console.log(r._value())  // C:\Program Files\node.js\node.exe
    

    6.IO函子存在的问题

    首先需要引入node的fs模块和lodash的fp模块

    const fs = require('fs')
    const fp = require('lodash/fp')
    

    然后封装一个IO函子,下面的函数其实不能叫函子了,因为经过readFile和print的进一步封装,of和map方法都没有起到作用,把它们都注释掉也不会影响代码的执行。

    class IO {
        // static of (x) {
        //     return new IO(function () {
        //         return x
        //     })
        // }
        constructor(fn) {
            this._value = fn
        }
        // map(fn) {
        //     return new IO(fp.flowRight(fn, this._value))
        // }
    }
    

    接下来我们封装两个函数,分别用来读取文件和打印读取到的内容。

    var readfile = function (filename) {
        return new IO(function () {
            return fs.readFileSync(filename, 'utf-8')
        })
    }
    var print = function (x) {
        return new IO(function () {
            console.log(x)
            return x
        })
    }
    

    我们组合这两个函数,并进行调用

    let cat = fp.flowRight(print, readfile)
    let r = cat('package.json')
    console.log(r)
    

    此时的打印结果为
    IO { _value: [Function] }
    Function实际上是 IO(IO(x)) ,是一个嵌套的函子,这个函子的外层是print返回的函子,里面的是readFile返回的函子。此时函数真正的代码并没有去执行,只有进一步的调用才会去真正的执行读取文件和打印代码的操作。
    let r = cat('package.json')._value()._value() 这样运行代码,在控制台中就会读取并打印出文件代码。
    每次都需要这样调用太过于麻烦,所以接下来要说的是Monad函子

    7.Monad函子(单子)

    上面的readfile和print函数在调用的时候就相当于调用了Pointed函子的of静态方法,因为对于普通函子而言,of接收的参数是普通的值value,IO函子的of接收的是一个带有返回值的函数fn,而经过readfile和print函数的封装,此时相当于调用IO函子的of静态方法时传入的参数是一个不是一个普通函数,而是一个IO函子,在接下来通过fp.flowRight(print, readfile)进行函数组合的时候,结果自然就形成了函子的嵌套关系。
    如果函数嵌套的话,我们可以使用函数组合去解决,如果函子嵌套的话,我们使用Monad函子解决。
    我们在使用monad时候,经常会把map和join联合起来去使用,因为map的作用就是当前的函数和函子内部的value组合起来,返回一个新的函子,map在组合函数的时候,这个函数最终也会返回一个函子,这时候需要调用join把它变扁和拍平,而flatMap的作用就是同时去调用map和jion。
    一个函子如果具有join和of两个方法并遵守一些定律就是一个Monad函子。
    上面的IO函子我们可以变成下面这样

    class IO {
        static of (x) {
            return new IO(function () {
                return x
            })
        }
        constructor(fn) {
            this._value = fn
        }
        map(fn) {
            return new IO(fp.flowRight(fn, this._value))
        }
        join() {
            return this._value()
        }
        flatMap(fn) {
            return this.map(fn).join()
        }
    }
    

    调用的时候不需要再去组合函数cat,也不再需要进行多层的_value()的调用最终执行代码。

    let r = readfile('package.json').flatMap(print).join()
    console.log(r)
    

    上面代码直接就可以将读取到的文件代码打印在控制台上。

    相关文章

      网友评论

        本文标题:详解函数式编程中的函子

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