函数式编程范式(4)- 函数组合

函数组合

纯函数和柯里化很容易写出洋葱代码 h(g(f(x)))

如“获取数组的最后一个元素再转换成大写字母” _toUpper(_.first(_.reverse(array)))

函数组合可以让我们把细粒度的函数重新组合生成一个新的函数

管道

我们可以将使用函数处理数据想象成一个管道。

image.png

当 fn 函数比较复杂的时候,我们可以把 fn 函数拆分成多个小函数

image.png

fn = compose(f1, f2, f3)
b = fn(a)

函数组合

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

函数组合默认是从右到左执行

// 函数组合演示
function compose (f, g) {
    return function (value) {
    return f(g(value))
  }
}
  
function first (arr) {
  return arr[0]
}

function reverse (arr) {
  return arr.reverse()
}

// 从右到左运行
let last = compose(first, reverse)
console.log(last([1, 2, 3, 4]))

洋葱代码并没有减少,只是封装起来了。

Lodash 中的组合函数

我们自己实现的 compose 只能组合两个函数,如果需要组合更多函数,我们可以使用 Lodash 中提供的组合函数 flow() 或 flowRight。

flow() 是从左到右,flowRight() 是从右到左运行,使用的更多一些。

// lodash 中的函数组合的方法 _.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']))

原理模拟

模拟 lodash 中的 flowRight 方法,对一个数组进行操作。

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

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

function compose (...args) {
  // 再次调用需要传递一个数组,即 value
  return function (value) {
    // 最终需要返回值
    // 先使用 reverse 方法反转数组
    // 核心:使用数组 reduce 方法对数组进行“累计运算”
    return args.reverse().reduce(function (acc, fn) {
      // acc 累计器
      // fn 当前项,这里是一个函数
      return fn(acc)
    }, value)
  }
}

使用 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']))

结合律

函数的组合要满足结合律(associativity),这里的结合律即数学意义的结合律。

我们既可以把 g 和 h 组合,还可以把 f 和 g 组合,结果都是一样的。

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

实际例子

// 函数组合要满足结合律
const _ = require('lodash')

// const f = _.flowRight(_.toUpper, _.first, _.reverse)
const f = _.flowRight(_.flowRight(_.toUpper, _.first), _.reverse)

console.log(f(['one', 'two', 'three']))
// 函数组合要满足结合律
const _ = require('lodash')

// const f = _.flowRight(_.toUpper, _.first, _.reverse)
const f = _.flowRight(_.toUpper, _.flowRight(_.first, _.reverse))

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

根据这两个不同的组合结果可知,不论是组合 _.toUpper_.first 还是组合 _.first_.reverse,结果都不会改变,均为 TREE

如何调试

当我们使用函数组合,结果与预期不一致时,我们应该如何调试呢?

一个例子,我们将 NEVER SAY DIE 转换为 never-say-die,我们先准备以下函数:

const _ = require('lodash')

// Lodash split 方法第一个参数为 string,第二个参数为 separator
// 由于 split 方法有多个参数,在函数组合中我们无法直接使用 Lodash split 方法
// 我们需要将多个参数转换为一个参数的函数,即函数柯里化
// 并且当把函数组合完毕后,最终调用函数的时候,才会传入所需要的字符串,因此字符串参数应该在最后的位置
// _.split()
const split = .curry((sep, str) => _.split(str, sep))

// _toLower()

// Lodash join 方法与 split 方法类似,我们也需要重新写一个函数
// _.join()
const join = _.curry((sep, array) => _.join(array, sep))
我们已经对 split 函数进行了柯里化,因此当调用 split 的时候,可以只传部分参数。
const f = _.flowRight( join('-'), _.toLower, split(' '))
                      
console.log(f('NEVER SAY DIE'))
// 结果: n-e-v-e-r-,-s-a-y-,-d-i-e

实际结果与我们的预期结果不符,我们需要调试看是哪部分函数出现了问题。

