有时会听到组合函数这个概念,就是到这是高阶函数,函数式编程,特别高大上。但是可能我们都没察觉到,平时一直都在使用它。(本文阅读时间约15分钟)

目录

1. 组合函数是什么?

2. COMPOSE

3. FLOW

4. PIPE

5. 原来如此!

6. 参考资料


1. 组合函数是什么

组合函数是将两个函数组合成一个新函数。(柯里化可以让函数更灵活,让函数的粒度更小,便于函数组合产生最大的功能),也就是说,用新函数调用一个函数,获取结果,然后将其传递给另一个函数。仅此而已。在代码中,它看起来像这样:

// c2是组合的新函数,相当于另外两个函数的缩写
const c2 = (funcA, funcB) => x => funcA(funcB(x));

可以看到出现这样一种情况,从一个函数返回一个函数。这就是为什么那里有两个箭头。

我们怎么实际应用它?来,让我们想象一下我们正在开发某种评论功能。例如,我们希望在评论中仅允许图片和链接,但不允许任何其他HTML。为了实现这一点,我们将创建一个简单的Markdown。链接如下所示:

[link text goes here](http://example.com/example-url)

图片如下所示:

![alt text goes here](/link/to/image/location.png)

现在,使用正则表达式,我们可以为每个表达式编写一个函数。用适当的 HTML 替换字符串并:

const imagify = str => str.replace(/!\[([^\]"<]*)\]\(([^)<"]*)\)/g,'<img src="$2" alt="$1" />'
);
const linkify = str => str.replace(/\[([^\]"<]*)\]\(([^)<"]*)\)/g,'<a href="$2" rel="noopener nowfollow">$1</a>'
);

创建一个同时转换图片和链接的函数,就是c2():

const linkifyAndImagify = c2(linkify, imagify);

不过,c2()在这里使用并没减少太多代码:

const linkifyAndImagify = str => linkify(imagify(str));

c2()函数包含了这2项处理。如果我们添加更多功能,过程会稍微繁琐一点。例如,还又要添加对下划线的处理:

const emphasize = str => str.replace(/_([^_]*)_/g,'<em>$1</em>'
);

我们可以将它与其他处理函数能一起添加:

const processComment = c2(linkify, c2(imagify, emphasize));

与普通写法相比较:

const processComment = str => linkify(imagify(emphasize(str)));

使用c2(), 就显得更短。但也没短很多。如果我们可以定义自定义操作符,那就更好了。例如,我们可以定义一个项目符号运算符 (•),它将右侧的函数与左侧的函数组合在一起。然后我们将processComment()像这样构建我们的函数:

const processComment = linkify • imagify • emphasize;

JavaScript没有让我们定义自定义操作符。无它,我们将编写一个多元组合函数。

2. COMPOSE

我们为了让多个函数的组合变得更容易。为此,我们可以使用剩余参数将参数列表转换为数组。让它们排成一行,就可以.reduceRight()依次调用每个函数。代码如下所示:

const compose = (...fns) => x0 => fns.reduceRight(
    (x, f) => f(x),
    x0
);

为了说明compose()如何运行,在评论处理中又又再添加一项功能。允许评论者通过在一行的开头<h3>放置三个井号:###

const headalize = str => str.replace(
    /^###\s+([^\n<"]*)/mg,
    '<h3>$1</h3>'
);

我们构建我们的函数来处理,如下所示:

const processComment = compose(linkify, imagify, emphasize, headalize);

如果代码太长,就折行显示:

const processComment = compose(
    linkify,
    imagify,
    emphasize,
    headalize
);

不过这里有个小问题。headalize()是序列中最后一个函数却是第一个运行的函数,这有点尴尬。如果我们从上到下阅读,函数的顺序是相反的。这是因为compose()运行手写时的顺序:

const processComment = str => linkify(imagify(emphasize(headalize(str))));

这就是为什么compose()使用.reduceRight()而不是.reduce(). 顺序很重要。如果我们之前用linikfy()调用过imagify(),代码将无法运行。所有的图片都变成了链接。

