阅读 66

JavaScript中的函数式编程--函数组合

函数组合

问题:纯函数和柯里化很容易写出洋葱(一层套一层)代码,形如:h(g(f(x))),实际中遇到的问题如:获取数组的最后一个元素在转换成大写字母,_.toUpper(_.first(_.reverse(array))) 解决:函数组合可以让我们把细粒度的函数重新组合生成一个新的函数

管道

下面这张图标识程序中使用函数处理数据的过程,给fn函数输入参数a,返回结果b,可以想象a通过一个管道得到了b数据。

avatar
思考:如果中间的管道(fn)特别长(复杂)时我们可不可以将其拆分成多个短管道(小函数)? 下面这张图中可以想象成把fn这个管道拆分成了3个管道f1、f2、f3,数据a通过管道f3得到结果m,m在通过管道f2得到结果n,n在通过管道f1得到最终结果b
avatar
使用函数展示:

    fn = compose(f1, f2, f3)
    b = fn(a)
复制代码

函数组合的概念

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

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

函数组合示例:

    function compose(f, g) {
        // 组合完成后返回一个函数,此函数接收一个参数
        return function(value) {
            // 依次从右向左执行函数
            return f(g(value))
        }
    }
    // 使用:获取数组中的最后一个元素(使用反转+取第一个元素)
    function reverse(array) {
        return array.reverse()
    }
    function first(array) {
        return array[0]
    }
    // 使用compose
    const last = compose(first, reverse)
    console.log(last([1, 2, 3, 4, 5 ,6]))
复制代码

Lodash中的组合函数

  • lodash中的组合函数
    • lodash中的组合函数flow()或者flowRight(),他们都可以组合多个函数
    • flow()是从左到右运行
    • flowRight()是从右到左运行,使用的更多一些

lodash示例:

    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', 'tow', 'three'])
复制代码

组合函数的实现原理

分析:调用组合函数会一次执行传入的函数,每次执行完成一个函数后会将结果交给下一个要执行的函数

    // 传入参数个数不确定,所以使用ES6中展开剩余参数(...args)
    function compose(...args) {
        // 执行后返回一个函数
        return function (value) { // 需要接收一个参数
            // 执行完成后需要返回结果
            // 实现传入函数的从右往左执行,使用reverse将args反转
            // 接下来需要执行args中的函数并将结果给后续使用,reduce正好满足需求
            return args.reverse().reduce((acc, fn) => fn(acc), value)
        }
    }
    
    // ES6简化
    const compose = (...args) => value => args.reverse().reduce((acc, fn) => fn(acc), value)
    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', 'tow', 'three']))
复制代码

函数组合-结合律

函数的组合要满足结合律(associativity)

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

示例:

    // 结合律
    let f = compose(f, g, h)
    let associative = compose(compose(f, g), h) == compose(f, compose(g, h))
    // => true
复制代码

函数组合-调试

如何调试组合函数?

  • 首先组合的函数都是纯函数,之前我们说过纯函数有点重有一条是便于调试,那我们便可以使用这一个特点

