3

前言

上一篇文章介绍了javascript中的compose函数的实现,我是用了递归的思想去让函数依次执行,lodash中是用了迭代的思想依次执行函数,但实现了以后我还是觉得有些别扭,仔细想想,我们实现的是一个函数式编程用到的函数,但是实现的方法还是太命令式了,函数还是命令式的执行,通俗点说,还是太把函数当成函数了,在我的理解中,函数和普通变量没什么区别,只是执行的方法不一样,一旦赋予了函数这个执行的属性,我们就可以完全将函数当成普通变量去对待。

函数和普通变量没什么区别,只是需要偶尔执行一下

实现

1.函数世界的加号

举个例子

1 + 2 = 3
'a' + 'b' = 'ab'
func1 '+' func2 -> func3

前两个例子就是普通变量的操作,最后一个例子是函数的操作,本质上看来,没有任何区别,两个函数作用的结果就是生成一个函数,只不过在函数的世界里,这个加号的意义就是如何变换生成一个新的函数,回到compose来,在compose中,加号的意义就是把一个函数的执行结果当成下一个函数的输入,最后在生成一个函数,就像下面这样

var fn = (func1, func2) => (...args) => func2.call(this, func1.apply(this, args))

在这个例子里面,func1的执行结果就是func2的参数,并且生成了一个新的函数fn,我们给这个fn传递参数,它就会作为func1的参数来启动执行,最后得到了函数依次执行的效果,这就是最简单的compose,这个函数就是ramda.js实现compsoe需要的第一个函数_pipe

var _pipe = (f, g) => (...args) => g.call(this, f.apply(this, args))

_pipe就定义了compose中所谓加号的意义了。

2.'不一样的'reduce

在这里提到了reduce,是不是有一点感觉,reduce的作用就是让一个数组不断的执行下去,所以肯定能和咱们这个compose有点联系,先举个reduce最常用的例子,求数组的和

var a = [1,2,3,4,5]
a.reduce((x, y) => x + y, 0)

这个就是不断的将两个数求和,生成一个新的数,再去和下一个数求和,最后得到15,下面想一下,如果把数字换成函数会怎么样,两个函数结合生成一个新的函数,这个结合法则就使用上面的_pipe,这个新的函数再去结合下一个函数,直到最后一个函数执行完,我们得到的还是函数,我们前面说了,函数知识偶尔需要执行一下,这个函数的生成和执行过程是反向递归的过程。利用这个思想,就可以寥寥几行(甚至只需要一行)就写出来这个非常函数式的compose

var reverse = arr => arr.reverse()
var _pipe = (f, g) => (...args) => g.call(this, f.apply(this, args));
var compose = (...args) => reverse(args).reduce(_pipe, args.shift())

举个例子验证一下,我们把首个函数做多元处理,再upperCase,再repeat

var classyGreeting = (firstName, lastName) => "The name's " + lastName + ", " + firstName + " " + lastName
var toUpper = str => str.toUpperCase()
var repeat = str => str.repeat(2)
var result = compose(repeat, toUpper, classyGreeting)('dong', 'zhe')
// THE NAME'S ZHE, DONG ZHETHE NAME'S ZHE, DONG ZHE

我在这里把函数生成过程分析一下

首先我们用_pipe组合classyGreetingtoUpper

f1 = _pipe(classyGreeting, toUpper)
f1 = (...args) => toUpper.call(this, classyGreeting.apply(this, args))

_pipe继续结合f1, repeat

f2 = _pipe(f1, repeat)
f2 = (...args) => repeat.call(this, f1.apply(this, args))

函数的执行过程就会将参数层层传递到最里面的classyGreeting开始执行,从而完成函数的依次执行。ramda.js自己实现了reduce,不仅支持数组的reduce,还支持多种数据结构的reduce,(兼容性也更好?),下一步来分析是如何自己实现数组的reduce的,可与看出,自己分离出来逻辑之后,函数的执行过程和组合的规则部分将分离的更彻底。

3.自己写一个reduce

