函数式编程--函数组合(Function composition)

391 阅读7分钟

1、基本概念

函数组合(Function composition)

在计算机科学中,函数组合是将多个简单的函数,组合成一个更复杂的函数的行为或机制。每个函数的执行结果,作为参数传递给下一个函数,最后一个函数的执行结果就是整个函数的结果。

如下图,可以把函数的处理过程想象成一个管道,a表示输入值,b表示输出值,fn表示处理数据的管道。

bg2017031205.jpg

如果处理过程(fn)变的复杂,就可以把fn拆解成更小的单元(f1,f1,3)。a成为f1的输入值;f1的输出值m,成为f2的输入值;f2的输出值n,成为f3的输入值,最终得到输出值b,也即整个过程的输出值。

bg2017031206.jpg

结合律(associativity)

在数学中,意指在一个包含有二个以上的可结合运算子的表示式,只要算子的位置没有改变,其运算的顺序就不会对运算出来的值有影响。

加法和乘法都是可结合的:

(x * y) * z === x * (y * z)
(x + y) + z === x + (y + z)

函数组合遵循结合律,意指只要函数的位置没有发生改变,任意组合优先执行,都不影响执行结果。

compose(f,g,h) == compose(compose(f, g), h) == compose(f, compose(g, h))

2、目标用途

  • 避免洋葱嵌套代码,提高代码的可读性

开发中,我们经常会用到这样的函数,比如f(g(h()))。举一个简单例子,假设我们要做一个算术运算,求((3 + 5) * 2)的平方。实际需要用到三个函数:


// 加法运算
const add = (a,b) => a+b
const double = (x) => x * 2
const square = (n) => Math.pow(n,2)

square(double(add(3,5))) // 256

这种洋葱式的语法结构,可读性极差。如果,嵌套关系更多,单凭肉眼几乎很难辨别和区分出调用关系。这类场景,使用函数组合,语法将变得简单清晰。


const cmp = compose(square, double, multi)

const result = cmp(3,5) // 256

  • 组合多个单一功能函数生成特定功能的新函数

单一性原则,有助于提高函数的适用性和封闭性。但是,面对复杂功能,方便的组合使用单一功能函数却是个问题。函数组合可以把功能单一的函数,重新组合成功能相对更加丰富完备、通用性更好的新函数,方便使用和复用。

举一个简单的例子,我们经常需要对输入内容进行格式处理。比如,金额。需要经历去除收尾空格、长度截取限制、千分位格式化等处理过程。

const trim = (str) => String.prototype.trim.call(str)
const limit = (str) => String.prototype.substr.call(str,0,11)
const numberic = (str) => String(str).replace(/(\d{1,3})(?=(\d{3})+(?:$|\.))/g, "$1,")

const format = compose(numberic,limit,trim)

format(10000) // 10,000
format(123000000) // 123,000,000
format(' 123456789000000 ') // 12,345,678,900
  • 把功能复杂的函数拆解成功能相对单一的函数,便于维护和复用

和上面相反,可以把多个单一功能的函数,重新整合成功能复杂的函数;反之,也可以把功能复杂的函数,拆解成多个单一功能的函数,可单独使用,也可以compose重新组合使用;这样做,方便我们维护和重复使用。

3、实现原理

下面一起分析一下compose的实现

首先,compose是一个高阶函数

  • 需要一到多个函数作为参数
  • 其返回值也是一个函数,用于接受参数
function compose(...funcs){
    return function(){
        // ...
    }
}

其次,函数组合(Function composition),从右向左依次执行,并且每个函数的执行结果,作为参数传递给下一个参数,直至所有函数执行完毕。这种累积计算结果的运算,非常符合Array.prototype.reduce函数的应用场景。唯一的区别是reduce的运算顺序从左向右执行。可以借助reverse方法反转顺序,或者直接使用reduceRight方法。

借助reverse反转顺序

function compose(...funcs){
    return function(...args){
        const start = funcs.pop()
        return funcs.reverse().reduce((result, next)=>next.call(this,result), start.apply(this,args))
    }
}

使用reduceRight

function compose(...funcs){
    return function(...args){
        const start = funcs.shift()
        return funcs.reduceRight((result, next)=>next.call(this,result), start.apply(this,args))
    }
}

最后,需要完善compose的参数验证。参数长度为0,说明没有待组合的函数,返回一个返回值即是参数本身的空函数;如果只有一个待组合的函数,直接返回;完整的compose函数如下所示。


function compose(...funcs) {
    let len = funcs.length
    if (len === 0) {
        return (...args) => args
    }

    if (len === 1) {
        return (...args) => funcs[0].apply(this, args)
    }

    return function (...args) {
        const start = funcs.pop()
        return funcs.reverse().reduce((result, next) => next.call(this,result), start.apply(this,args))
    }
}

当然还可以对参数funcs类型加以验证

function compose(...funcs) {
    let len = funcs.length
    for (const fn of funcs) {
        if (typeof fn !== 'function') throw new TypeError('Params must be composed of functions!')
    }

    if (len === 0) {
        return (...args) => args
    }

    if (len === 1) {
        return (...args) => funcs[0].apply(this, args)
    }

    return function (...args) {
        const start = funcs.pop()
        return funcs.reverse().reduce((result, next) => next.call(this,result), start.apply(this,args))
    }
}

