第2章 函数组合

组合函数就像堆乐高积木一样,将不同的组件按照不同的方式拼成一个有用的组件。

一.输出到输入

上一章我们已经看到过一些组合函数,比如 unary(adder(3)), 这将2个函数组合到一起,其数据流如下:

functionValue <-- unary <-- adder <-- 3

3adder(..) 的输入值, adder(..)的输出值 又是 unary(..) 的输入值, unary(..)的输出值为 functionValue, 这叫做 unary(..)adder(..) 的组合。 这种组合就像传送带一样,比如巧克力的生产: 冷却 --> 切割 --> 包装。

1.命令式函数示例

function words(str) {
    return String(str)
            .toLowerCase()
            .split(/\s|\b/) // 按照空格或单词边界拆分
            .filter(v => /^[\w]+$/.test(v)) //按单词过滤    
}

function unique(list) {
    var uniqList = [];
    for (let i = 0; i < list.length; i++) {
        if (uniqList.indexOf(list[i]) === -1) {
            uniqList.push(list[i]);
        }
    }
    return uniqList;
}

var text = "To compose two functions together, pass the \
output of the first function call as the input of the \
second function call.";     

var wordsFound = words( text );
var wordsUsed = unique( wordsFound );

wordsUsed;
// ["to","compose","two","functions","together","pass",
// "the","output","of","first","function","call","as",
// "input","second"]

我们将 words(..) 的输出值命名为 wordsFound,同时作为 unique的输入值。

我们可以将中间过程wordsFound省略掉,直接写为:

var wordsUsed = unique( words(text) )

这样变为一个组合函数,注意传统的函数 left-to-right, 但是组合函数一般都是 right-to-left 或者 inner-to-outer.

我们可以将上面的函数进一步的进行封装:

function uniqueWords(str) {
    return unique( words(str) );
}
// 这样就可以不再写
var wordsUsed = unique( words(text) )
// 而是直接写为
var wordsUsed = uniqueWords(text);

2.Machine Making

上面的例子就像我们巧克力厂设备组合在一起,去掉了中间过程(冷却,切割,包装),直接输入一个值就输出一个值。下面我们进一步的"生产"一种设备,这个设备能够直接制造出向uniqueWords这样的设备。

function compose2(fn2, fn1) {
    return function composed(oriValue) {
        return fn2( fn1(oriValue) );
    }
};
// ES6写法
const compose2 =
    (fn2, fn1) =>
        oriValue =>
            fn2( fn1(oriValue) );

下面我们"生产" uniqueWords 这种设备:

var uniqueWords = compose2(unique, words);
var wordsUsed = uniqueWords(text);

3.组合变形

<-- unique <-- words 这种组合似乎是这两个函数唯一组合顺序,但其实我们可以像乐高积木一样改变2个组件的组合方式来改变其功能。

var letters = compose2(words, unique);
var chars = letters("How are you Henry?");
chars;
// ["h","o","w","a","r","e","y","u","n"]

这个例子虽然比较巧合,但是这里是为了说明函数的组合不是一直是单向的

二.通用组合

如果我们可以将2个函数组合在一起,我们可以将任意多个函数组合在一起,通用数据流如下:

finalValue <-- func1 <-- func2 <-- ... <-- funcN <-- oriValue

1.通用实现

function compose(...fns) {
    return function composed(result) {
        // 赋值上面的函数数组
        var lists = fns.slice();

        while (list.length > 0) {
            // 将最后一个函数从数组中取出, pop()
            // 并且执行它
            result = list.pop()(result);
        }
        
        return result;
    };
}

// ES6
const compose =
        (...fns) =>
            result => {
                var lists = fns.slice();
                while (lists.length > 0) {
                    result = lists.pop()(result);
                }
                return result;
            };

2.示例

比如我们在 unique(words(text)) 的基础上加一层过滤条件, 对单词长度小于4的进行过滤,即 skipShortWords(unique(words(text)))

function skipShortWords(list) {
    var filteredList = [];
    for (let i = 0; i < list.length; i++) {
        if (list[i].length > 4) {
            filteredList.push(list[i]);
        }
    }
    return filteredList;
}

下面使用compose方法:

var biggerWords = compose(skipShortWords, unique, words);
var wordsUsed = biggerWords(text);
wordsUsed;
// ["compose","functions","together","output","first",
// "function","input","second"]

3.结合partialRight()

使用 partialRight() 我们可以做一些更有意思的事,比如先指定后面的2个参数 unique(..)words(..)

// skipShortWords中使用 "<= 4" 代替 "> 4"
// 改名为skipLongWords
function skipLongWords(list) { /* ... */}