reduce接受三个参数,执行函数,初始值,执行队列(可以不止为一个数组),返回一个针对这些参数的reduce处理,这里只写数组部分(_arrayReduce),源码中还包含了关于迭代器的_iterableReduce 等等,而且ramda.js对执行函数也有一层对象封装,扩展了函数的功能

var reduce = (fn, acc, list) => (fn = _xwrap(fn), _arrayReduce(fn, acc, list))

在写_arrayReduce之前,先来看一下函数的对象封装_xwrap

var _xwrap = (function(){
    function XWrap(fn) {
        this.f = fn;
    }
    XWrap.prototype['@@transducer/init'] = function() {
        throw new Error('init not implemented on XWrap');
    };
    XWrap.prototype['@@transducer/result'] = function(acc) {
        return acc;
    };
    XWrap.prototype['@@transducer/step'] = function(acc, x) {
        return this.f(acc, x);
    };
    return function _xwrap(fn) { return new XWrap(fn); };
})()

其实就是对函数执行状态做了一个分类管理
@@transducer/step 这种状态认为是一种过程状态
@@transducer/result 这种状态被认为是一种结果状态
这种状态管理通过对象也是合情合理的
最后再来完成_arrayReduce,就很简单了,这个函数只是专心一件事情,就是写reduce的过程规则。

var _arrayReduce = (xf, acc, list) => {
    var idx = 0
    var len = list.length
    while (idx < len) {
        acc = xf['@@transducer/step'](acc, list[idx]);
        idx += 1;
    }
    return xf['@@transducer/result'](acc);
}

至此,ramda.js简化版的reduce就完成了。

4.其他一些功能

tail用来分离初始值和执行队列的,因为初始函数是多元的(接收多个参数),执行队列都是一元(接收一个参数)的,分离还是有必要的

var tail = arr => arr.slice(1)

reverse改变执行顺序

var reverse = arr => arr.reverse()  

_arity我把源代码贴出来,我也不知道为什么这样做,可能是明确指定参数吧,因为reduce生成的函数是可以接受多个参数的,_arity就是处理这个函数的

var _arity = (n, fn) => {
    switch (n) {
    case 0: return function() { return fn.apply(this, arguments); };
    case 1: return function(a0) { return fn.apply(this, arguments); };
    case 2: return function(a0, a1) { return fn.apply(this, arguments); };
    case 3: return function(a0, a1, a2) { return fn.apply(this, arguments); };
    case 4: return function(a0, a1, a2, a3) { return fn.apply(this, arguments); };
    case 5: return function(a0, a1, a2, a3, a4) { return fn.apply(this, arguments); };
    case 6: return function(a0, a1, a2, a3, a4, a5) { return fn.apply(this, arguments); };
    case 7: return function(a0, a1, a2, a3, a4, a5, a6) { return fn.apply(this, arguments); };
    case 8: return function(a0, a1, a2, a3, a4, a5, a6, a7) { return fn.apply(this, arguments); };
    case 9: return function(a0, a1, a2, a3, a4, a5, a6, a7, a8) { return fn.apply(this, arguments); };
    case 10: return function(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9) { return fn.apply(this, arguments); };
    default: throw new Error('First argument to _arity must be a non-negative integer no greater than ten');
  }
}

5.整合

最后整合出来两个最终的函数pipecompose

var pipe = (...args) => _arity(args[0].length, reduce(_pipe, args[0], tail(args)))
var remdaCompose = (...args) => pipe.apply(this, reverse(args))

再把上面的demo试一下

console.log(remdaCompose(repeat, toUpper, classyGreeting)('dong', 'zhe'))
// THE NAME'S ZHE, DONG ZHETHE NAME'S ZHE, DONG ZHE

整合的完全版我放到了github

总结

这篇文章主要分析了ramda.js实现compose的过程,其中分析了如何把函数看成一等公民,如何实现一个reduce等等。可以看出,compose的实现从头到尾都是函数式编程的思想,下一篇文章打算结合社区的一道问答题来介绍一下如何用函数式思想来解决问题。我也是初学函数式,有什么说的不准确的地方希望多多指正。


dongzhe3917875
1.5k 声望76 粉丝