详解函数式编程中的函子

作者: 前端辉羽 | 来源:发表于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)

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

相关文章

  • Functor函子

    为什么学习函子? 函子是函数式编程里面最重要的数据类型,也是基本的运算单位和功能单位 函子作用:控制函数式编程中的...

  • 详解函数式编程中的函子

    本文目录:1.什么是函子2.MayBe函子3.Either函子4.Pointed函子5.IO函子6.IO函子存在的...

  • 函子的概念

    一、函子 为什么要学习函子? 到目前为止已经学习了函数式编程的一些基础,但是我们还没有演示在函数式编程中如何把副作...

  • Swift函数式编程十三(函子、适用函子、单子)

    解释一些函数式编程中的专用术语和一些常⻅模式,比如函子 (Functor)、适用函子 (Applicative F...

  • 深入浅出Rxjs笔记 一

    一.函数式编程 函数式编程要求: 声明式 纯函数 数据不可变js 不算纯粹意义上的函数式编程语言,但是,在js中函...

  • 函数式编程概念总结

    函数式编程 对应于 命令式编程诞生。 函数式编程 强调数据流。强调计算什么,而不是怎么计算。 就是从函子(容器)传...

  • 函数式编程——Functor、Applicative、Monad

    原文链接 了解函数式编程的同学可能或多或少都听说过 函子(Functor)、适用函子(Applicative)、单...

  • 函数式编程小思考4.2 笔记

    函数式编程小思考4 笔记JS函数式编程指南Data.Task 函子 源码 补充两个内容 用得到的curry函数 文...

  • Python-函数式编程

    函数式编程简介 一、什么是函数式编程? 函数:function函数式:functional,是一种编程范式 二、函...

  • 函数式编程(四)

    函子(Functor) 函子的作用,在函数式编程中如何把副作用控制在可控的范围内、异常处理、异步操作等。 什么是 ...

网友评论

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

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