第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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 207,113评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,644评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,340评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,449评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,445评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,166评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,442评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,105评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,601评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,066评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,161评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,792评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,351评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,352评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,584评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,618评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,916评论 2 344

推荐阅读更多精彩内容