函数组合

  • 纯函数和了柯里化很容易写出洋葱代码 h(g(e(x)))
  • 函数组合可以让我们把细粒度的函数重新组合生成一个新的函数
  • 函数组合并没有减少洋葱代码,只是封装了洋葱代码
  • 函数组合执行顺序从右到左
  • 满足结合律既可以把g和h组合 还可以把f和g组合,结果都是一样的

数据的管道

如果一个函数经过多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并成一个函数

  • 函数就像是数据的通道,函数组合就是把这些管道链接起来,让数据传过多个管道行程最终结果
  • 函数组合默认从右到左执行

下面就是数据处理的过程,给fn参数a,返回结果b可以想象a 数据通过一个管道得到了b数据

a=====>fn=========>b

可以把fn管道拆分成多个小管道,这样发生问题可以很快的排查到哪里出了问题

a=====>fn(fn1=====>fn2====>fn3)=====>b

函数组合示例

//函数组合示例
// 组合
function compose (f, g) {
  return function (value) {
    // 洋葱代码并没有减少只是被封装起来了
    return f(g(value))
  }
}

// 获取数组最后一个元素
function reverse (array) {
  return array.reverse()
}

function first (array) {
  return array[0]
}

const last = compose(first, reverse)

console.log(last([1, 2, 3, 4]))

获取元素的最后一个参数可以拆分为两个管道,一个管道翻转数据,第二个管道获取元素的第一个元素,这两个函数可以单独使用,也可以组合起来成为更强大的函数。上面的例子只是一个很简单的操作,所以看起来好像并不便利,当项目中很多方法组合的时候就能展显示出了

lodash中的函数组合

  • flow 是从左右到执行
  • flowRight是从右到左运行,使用的更多一些
const _ = require('lodash')

const reverse = arr => arr.reverse()
const first = arr => arr[0]
const toUpper = s => s.toUpperCase()

// 做右到左执行
const f = _.flowRight(toUpper, first, reverse)

console.log(f(['one', 'two', 'three']))

组合函数原理模拟

const _ = require('lodash')

const reverse = arr => arr.reverse()
const first = arr => arr[0]
const toUpper = s => s.toUpperCase()

// 做右到左执行
function compose (...args) {
  return function (value) {
    // 从右到左执行所以需要翻转Ï
    return args.reverse().reduce(function (acc, fn) {
      return fn(acc)
    }, value)
  }
}

const f = compose(toUpper, first, reverse)
console.log(f(['one', 'two', 'three']))

es6 方式书写

const compose = (...args) => value =>
  args.reverse().reduce((acc, fn) => fn(acc), value)

const f = compose(toUpper, first, reverse)
console.log(f(['one', 'two', 'three']))

函数组合结合律

函数组合要满足结合律,我们可以先把fg组合,还可以把gh组合,结果都是一样的

// 结合律
let f = compose(f, g, h)
let asscociative = compose(compose(f, g), h) == compose(f, compose(g, h))

是数学中的结合律,不是说把 执行顺序颠倒打乱

const _ = require('lodash')

// 前面两个组合与后面两个组合结果一致
const f = _.flowRight(_.flowRight(_.toUpper, _.first), _.reverse)
const f = _.flowRight(_.toUpper,_.flowRight( _.first, _.reverse))

console.log(f(['one', 'two', 'three']))

调试结合律

通过写一个 把 AAA BBB CCC ====> aaa-bbb-ccc 的例子来看怎么调试组合函数

// 调试函数组合
// AAA BBB CCC ====> aaa-bbb-ccc

const _ = require('lodash')

// _.split 多个参数所以需要柯里化
const split = _.curry((sep, str) => _.split(str, sep))

//
// _.toLower

// 多个参数所以需要柯里化
const join = _.curry((sep, array) => _.join(array, sep))

// 错误写法
const f = _.flowRight(
  join('-'),
  _.toLower,
  split(' ')
)
// a-a-a-,-b-b-b-,-c-c-c

上面输出的结果显然是不是我们想要的结果,要怎么来追溯哪里出了问题呢,通过函数结合律我们可以在操作后插入一个打印函数来查看数据是否是期望的状态

// 为什么要两个参数,因为多次打印的时候不知道是什么地方打印的数据,所以需要一个tag 来区分步骤
const trace = _.curry((tag, v) => {
  console.log(tag, v)
  return v
})

// 错误写法
const f = _.flowRight(
  join('-'),
  trace('toLower'),
  _.toLower,
  trace('split'),
  split(' ')
)

lodash 中的 FP 模块

lodashfp 模块提供了实用的对函数式编程友好的方法,提供了不可变的auto-curried iteratee-first data-last 的方法

已经是柯里化的,如果一个方法的参数是函数的话,函数优先,数据在后

fp.map(fp.toUpper, ['1', '2', '3'])
fp.map(fp.toUpper)(['1', '2', '3'])

例如map方法,先传入处理函数,在传数据, 用fp模块中的方法来处理AAA BBB CCC ====> aaa-bbb-ccc

const fp = require('lodash/fp')
const f = fp.flowRight(fp.toLower, fp.join('-'), fp.split(' '))
console.log(f('AAA-BBB-CCC'))

很简单就完成了AAA BBB CCC ====> aaa-bbb-ccc 操作

lodash中map方法存在的问题

lodashlodash/fp 里面的map方法参数有一定的差距,参数顺序一个是数据在前,一个数据在后、回调函数的参数也不一致。lodash的map方法的回调函数有三个参数,例如下面 字符串转化为数字的时候后就会出现问题parseInt第二个参数是转化进制所以结果不是取整后的数据

const _ = require('lodash')
console.log(_.map(['10', '20', '30'], parseInt))
// parseInt('10', 0, arr)
// parseInt('20', 1, arr) 不支持
// parseInt('30', 2, arr) 转二进制
// map 回调函数的参数是 (value, index|key, collection)

lodash/fp中的map 回调参数就只有一个参数,就不会有以上问题

const fp = require('lodash/fp')

console.log(fp.map(parseInt, ['10', '20', '30']))