示例:按照指定格式转换字符串

    // 输入字符 'ABC DE FGHI'
    // 输出字符 'abc-de-fghi'
    // 分析:1.首先需要将字符按照空格分隔:split
    // 分析:2.将字符转成小写:toLower
    // 分析:3.用-分隔字符:join
    const _ = require('lodash')
    // split(str, sep) 使用柯里化将其转化为一元参数
    const split = _.curry((sep, str) => _.split(str, sep))
    // toLower(str) 单参数纯函数,不需要处理,直接使用 _.toLower 即可
    // join(array, sep)
    const join = _.curry((sep, array) => _.join(array, sep))
    // 当我们使用split分割以后得到一个数组,此时不能直接使用toLower
    // 此时我们遍历数组再使用toLower处理,需要一个map函数
    const map = _.curry((fn, array) => _.map(array, fn))
    // 进行函数组合
    const f = _.flowRight(join('-'), map(_.toLower), split(' '))
    // 以上是我们为调试做的准备,那么如果我们想在执行组合中某个函数后得到结果该怎么办
    // 首先我们函数组合中每执行完一个函数都会将结果返回
    // 那我们根据这个特点是不是可以在该函数后插入一个log函数呢?
    const log = v => {
        console.log(v)
        // 需要将结果返回供之后函数使用
        return v
    }
    const f = _.flowRight(join('-'), log, map(_.toLower), log, split(' '))
    // 此时log可输出,但是我们无法清除区分哪个输出对应哪个log,改造下
    const trace = _.curry((tag, v) => {
        console.log(tag, v)
        return v
    })
    const f = _.flowRight(join('-'), trace('map之后'), map(_.toLower), trace('split之后'), split(' '))
    console.log(f('ABC DE FGHI'))
复制代码

Lodash中的FP(Function Programming)模块

lodash/fp

  • lodash的fp模块提供了实用的对函数式编程友好的方法
  • 提供了不可变**auto-curried(已被柯里化) iterates-first(函数优先) data-last(数据滞后)**的方法

示例:lodash中的普通模块与fp模块

    // 普通模块
    // 未被柯里化的函数:数据优先,函数滞后
    const _ = require('lodash')
    _.map(['a', 'b', 'c'], _.toUpper)
    // => ['A', 'B', 'C']
    _.map(['a', 'b', 'c'])
    // => ['a', 'b', 'c']
    _.split('Hello World', ' ')
    // => ['Hello', 'World']
    
    // lodash/fp模块
    // 柯里化的函数:函数优先,数据滞后
    cnost 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')
复制代码

示例:使用fp模块重写调试中的例子

    const fp = require('lodash/fp')
    const f = fp.flowRight(fp.join('-'), fp.map(fp.toLower), fp.split(' '))
    console.log(f('ABC DE FGHI'))
复制代码

Point Free

Point Free:我们可以把数据处理的过程定义成与数据无关的合成运算,不需要用到代表数据的哪个参数,只要把简单的运算步骤合成到一起,在使用这种模式之前我们需要定义一些辅助的基本运算函数

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

解释示例:组合函数

    // 1.没有指明处理的数据
    // 2.通过组合函数组合多个运算函数
    // 3.定义了join、map、toLower、split运算函数
    const f = fp.flowRight(fp.join('-'), fp.map(_.toLower), fp.split(' '))
复制代码

示例:字符串转换

    // 将空格替换为_,大写转换成小写
    // Hello     World => hello_world
    const fp = require('lodash/fp')
    // 分析:手写大写转小写,然后正则匹配_替换空格
    const f = fp.flowRight(fp.replace(/\s+/g, '_'), fp.toLower)
    console.log(f('Hello    World'))
复制代码

通过以上示例我们可以看出Point Free其实就是函数组合

Point Free案例

把一个字符串中的首字母提取并转换成大写,使用. 作为分隔符得到新的字符串

    // 输入: world wide web
    // 输出:W. W. W
    // 分析:1.将字符串按照空格分割
    // 分析:2.遍历数组将字母全部转换为大写
    // 分析:3.遍历数组取转换后单次的首字母
    // 分析:4.使用. 分割获取新字符串
    const fp = require('lodash/fp')
    const f = fp.flowRight(fp.join('. '), fp.map(fp.first), fp.map(fp.toUpper), fp.split(' '))
    console.log(f('world wide web'))
    // 通过以上方式我们实现了要求
    // 但是我们发现组合中使用了两次map遍历,对性能有一定影像,来优化一下
    // 两次map遍历都是对同一个数组操作,所以可以合并一下
    const f = fp.flowRight(fp.join('. '), fp.map(fp.flowRight(fp.first, fp.toUpper)), fp.split(' '))
复制代码