【技术分享】函数式编程入门初探

2020-11-27

JavaScript作为一门“灵活的语言,在使用过程中有很多”骚操作“,但是带来的问题也是很多的。通过FP编程,让代码健壮。

函数式编程用来描述数据(函数)之间的映射关系。简单点来说就是把操作数据的过程用函数封装起来,就是函数式编程的思维。 FP编程特点:子任务,纯函数、函子、无状态。

部分手写代码地址: https://github.com/gzg1023/fackAchieve

使用FP思维开发的产物

//传统方式计算两数之和
let result1 = a + b
let result2 = c + d

// 函数式编程计算两数之和
function addFun(a,b){
   return a + b
}
let result3 = addFun(e,f)

函数式编程特点

1.函数是"一等公民”

  • 函数可以存储在变量中
  • 函数作为参数
  • 函数作为返回值

高阶函数

如:map, filter, forEach

实现见 https://github.com/gzg1023/fackAchieve

2.纯函数(数学上的函数)

相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用。

好处:

  • 可缓存,因为每次输入会输入一样的结果。
  • 可测试,纯函数可以让测试更方便
  • 并行处理,纯函数不需要共享变量(用在web worker)

副作用包括不限于:

  • 更改文件系统
  • 往数据库插入记录
  • 发送一个 http 请求
  • 可变数据
  • 打印/log
  • 获取用户输入
  • DOM 查询
  • 访问系统状态

3.柯里化

  1. 只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。
  2. 内部使用闭包缓存参数,让函数变的更灵活,函数的粒度更小
  3. 当一个函数有多个参数的时候先传递一部分参数调用它(这部分参数以后永远不变)当一个函数有多个参数的时候先传递一部分参数调用它(这部分参数以后永远不变)

然后返回一个新的函数接收剩余的参数,返回结果.

const _ = require('lodash') // 要柯里化的函数
function getSum (a, b, c) {
  return a + b + c
}
// 柯里化后的函数
let curried = _.curry(getSum) // 测试
console.log(curried(1, 2, 3)) 
console.log(curried(1)(2)(3))
console.log(curried(1, 2)(3))

4.函数组合

  1. 函数就像是数据的管道,函数组合就是把这些管道连接起来,让数据穿过多个管道形成最终结果
  2. 函数组合默认是从右到左执行
var loudLastUpper = compose(exclaim, toUpperCase, head, reverse)

loudLastUpper(['jumpkick', 'roundhouse', 'uppercut']);
//=> 'UPPERCUT!'

pointfree

概念:不使用所要处理的值,只合成运算过程

特点:函数无须提及将要操作的数据是什么样的,pointfree 模式能够帮助我们减少不必要的命名,让代码保持简洁和通用。

const fp = require('lodash/fp')
// pointfree
// 字符提取
const firstLetterToUpper = fp.flowRight(join('. '),

fp.map(fp.flowRight(fp.first, fp.toUpper)), split(' '))

console.log(firstLetterToUpper('world wild web')) // => W. W. W

5. debug

定义trace函数,然后插入到要调试到函数位置后面进行打印。

var trace = curry(function(tag, x){
  console.log(tag, x);
  return x;
});

6. Functor 函子

容器:包含值和值的变形关系(这个变形关系就是函数)

函子:是一个特殊的容器,通过一个普通的对象来实现,该对象具有 map 方法,map 方法可以运 行一个函数对值进行处理(变形关系)

其中实现了of方法的函子就是Pointed函子

// 函子容器
class Containser {
    constructor(value){
        this._value = value
    }
    static of (value){
        return new Containser(value)
    }
    map(fn){
        return Containser.of(fn(this._value))
    }
}
let a =  Containser.of(5).map(x=>x+1).map(x=>x*x)
console.log(a)

特性总结:

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

MayBe函子

进行异常处理的函子

class MayBe {
    constructor(value){
        this._value = value
    }
    static  of (value){
        return new MayBe(value)
    }
     map (fn) {
        return this.isNot() ? MayBe.of(null) : MayBe.of(fn(this._value)) 
    }
    isNot(){
        return this._value === undefined || this._value === null
    }

}
let a =  MayBe.of(null).map(x => x.toUpperCase())
let b =  MayBe.of('tset Maybe').map(x => x.toUpperCase())
console.log(a)
console.log(b)

Either函子

Either函子容器,可以进行异常处理的函子,Either定义两个子函数(可以定义多个,类似if/else)作为处理数据的基准, 如果正确进入右函子继续执行,如果报错,进入左函子打印出异常

class leftEither {
    constructor(value) {
        this._value = value
    }
    static of(value) {
        return new leftEither(value)
    }
    map(fn) {
        return this._value
    }
}
class rightEither {
    constructor(value) {
        this._value = value
    }
    static of(value) {
        return new rightEither(value)
    }
    map(fn) {
        return rightEither.of((this._value))
    }
}
function parseJSON(json) {
    try {
        return rightEither.of(JSON.parse(json));
    } catch (e) {
        return leftEither.of({ error: e.message });
    }
}

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

log(调试函子技巧)

通过一个中间函数,打打印日志

const _ = require('lodash')
const trace = _.curry((tag, v) => { console.log(tag, v)
return v
})
const split = _.curry((sep, str) => _.split(str, sep)) 
const join = _.curry((sep, array) => _.join(array, sep)) 
const map = _.curry((fn, array) => _.map(array, fn))
const f = _.flowRight(join('-'), trace('map 之后'), map(_.toLower), trace('split 之后'), split(' '))
console.log(f('NEVER SAY DIE'))

IO函子(惰性执行)

  • IO 函子中的 _value 是一个函数,在io函子中把函数作为值来处理
  • IO 函子可以把不纯的动作存储到 _value 中,延迟执行这个不纯的操作(惰性执行),包装当前的操作纯
  • 把不纯的操作交给调用者来处理
class IO {
    constructor(fn) {
        this._value = fn
    }

    static of(value) {
        return new IO(function () {
            return value
        })
    }

    map(fn) {
      return  new IO(_flowRight(fn, this._value))
    }
}
let obj = {
    msg: 'hello io'
}
let a = IO.of(obj).map(p => p.msg)
console.log(a._value())

monad(单子)函子

Monad 函子是可以变扁的 Pointed 函子,IO(IO(x)),一个函子如果具有 join 和 of 两个方法并遵守一些定律就是一个 Monad

const fs = require('fs')
const fp = require('lodash/fp') // IO Monad
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()
    }
}
let readFile = function (filename) { return new IO(function() {
    return fs.readFileSync(filename, 'utf-8') })
    }
    let print = function(x) { return new IO(function() {
        console.log(x)
    return x })
    }
let r = readFile('package.json').map(fp.toUpper)
    .flatMap(print)
    .join()