创建一个 log 函数。

const log = v => {
  console.log(v)
  return v
}

// ...

const f = _.flowRight(join('-'), _.toLower, log, split(' '))

// ...

在组合函数中的参数之间中插入 log 函数从右到左依次进行排查。

经过排查,我们发现是由于 toLower 函数将 [ 'NEVER', 'SAY', 'DIE' ] 转换成了小写字符串 never,say,die,而 join 函数需要的是数组。我们可以在 toLower 函数外包裹一层 map 函数。

// ...
// 仍然需要柯里化
const map = _.curry((fn, array) => _.map(array, fn))

// ...

const f = _.flowRight(join('-'), map(_.toLower), split(' '))

优化 log 函数

当函数组合存在多个 log 函数时,难以定位函数所在位置。

我们可以使用 trace 额外传入 tag 帮助我们获取额外信息。

const trace = _.curry((tag, v) => {
  console.log(tag, v)
  return v
})

// ...
const f = _.flowRight(join('-'), trace('map 之后'), map(_.toLower), trace('split 之后'), split(' '))

Lodash 的 fp 模块

我们在使用 Lodash 中的方法时,由于不符合函数式编程,我们往往需要再进行柯里化改造,其实 Lodash 的 fp 模块直接提供了函数式方法。

lodash/fp 模块特点

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

lodash 模块中的方法是数据优先,函数之后。

// lodash 模块
const _ = require('lodash')
_.map(['a', 'b', 'c'], _.toUpper)
// => ['A', 'B', 'C']
_.map(['a', 'b', 'c'])
// => ['a', 'b', 'c']
_.split('Hello World', ' ')

lodash/fp 模块的方法是函数优先,数据之后,且已经柯里化。

// lodash/fp 模块
const fp = require('lodash/fp')
fp.map(fp.toUpper, ['a', 'b', 'c'])
fp.map(fp.toUpper)(['a', 'b', 'c'])
fp.split(' ', 'Hello World')
fp.split(' ')('Hello World')

案例

const fp = require('lodash/fp')

const f = fp.flowRight(fp.join('-'), fp.map(fp.toLower), fp.split('_'))

console.log(f('NEVER SAY DIE'))

Lodash map 方法的小问题

const _ = require('lodash')

console.log(_.map(['23', '8', '10'], parseInt))
// output: [ 23, NaN, 2 ]

为什么这里和预期结果不符?因为 _.map 会传递三个参数 value, index|key, collection,parseInt 会接收这三个参数。

parseInt('23', 0, array)
parseInt('8', 1, array)
parseInt('10', 2, array)

而 parseInt 第二个参数是进制。如果该参数小于 2 或者大于 36,则 parseInt() 将返回 NaN。

我们可以使用 lodash/fp 模块中的 map 方法。

const fp = require('lodash/fp')
console.log(fp.map(parseInt, ['23', '8', '10']))

Pointfree

一种编程风格

特点

  • 不需要指明处理的数据
  • 只需要合成运算过程
  • 需要定义一些辅助的基本运算函数

Pointfree 模式的实现方法实际就是函数组合。

 const f = fp.flowRight(fp.join('-'), fp.map(_.toLower), fp.split(' '))

案例一

// 非 Point Free 模式
// Hello World => hello_world
function f (word) {
  return word.toLowerCase().replace(/\s+/g, '_');
}
// Point Free
const fp = require('lodash/fp')
const f = fp.flowRight(fp.replace(/\s+/g, '_'), fp.toLower)
console.log(f('Hello World'))

案例二

// 把一个字符串中的首字母提取并转换成大写,使用,作为分隔符
// world wild web ===> W. W. W
const fp = require('lodash/fp')

// const firstLetterToUpper = fp.flowRight(fp.join('. '), fp.map(fp.first), fp.map(fp.toUpper), fp.split(' '))
console.log(firstLetterToUpper('world wild web'))

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值