// 先指定后面的2个参数
function filterWords = partialRight(compose, unique, words);

var biggerWords = filterWords( skipShortWords );
// ["compose","functions","together","output","first",
// "function","input","second"]

var smallerWords = filterWords( skipLongWords );
// ["to","two","pass","the","of","call","as"]

partialRight让我们能够指定compose后面的参数,后续步骤(或逻辑)可以根据自己的需求进行书写。

同样我们可以使用 curry 对组合函数进行控制,比如:

// 一般我们先将compose的参数反过来,这样就可以
// fn1 --> fn2--> ...
curry( reverseArgs(compose), 3)(words)(unique)(..)

三.使用Reduce实现通用组合

我们可能永远不会在项目中自己实现compose(..), 用么使用一些工具库帮助我们实现, 但是理解它的原理能够有效的帮助我们更好的函数编程。

1.使用reduce

这种实现方式更加的简洁,使用函数编程中常用的 reduce 函数

function compose(...fns) {
    return function composed(result) {
        // 此时fns.reverse()数组为
        // [fn1, fn2, ..., fnn]
        return fns.reverse().reduce(function reducer(result, fn) {
            return fn(result);
        }, result);
    };
}
// reduce函数
// fn1(result) --> fn2( fn1(result) ) --> ...
// 这样嵌套下去

// ES6
const compose = 
        (...fns) =>
            result =>
                fns.reverse().reduce(
                    (result, fn) => fn(result),
                    result
                ); 

2.首次调用传入多个参数的情况

上面的函数我们可以知道,每次传入的参数都只能为1个,这样大大的限制了函数的能力,如果函数都是一元的这样无所谓,但是对多远的就不适用。

function compose(...fns) {
    // reduce写在前面, 等待所有函数全部传入完毕后计算
    return fns.reverse().reduce(function reducer(fn1, fn2) {
        return function composed(...args) {
            return fn2( fn1(...args) );
        };
    });
}

// ES6
const compose = 
        (...fns) =>
            fns.reverse().reduce(
                (fn1, fn2) =>
                    (...args) =>
                        fn2( fn1(...args) );
            );

这种方式,先调用reduce,然后将所有的组合组合完成之后做一次性的计算;前面的compose都是传入一个函数就计算出结果,然后将结果作为下一次的输入值

3.使用递归来实现compose

递归形式为:
compose( compose(fn1, fn2, ..., fnN-1), fnN );

实现递归:

function compose(...fns) {
    // 取出最后面的2个实参
    // rest = [fn3, fn4, ..., fnN]
    var [fn1, fn2, ...rest] = fns.reverse();

    var composedFn = function composed(...args) {
        return fn2( fn1(...args) );
    };
        
    // 如果只有2个函数组合
    if (rest.length === 0) {
        return composedFn
    }
    
    return compose(...rest.reverse(), composedFn);
}

// ES6
const compose =
        (...fns) => {
            var [fn1, fn2, ...rest] = fns.reverse();
            var composedFn =
                    (...args) =>
                        fn2( fn1(...args) );
            if (rest.length === 0) {
                return composedFn;
            }
            compose(...rest.reverse(), composedFn);
        };

递归的实现方式思路更加的清晰

四.改变实现顺序 pipe()

上面的compose传参的顺序是 from-right-to-left, 这种顺序的优势是和书写顺序一致,有另一种 from-left-to-right 的顺序, 其本质就是,就是使用 shift() 替换 pop()。使用pipe()的优势是,按照参数执行顺序传入。

function pipe(...fns) {
    return function piped(result) {
        var list = fns.slice();
        
        while (list.length > 0) {
            // 取出第一个参数并执行
            result = list.shift()(result);
        }
        return result;
    }
}
// 或者直接对compose的传参顺序进行反向
// 利用reverseArgs()
const pipe = reverseArgs(compose);

所以上面的一些例子可以写为:

var biggerWords = compose( skipShortWords, unique, words);
// 使用pipe()
var biggerWords = pipe(words, unique, skipShortWords);

// 还有结合partialRight()
var filterWords = partialRight(compose, unique, words);

var filterWords = partialRight(pipe, words, unique);

总结

本章主要是谈组合,即将不同的函数结合成一个函数,也介绍了通用组合的几种实现方式。一种是只能传入一元参数,另一种是首次调用不限制传入参数的个数,以及使用递归的方法实现compose.其中也利用了上一章中实现的 partialRight, reverseArgs的辅助函数。另外抽象和point-free的介绍将在后续的章节中补充。

2016/10/29 14:59:35

你可能感兴趣的:(第2章 函数组合)