Compose 从右到左处理其功能。我们从一个字符串开始,将其传递给headalize(),然后emphasize(),然后imagify(),最后传递给linkify(),它返回另一个字符串。如果我们要在垂直列表中编写函数,为什么不颠倒顺序呢?我们可以编写一个函数来组合另一个方向的函数。这样,数据从上到下流动。

3. FLOW

要一个反方向版本compose(),我们需要做的就是使用.reduce()而不是.reduceRight(). 看起来像这样:

// We call this function 'flow' as the values flow,
// from left to right.
const flow = (...fns) => x0 => fns.reduce(
    (x, f) => f(x),
    x0
);

为了展示它是如何工作的,我们又又又添加另一个功能。这一次,我们将在反引号之间添加代码格式:

const codify = str => str.replace(/`([^`<"]*)`/g, '<code>$1</code>');

放到flow()中,就得到:

const processComment = flow(
    headalize,
    emphasize,
    imagify,
    linkify,
    codify
);

Flow 从左到右处理其功能。我们从一个字符串开始,将其传递给headalize(),然后emphasize(),然后imagify(),然后linkify(),最后传递给codify()。相比我们手动编写要好看的多:

const processComment = str => codify(
    linkify(
        imagify(
            emphasize(
                headalize(str)
            )
        )
    )
);

确实,flow()比较简洁。而且由于使用起来相当方便,我们可能会发现自己经常使用它来构建函数。

但是如果这个调用是一次性的,有时我们,会偷懒,只写成一个立即执行函数。例如:

const processedComment = flow(
    headalize,
    emphasize,
    imagify,
    linkify,
    codify
)(commentStr);

这时就很尴尬。可以发现这种立即执行函数的写法不好。就算我的同事不挑我这小毛病,那两个双括号仍然有也确实难看。

所以,我们可以创建另一个组合函数解决这个问题。

4. PIPE

我们将创建一个新函数 ,pipe(),它使用的剩余参数与 略有不同flow():

const pipe = (x0, ...fns) => fns.reduce(
    (x, f) => f(x),
    x0
);

pipe()与flow()在两个重要方面有所不同:

  1. 它返回一个值,而不是函数。也就是说,flow() 总是返回一个函数,而pipe()可以返回任何类型的值。

  2. 它接受一个值作为它的第一个参数。有了flow(),所有参数都必须是函数。但是对于pipe(),第一个参数是函数传递的值。

好处就是立即运行。这意味着我们不能重用组合函数。实际情况的大多数时候也不需要重用。

为了说明pipe()的效果,稍微改变一下示例。假设有一系列评论要处理。我们可能会定义一些业务函数来处理数组:

const map    = f => arr => arr =>arr.map(f);
const filter = p => arr => arr.filter(p);
const take   = n => arr => arr.slice(0, n);
const join   = s => arr => arr.join(s);

也许还有一些字符串的业务处理函数:

const itemize        = str => `<li>${str}</li>`;
const orderedListify = str => `<ol>${str}</ol>`;
const chaoticListify = str => `<ul>${str}</ul>`;
const mentionsNazi   = str => (/\bnazi\b/i).test(str);

然后我们可以像这样把它们放在一起pipe():

const comments = pipe(commentStrs,
    filter(noNazi),
    take(10),
    map(emphasize),
    map(itemize),
    join('\n'),
);

仔细看的话,pipe ()与函数链式调用并没有太大区别:

const comments = commentStrs
    .filter(noNazi)
    .slice(0, 10)
    .map(emphasize)
    .map(itemize)
    .join('\n');

问题来了,肯定有人人会觉得函数链看起来更干净一些,确实有些道理。那我为什么要浪费时间pipe(),既然所有业务函数所做的就是调用数组方法,为什么不直接调用?

答案是,pipe()比函数链有优势。即使pipe中的值没有可调用的方法,它也可以使用裸函数保持pipe。例如,我们可以添加chaoticListify()到我们的pipe:

const comments = pipe(commentStrs,
    filter(noNazi),
    take(10),
    map(emphasize),
    map(itemize),
    join('\n'),
    chaoticListify,
);

如果我们愿意,我们可以继续添加更多处理函数。并且可以通过这种方式构建整个业务处理过程。

5. 原来如此!

我认为compose()、flow()、pipe()非常简洁。但如果有人怀疑,我也能理解。毕竟,我们仍然可以使用变量赋值来编写上面的pipe代码:

const withoutNazis                 = commentStrs.filter(noNazi);
const topTen                           = withoutNazis.slice(0, 10);
const itemizedComments       = topTen.map(itemize);
const emphasizedComments = itemizedComments.map(emphasize);
const joinedList                       = emphasizedComments.join('\n');
const comments                     = chaoticListify(joinedList);

这段代码很好。对于很多人来说都是容易阅读和理解。既然能完成与compose版本相同的结果,为什么会有人用pipe()?

为了回答这个问题,我希望大家看看这两个代码块并做两件事:

  • 数一数分号数量。

  • 看看在变量赋值版本中使用了哪些业务函数。

看看变量赋值版本有六个分号了么?而pipe()版本确只有一个。这里有一些微小但重要的东西。 在变量赋值版本中,创建了6条语句, 在pipe()版本中,我们将整个东西组合成一个表达式。 使用表达式是函数式编程的核心思想。

使用pipe()为构造程序开辟了一种全新的方式, 我们就可以把代码写成一系列的指令, 这很像烹饪菜谱,先这样,再那样,然后再这样……。 在组合函数中,我们将用代码表示为函数之间的关系。

这看起来还是不那么令人印象深刻。 谁在乎组合函数是否为编写代码开辟了另一种方式呢? 若干年来,我们一直在写变量完成同样任务(变量赋值版本会产生更多的中间变量),区别只是改变了js引擎调用堆栈的一部分,而本质上,这两个版本做的是同一件事同样的结果。

但是,组合函数的意义并不在于它如何改变代码,它的意义在于它如何改变我们。 特别是它如何改变我们的思维方式。

组合函数鼓励把代码看作表达式之间的关系,这反过来又使得专注于想要的结果。 也就是说,相比于关注每一步的实现细节,更重要的是使用小型的、可重用的函数来编写代码。 这加强了对结果的关注,而不是实现细节。正因如此,代码变得具有更强的语义化和描述性。

到目前为止的示例代码,这种关注焦点的变化可能不是很明显,一直在用作比较的两个例子并没有太大的不同,下面可以证明pipe()版本更具有描述性,我们可以使pipe()版本运行的更高效(性能),而无需更改任何pipe代码:

const map = f => function*(iterable) {for (let x of iterable) yield f(x);
};const filter = p => function*(iterable) {for (let x of iterable) {if (p(x)) yield x;}
};const take = n => function*(iterable) {let i = 0;for (let x of iterable) {if (i >= n) return;yield x;
    i++;}
};const join = s => iterable => [...iterable].join(s);

这里不需要改变pipe():

const comments = pipe(commentStrs,filter(noNazi),take(10),map(emphasize),map(itemize),join('\n'),
    chaoticListify,
);

业务函数如何执行的细节并不是很重要,这次示例,使用 Generator 而不是数组方法。使用generator意味着我们不再创建数组。但这里的重点不在于效率,generators代码也可能根本不会提高性能,没关系。关键是结果。它使用完全不同的机制来遍历数据。并且得到相同的结果。

这里的重点是思维的转变。我们编写了使用变量赋值和generators的代码版本,我们得到同样的结果,因为pipe定义为函数之间的关系。为此,我们需要一堆可重用的业务处理函数。这让我们可以在不改变顶层设计意图的情况下更改实现细节。这就是为什么函数组合很重要的原因。

就其核心而言,组合函数并不复杂,结合两个函数很简单,容易理解。我们已经研究了如何使用组合函数,还将其扩展为组合多个函数。我们也探索了compose(),flow()和pipe()的不同形式。我们可以使用这些函数来创建简洁、优雅的代码。但组合函数之美不在于代码之美,而在于它如何改变我们,它为我们提供了思考代码的新方法。

6. 参考资料

  • JavaScript 正则表达式。

  • https://regexr.com。

  • TC39

JS组合函数(Composition):原来如此!相关推荐

  1. 翻译连载 | JavaScript轻量级函数式编程-第4章:组合函数 |《你不知道的JS》姊妹篇...

    原文地址:Functional-Light-JS 原文作者:Kyle Simpson-<You-Dont-Know-JS>作者 关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTM ...

  2. JS偏函数、组合函数、缓存函数

    JS偏函数.组合函数.缓存函数 一.JavaScript代码 [附件]/functools.js (function( window ) { "use strict"; var f ...

  3. 翻译连载 |《你不知道的JS》姊妹篇 |《JavaScript 轻量级函数式编程》- 第 4 章:组合函数...

    原文地址:Functional-Light-JS 原文作者:Kyle Simpson-<You-Dont-Know-JS>作者 关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTM ...

  4. 翻译连载 | JavaScript轻量级函数式编程-第4章:组合函数 |《你不知道的JS》姊妹篇

    原文地址:Functional-Light-JS 原文作者:Kyle Simpson-<You-Dont-Know-JS>作者 关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTM ...

  5. 翻译连载 | JavaScript轻量级函数式编程-第4章:组合函数 |《你不知道的JS》姊妹篇... 1

    原文地址:Functional-Light-JS 原文作者:Kyle Simpson-<You-Dont-Know-JS>作者 关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTM ...

  6. 【JS函数】JS函数之高阶函数、组合函数、函数柯里化

    自我介绍:大家好,我是吉帅振的网络日志:微信公众号:吉帅振的网络日志:前端开发工程师,工作4年,去过上海.北京,经历创业公司,进过大厂,现在郑州敲代码. JS函数专栏 1[JS函数]JS函数之普通.构 ...

  7. JS高级——纯函数、柯里化(手写自动柯里化函数)、组合函数(手写自动组合函数)

    一.理解JavaScript纯函数 函数式编程中有一个非常重要的概念叫纯函数,JavaScript符合函数式编程的范式,所以也有纯函数的概念: 在react开发中纯函数是被多次提及的: 比如react ...

  8. js用函数实现输出100以内与7有关的数_走近(javascript, 函数式)

    什么是函数式 目前主流的命令式编程方式当中,将程序抽象成数据和过程的集合.在这里,"名词"是第一词汇,我们将程序视为一系列自上而下的命令,去不断修改其中的数据,我们更专注于描述不同 ...

  9. js 闭包函数 构造函数_JavaScript中的闭包,库里函数和酷抽象

    js 闭包函数 构造函数 In this article, we will talk about closures and curried functions and we'll play aroun ...

最新文章

  1. 可延迟函数、内核微线程以及工作队列
  2. 2、Power Query-动态汇总单元格区域数据
  3. SQL语句中常用关键词及其解释如下.pdf
  4. (day 52 - 先序后序遍历计数 ) 剑指 Offer 55 - II. 平衡二叉树
  5. Python中List,tuple,Dictionary之间的区别
  6. 解决qt调试时Unknown debugger type No Engine
  7. 实验室信息化建设助力医药研发
  8. java影视app对接cms,原生影视App双端对接飞飞CMS
  9. SAP Fiori 的附件处理(Attachment handling)
  10. 如果再来一次,你还会选择互联网么?
  11. 美国加州大学数据安全保护措施TOP10借鉴与启发
  12. Android 开源项目分类汇总 APP功能汇总
  13. 一些公开免费的后台数据接口
  14. Chia官方矿池测试版正式上线!?
  15. linux rac节点主机不定时重启,RAC 有个节点不定时重启-还请大侠们帮忙看看
  16. 单元识别码是什么意思_NLPer入门指南 | 完美第一步
  17. Java“白皮书”的关键术语
  18. 《Delphi 版 everything、光速搜索代码》 关于获取文件全路径 GetFullFileName 函数的优化
  19. 这一把子彻底搞懂 setState 原理
  20. 用原生js把数字转换成货币人民币表示带逗号表示方法

热门文章

  1. 【Linux】IRQ
  2. python学习之爬取ts流电影
  3. 【NOIP 2017】Day2 T3 列队
  4. android商城demo,3 分钟快速 Demo(Android)
  5. Pytorch 学习率衰减方法
  6. 什么是一个可执行文件?
  7. python报错“IndentationError: unexpected indent“的两三解决方法
  8. GAMES101笔记_Lec07~09_着色 Shading
  9. JavaScript变量详解加实例教程
  10. 解决MATLAB中0与o,1与l难以分辨