实现方式还有很多,下面给出另外几种实现方式,有兴趣可以研究一下。

基于for循环的实现思路比较简单。循环执行,并把执行结果,作为参数向下传递,直至循环结束。

function compose(...funcs) {
    let len = funcs.length
    if(len===0){
        return (...args) => args
    }

    if (len === 1) {
        return (...args) => funcs[0].apply(this, args)
    }

    return function(...input) {
        let start = funcs.pop()
        let result = start.call(this, input)
        for (let fn of funcs.reverse()){
            result = fn.apply(this, result)
        }

        return result
    }
}

下面是redux中compose的实现思路。没有使用reverse,或者reduceRight,处理执行顺序问题。而是利用匿名函数,从外到里,层层封装缓存执行;执行时,顺序变成了从里到外,巧妙的处理了执行顺序。


function compose(...funcs) {
    if (funcs.length === 0) {
        return arg => arg
    }

    if (funcs.length === 1) {
        return funcs[0]
    }

    return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

增强版的compose

开发过程中,我们会经常用到异步编程。Promise、ajax请求、async...await、setTimeout等诸多异步编程方式,给前端开发带来了极大的便利。下面我们借助async...await,在原来到基础上改造增强compose函数,使其支持异步函数。

reduce版本的异步compose

需要注意async...await的用法问题


function asyncCompose(...funcs) {
    let len = funcs.length
    if (len === 0) {
        return (...args) => args
    }

    if (len === 1) {
        return (...args) => funcs[0].apply(this, args)
    }

    const start = funcs.pop()
    return async function (...args) {
        return await funcs.reverse().reduce(async (result, next) => next.call(this, await result), await start.apply(this, args))
    }
}

for...of版本的异步compose

function asyncCompose(...funcs) {
    let len = funcs.length
    if (len === 0) {
        return (...args) => args
    }

    if (len === 1) {
        return funcs[0]
    }

    return async function (...input) {
        let start = funcs.pop()
        let result = await start(...input)

        for (let fn of funcs.reverse()) {
            result = await fn(result)
        }

        return result
    }
}

实例验证:

let str = ' 123456789000000 '

const sleep = (fn, time) => new Promise((resolve) => {
    setTimeout(() => resolve(fn()), time)
})

const trim = (str) => String.prototype.trim.call(str)
const limit = (str) => String.prototype.substr.call(str, 0, 11)
const numberic = (str) => String(str).replace(/(\d{1,3})(?=(\d{3})+(?:$|\.))/g, "$1,")

const asyncTrim = (str) => sleep(() => trim(str), 1000)
const asyncLimit = (str) => sleep(() => limit(str), 1000)

console.log(compose(numberic, limit, trim)(str));


(async () => console.log(await asyncCompose(numberic, asyncLimit, asyncTrim)(str)))()

管道(Pipeline)

函数式编程中,还有一个跟函数组合(Function Composition)非常类似的概念,叫管道(Pipeline)。我们可以简单把它理解为从左向右的compose。其实现原来和应用跟compose也基本相似。

举一个简单的例子,lodash库中pipeline对应flow方法,compoe对应flowRight。一起看一下它的源代码。


function flow(...funcs) {
    const length = funcs.length
    let index = length
    while (index--) {
        if (typeof funcs[index] !== 'function') {
        throw new TypeError('Expected a function')
        }
    }
    return function(...args) {
        let index = 0
        let result = length ? funcs[index].apply(this, args) : args[0]
        while (++index < length) {
        result = funcs[index].call(this, result)
        }
        return result
    }
}

function flowRight(...funcs) {
    return flow(...funcs.reverse())
}

4、关于Pointfree的一点讨论

关于Pointfree的概念,讨论有很多,甚至中文翻译都没有准确的定论。

简单说:

Pointfree就是把数据处理的过程,定义成一种与参数无关的合成运算。不需要用到代表数据的那个参数,只要把一些简单的运算步骤合成在一起即可。

如何理解这句话?我个人觉得从函数式编程的本质去理解,更加清晰。函数式编程,简单笼统的说就是处理函数的过程。把已有的函数处理过程,处理成更适用,更容易维护,更自动化的函数。柯理化(Currying)和偏函数(Partial Application)是针对参数的处理;函数组合(Composition)和管道(Pipeline)是针对函数的组合和拆分的处理;这两种针对函数的编程都有一个共同的特点:只关注函数的处理并生成新的函数,并未显式的声明参数。

举个简单的例子,这里通过compose组合numberic,limit,trim生成format函数。有关参数的信息是隐式的包含在函数的处理过程当中。format接受参数,会传导给底层函数去处理,并得到最终的返回值。

const format = compose(numberic,limit,trim)

5、参考资料

www.codementor.io/@michelre/u…

en.wikipedia.org/wiki/Functi…

github.com/mqyqingfeng…

github.com/sindresorhu…

segmentfault.com/a/119000001…

www.ruanyifeng.com/blog/2017/0…