JS组合函数(Composition):原来如此!
有时会听到组合函数这个概念,就是到这是高阶函数,函数式编程,特别高大上。但是可能我们都没察觉到,平时一直都在使用它。(本文阅读时间约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()在两个重要方面有所不同:
它返回一个值,而不是函数。也就是说,flow() 总是返回一个函数,而pipe()可以返回任何类型的值。
它接受一个值作为它的第一个参数。有了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):原来如此!相关推荐
- 翻译连载 | JavaScript轻量级函数式编程-第4章:组合函数 |《你不知道的JS》姊妹篇...
原文地址:Functional-Light-JS 原文作者:Kyle Simpson-<You-Dont-Know-JS>作者 关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTM ...
- JS偏函数、组合函数、缓存函数
JS偏函数.组合函数.缓存函数 一.JavaScript代码 [附件]/functools.js (function( window ) { "use strict"; var f ...
- 翻译连载 |《你不知道的JS》姊妹篇 |《JavaScript 轻量级函数式编程》- 第 4 章:组合函数...
原文地址:Functional-Light-JS 原文作者:Kyle Simpson-<You-Dont-Know-JS>作者 关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTM ...
- 翻译连载 | JavaScript轻量级函数式编程-第4章:组合函数 |《你不知道的JS》姊妹篇
原文地址:Functional-Light-JS 原文作者:Kyle Simpson-<You-Dont-Know-JS>作者 关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTM ...
- 翻译连载 | JavaScript轻量级函数式编程-第4章:组合函数 |《你不知道的JS》姊妹篇... 1
原文地址:Functional-Light-JS 原文作者:Kyle Simpson-<You-Dont-Know-JS>作者 关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTM ...
- 【JS函数】JS函数之高阶函数、组合函数、函数柯里化
自我介绍:大家好,我是吉帅振的网络日志:微信公众号:吉帅振的网络日志:前端开发工程师,工作4年,去过上海.北京,经历创业公司,进过大厂,现在郑州敲代码. JS函数专栏 1[JS函数]JS函数之普通.构 ...
- JS高级——纯函数、柯里化(手写自动柯里化函数)、组合函数(手写自动组合函数)
一.理解JavaScript纯函数 函数式编程中有一个非常重要的概念叫纯函数,JavaScript符合函数式编程的范式,所以也有纯函数的概念: 在react开发中纯函数是被多次提及的: 比如react ...
- js用函数实现输出100以内与7有关的数_走近(javascript, 函数式)
什么是函数式 目前主流的命令式编程方式当中,将程序抽象成数据和过程的集合.在这里,"名词"是第一词汇,我们将程序视为一系列自上而下的命令,去不断修改其中的数据,我们更专注于描述不同 ...
- js 闭包函数 构造函数_JavaScript中的闭包,库里函数和酷抽象
js 闭包函数 构造函数 In this article, we will talk about closures and curried functions and we'll play aroun ...
最新文章
- 可延迟函数、内核微线程以及工作队列
- 2、Power Query-动态汇总单元格区域数据
- SQL语句中常用关键词及其解释如下.pdf
- (day 52 - 先序后序遍历计数 ) 剑指 Offer 55 - II. 平衡二叉树
- Python中List,tuple,Dictionary之间的区别
- 解决qt调试时Unknown debugger type No Engine
- 实验室信息化建设助力医药研发
- java影视app对接cms,原生影视App双端对接飞飞CMS
- SAP Fiori 的附件处理(Attachment handling)
- 如果再来一次,你还会选择互联网么?
- 美国加州大学数据安全保护措施TOP10借鉴与启发
- Android 开源项目分类汇总 APP功能汇总
- 一些公开免费的后台数据接口
- Chia官方矿池测试版正式上线!?
- linux rac节点主机不定时重启,RAC 有个节点不定时重启-还请大侠们帮忙看看
- 单元识别码是什么意思_NLPer入门指南 | 完美第一步
- Java“白皮书”的关键术语
- 《Delphi 版 everything、光速搜索代码》 关于获取文件全路径 GetFullFileName 函数的优化
- 这一把子彻底搞懂 setState 原理
- 用原生js把数字转换成货币人民币表示带逗号表示方法
热门文章
- 【Linux】IRQ
- python学习之爬取ts流电影
- 【NOIP 2017】Day2 T3 列队
- android商城demo,3 分钟快速 Demo(Android)
- Pytorch 学习率衰减方法
- 什么是一个可执行文件?
- python报错“IndentationError: unexpected indent“的两三解决方法
- GAMES101笔记_Lec07~09_着色 Shading
- JavaScript变量详解加实例教程
- 解决MATLAB中0与o,1与l难以分辨