1-2-2-函子

函子

到目前位置我们已经学习了函数式编程的一些基础,但是我们还没有演示在函数式编程中如何把副作用控制在可控的范围内、异常处理、异步操作等。

什么是Functor

  • 容器:包含值和值的变形关系(这个变形关系就是函数)
  • 函子:是一个特殊的容器,通过一个普通的对象来实现,该对象具有map方法,map方法可以运行一个函数参数对值进行处理(变形关系)
// Functor 函子
// 函子是一个普通的对象,这个对象里维护一个私有的值,并且对外公布一个map方法。所以可以使用一个类来描述一个函子。

class Container {
    constructor (value) {
        this._value = value // 约定以下划线开头的成员都是私有的成员
    }

    map (fn) {
        return new Container(fn(this._value))
    }
}

let r = new Container(5)
    .map(x => x + 1)
    .map(x => x * x)

    console.log(r) // Container { _value: 36 }

map 方法最终返回的不是值,而是一个新的函子对象,在这个新的函子对象里面去保存新的值,这个值始终不对外公布,想要处理这个值只能通过调用这个函子的map方法去调用我们传入的处理这个数据的函数。

每次创建函子的时候都要 new ,所以可以将创建函子的方法封装一下。

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 + 1)
    .map(x => x * x)

    console.log(r) // Container { _value: 36 }

函子总结

  • 函数式编程的运算不直接操作值,而是由函子完成。
  • 函子就是一个实现了map契约的对象。
  • 我们可以把函子想象成一个盒子,这个盒子里封装了一个值。
  • 想要处理盒子中的值,我们需要给盒子的 map 方法传递一个处理值的函数(纯函数),有这个函数来对值进行处理。
  • 最终map方法返回一个包含新值的盒子(函子)。

有多少运算就有多少函子,可以使用不用的汉字来解决不同的问题。

// 演示null undefined 的问题
Container.of(null)
    .map(x => x.toUpperCase())

这段代码会报错,这就说明传入的这个函数并不是一个纯函数,因为纯函数对于不同的输入始终会有不同的输出,而输入null的时候,就没有输出,而是报错。这个 null 就是副作用。如何解决这个副作用呢?

MayBe 函子

MayBe ,可能会是……

  • 编程的过程中可能会遇到很多错误,需要对这些错误做相应的处理
  • MayBe 函子的作用就是可以对外部的空值情况做处理(控制副作用在允许的范围)
// MayBe 函子
class MayBe {
    static of (value) {
        return new MayBe(value)
    }
    constructor (value) {
        this._value = value
    }
    isNothing () {
        return (this._value === undefined || this._value === null)
    }
    map (fn) {
        return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value))
    }
}
// let r = MayBe.of("hello world")
//     .map(x => x.toUpperCase())
//     console.log(r)

// let r = MayBe.of(undefined)
//     .map(x => x.toUpperCase())
//     console.log(r) // MayBe { _value: null }

let r = MayBe.of(undefined)
    .map(x => x.toUpperCase())
    .map(null)
    .map(x => split(' '))
    console.log(r) // MayBe { _value: null }

上述代码中解决了传入的值为 null 或者 undefined 的副作用,但是,在最后的调用中,最后返回的函子对象中的值为 null ,但是我们链式操作了三步,就不知道这个 null 是在那一步中产生的。怎么解决呢?

Either 函子

  • Either 两者中的任何一个,类似于if…else…的处理。
  • 异常会让函数变得不纯,Either 函子可以用来做异常处理。
// Either 函子

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))
    }
}
function parseJSON (str) {
    try {
        return Right.of(JSON.parse(str))
    } catch (e) {
        return Left.of({ error: e.message })
    }
}
// let r = parseJSON('{ name: zs }')
// console.log(r) // Left { _value: { error: 'Unexpected token n in JSON at position 1' } }
let r = parseJSON('{ "name": "zs" }')
    .map(x => x.name.toUpperCase())
console.log(r) // Right { _value: 'ZS' }

IO 函子

  • IO函子中的 _value 是一个函数,这里是把函数作为值来处理的。
  • IO函子可以把不纯的动作存储到 _value 中,延迟执行这个不纯的操作(惰性执行)。
  • 把不纯的操作交给调用者来处理。
// IO 函子
const fp = require('lodash/fp')

class IO {
    static of (value) {
        return new IO(function () {
            return value
        })
    }
    constructor (fn) {
        this._value = fn
    }
    map (fn) {
        return new IO(fp.flowRight(fn, this._value))
    }
}
// 调用
let r = IO.of(process).map(p => p.execPath)
console.log(r, r._value()) // IO { _value: [Function] } D:\App\nodejs\node.exe

IO 函子就是将普通的函子中的 _value 改成了一个返回 “_value” 的函数,当给函子传递一个函数的时候,就算这个函数是一个不纯的操作,它最终返回的新的函子的 _value 也会是一个固定的函数,这样就满足了对于不同的输入就会有不同的输出,且不会出现异常,就算是出现异常也是在延迟到后面的操作出现的,这样就对某些不纯的操作(副作用)进行了有效的控制。

本质上,就是把map中传递的有可能不纯的操作延迟到 _value 这个函数的时候执行的时候。从而保证函子的副作用在可控的范围内发生。

folktale

函子可以帮我们控制副作用,进行异常处理,还可以去处理异步任务,因为在异步任务中会出现地狱回调使用 task 函子可以避免回调地狱。

Task 异步执行

  • folktale 一个标准的函数式编程库

    • 和lodash、ramda不同的是,他没有提供很多功能函数。
    • 只提供了一些函数式处理的操作,例如:compose 、curry等,一些函子Task、Either、MayBe等
//**********************************************folktale */ 
const {compose, curry} = require('folktale/core/lambda')
const {toUpper, first} = require('lodash/fp')

let fn = compose(toUpper, first)
console.log(fn('aaa', 'bbb', 'ccc'))

let f = curry(3, (a, b, c) => {
    return a + b + c
})
console.log(f(1)(2)(3))

Task函子

// *****************************************task
// task 处理异步任务
const fs = require('fs')
const { task } = require('folktale/concurrency/task')
const { split, find } = require('lodash/fp')

function readFile (url) {
    return task(resolver => {
        fs.readFile(url, 'utf-8', (err, data) => {
            if(err) resolver.reject(err)
            resolver.resolve(data)
        })
    })
}

readFile('package.json')
    .map(split('\n'))
    .map(find(x => x.includes('version')))
    .run()
    .listen({
        onRejected: err => {
            console.log(err)
        },
        onResolved: value => {
            console.log(value)
        }
    })

如果要获取文件中数据的 version 字段,可以直接在这个onResolved回调中处理数据,但是这样并不符合函数式编程。

Pointed 函子

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

1-2-2-函子_第1张图片

你可能感兴趣的:(#,Part,1,·,JavaScript,深度剖析)