七、转换函数——柯里化和部分应用
在第 6 章,生成函数——高阶函数中,我们看到了几种操作函数的方法,以获得一些功能上的改变的新版本。 在本章中,我们将讨论一种特殊的转换,一种factory方法,它允许您使用一些固定参数生成任何给定函数的新版本。
我们将考虑以下事项:
- curcurry:经典的 FP 理论函数,它将多参数函数转换为一元函数序列。
- :另一个历史悠久的 FP 转换,它通过修改函数的一些参数来生成新版本的函数。
- Partial curting(a name of my own):可以看作是前两种转换的混合。
公平地说,我们还将看到,通过简单的箭头函数可以更清晰地模拟其中一些技术。 然而,由于你很容易在 FP 上的各种文本和网页中发现 curging 和局部应用,所以了解它们的含义和用法是非常重要的,即使你选择了更简单的方法。 使用本章中的技术将为您提供一种用其他函数生成函数的不同方法,我们将在接下来的章节中查看这些思想的几个应用。
一点理论
我们将在本章中讨论的概念在某些方面非常相似,而在另一些方面则非常不同。 我们经常会对它们的真正含义感到困惑,也有很多网页滥用术语。 你甚至可以说,本章中的所有转换都是大致相同的,因为它们让你将一个函数转换为另一个函数,修复一些参数,让其他函数自由,最终导致相同的结果。 好吧,我同意,这不是很清楚! 所以,让我们从澄清问题开始,并提供一些简短的定义,我们将在后面展开。 (如果你觉得你的眼睛发呆了,请跳过这部分,待会再来看!) 是的,你可能会觉得下面的描述有点令人费解,但请容忍我们——我们稍后会更详细地讲解:
- 局部套用转化的过程是一个米必要功能(即函数的参数数量)成一个序列的一元函数,每个接收一个参数的原始功能,从左到右。 (第一个函数接收原始函数的第一个参数,并返回接收第二个参数的第二个函数,并返回接收第三个参数的第三个函数,以此类推。) 在使用参数调用时,每个函数都会生成序列中的下一个函数,最后一个函数执行实际计算。 部分应用的想法提供n 参数一个m必要功能,被n 小于或等于【显示】,将它转换成一个函数与(mn)参数。 每次您提供一些参数时,都会生成一个新的函数,其大小更小。 当您提供最后一个参数时,将执行实际的计算。 部分局部套用是一个混合的两个前的想法:您提供n 参数(从左到右)必要功能和你产生一个新函数的参数数量(mn*)。 当这个新函数接受其他一些参数时,也从左到右,它将生成另一个函数。 当提供最后一个参数时,函数将生成正确的计算结果。*
*在本章中,我们将看到这三种转换,它们需要什么,以及实现它们的方法。 关于这一点,我们将介绍编写每个高阶函数的多种方法,这将使我们对编写 JavaScript 的有趣方法有一些了解,您可能会对其他应用感兴趣。
局部套用
我们已经在第 1 章的箭头函数章节中提到过,,Becoming Functional - Several Questions,One argument or many? 【显示】的部分第三章,开始功能——一个核心概念,但我们更彻底。 curry 是一种允许您只使用单变量函数的技术,即使您需要多变量函数。
The idea of converting a multi-variable function into a series of single-variable functions (or, more rigorously, reducing operators with several operands, to a sequence of applications of a single operand operator) was worked on by Moses Schönfinkel, and there have been some authors who suggest, not necessarily tongue-in-cheek, that currying would be more correctly named Schönfinkeling!
在下一节中,我们将首先看到如何处理有许多参数的函数,然后我们将继续了解如何手工进行 curry,或者使用bind()
或eval()
。
处理多个参数
咖喱的概念本身就很简单。 如果你需要一个有三个参数的函数,你可以使用箭头函数写出如下内容:
const make3 = (a, b, c) => String(100 * a + 10 * b + c);
或者,你可以有一个函数序列,每个函数都有一个参数,如下所示:
const make3curried = a => b => c => String(100 * a + 10 * b + c);
或者,您可能希望将它们视为嵌套函数,如以下代码片段:
const make3curried2 = function(a) {
return function(b) {
return function(c) {
return String(100 * a + 10 * b + c);
};
};
};
就用法而言,在如何使用每个函数方面有一个重要的区别。 虽然您可以以通常的方式调用第一个定义,例如make3(1,2,4)
,但这对第二个定义不起作用。 make3curried()
是一个一元(单个参数),所以应该写make3curried(1)
。 但这又能得到什么呢? 根据前面的定义,这个函数也返回一元函数——那个函数也返回一元函数! 因此,正确的调用将得到与三元函数相同的结果:make3curried(1)(2)(4)
! 参见图 7.1:
Figure 7.1: The difference between a common function and a curried equivalent.
仔细研究这个——我们有第一个函数,当我们对它应用一个参数时,我们得到第二个函数。 对它应用一个参数将产生第三个函数,最终的应用将产生所需的结果。 在理论计算中,这可以看作是一种不必要的练习,但它实际上带来了一些好处,因为您可以始终使用一元函数,即使您需要带有更多参数的函数。
Since there is a currying transformation, there is also an uncurrying one! In our case, we would write make3uncurried = (a, b, c) => make3curried(a)(b)(c)
to revert the currying process and make it usable, once again, to provide all parameters in one sitting.
在一些语言中,例如 Haskell,函数只允许接受一个参数——但是,该语言的语法允许您调用函数,就像允许使用多个参数一样。 以我们的示例为例,在 Haskell 中,编写make3curried 1 2 4
将生成结果124
,任何人甚至不需要知道它涉及三个函数调用,每个调用都有一个参数。 由于您没有在参数周围写圆括号,也没有用逗号分隔它们,因此您无法判断您提供的不是一个值的三元组,而是三个奇异值。
curry 是 Scala 或 Haskell 的基础,它们都是全函数式语言,但 JavaScript 有足够的特性允许我们在工作中定义和使用 curry。 这不会那么容易,毕竟,它不是内置的——但我们能够应付。
因此,回顾一下基本概念,我们原make3()
与make3curried()
的主要区别如下:
make3()
是三元函数,make3curried()
是一元函数。make3()
返回一个字符串;make3curried()
返回另一个函数,这个函数本身返回一个第二个函数,第二个函数又返回一个第三个函数,该函数最终返回一个字符串!- 你可以通过写类似于
make3(1,2,4)
的东西来生成一个字符串,它返回124
,但是你必须写make3curried(1)(2)(4)
来得到相同的结果。
你为什么要这么麻烦? 让我们看一个简单的例子,然后我们会看更多的例子。 假设你有一个函数,计算某一金额的增值税(增值税),如下所示:
const addVAT = (rate, amount) => amount * (1 + rate / 100);
addVAT(20, 500); // 600 -- *that is,* 500 + 20%
addVAT(15, 200); // 230 -- 200 +15%
如果您必须应用一个单一的、恒定的速率,那么您可以 curryaddVAT()
函数,以产生一个总是应用给定速率的更专门的版本。 例如,如果你的国家利率是 6%,那么你可以得到如下结果:
const addVATcurried = rate => amount => amount * (1 + rate / 100);
const addNationalVAT = addVATcurried(6);
addNationalVAT(1500); // 1590 -- 1500 + 6%
第一行定义了 vat 计算函数的 curry 版本。 给定一个税率,addVATcurried()
返回一个新函数,当给定一笔钱时,它最终将原来的税率加到其中。 因此,如果国家税率是 6%,那么addNationalVAT()
将是一个函数,它将在给定的任何数额上增加 6%。 例如,如果我们像前面代码一样计算addNationalVAT(1500)
,结果将是 1590:1500 美元,加上 6%的税。
当然,你可能会说,仅仅加 6%的税,咖喱有点多,但简化才是最重要的。 让我们再看一个例子。 在您的应用中,您可能希望包含一些日志记录,使用如下函数:
let myLog = (severity, logText) => {
// *display logText in an appropriate way,*
// *according to its severity ("NORMAL", "WARNING", or "ERROR")*
};
然而,使用这种方法,每一次你想要显示一个正常的日志消息,您将编写myLog
("NORMAL"
,一些正常的文本),并警告,你会写myLog
("WARNING"
,有些警告),但你可以简化这一点与鞭笞,通过修复myLog()
的第一个参数如下,与curry()
功能,我们以后再看。 我们的代码可以如下所示:
myLog = curry(myLog);
// *replace myLog by a curried version of itself*
const myNormalLog = myLog("NORMAL");
const myWarningLog = myLog("WARNING");
const myErrorLog = myLog("ERROR");
你得到了什么? 现在您可以编写myNormalLog("some normal text")
或myWarningLog("some warning")
,因为您已经 curry 了myLog()
,然后修复了它的参数——这使得代码更简单、更容易阅读!
顺便说一下,如果你喜欢,你也可以在一个单一的步骤中获得相同的结果,使用原始的myLog()
功能,通过逐个情况地对它进行 curry:
const myNormalLog2 = curry(myLog)("NORMAL");
const myWarningLog2 = curry(myLog)("WARNING");
const myErrorLog2 = curry(myLog)("ERROR");
因此,拥有一个curry()
函数可以让你修复一些参数,同时让其他参数仍然开放; 让我们看看如何用三种不同的方法来做这件事。
用手梳刷
在尝试更复杂的东西之前,我们可以手工生成一个函数,而不需要任何特殊的辅助函数或其他任何东西。 事实上,如果我们只是想为一个特殊情况实现 curry,不需要做任何复杂的事情,因为我们可以用简单的箭头函数来管理:我们看到了make3curried()
和addVATcurried()
,所以没有必要重新考虑这个想法。
相反,让我们研究一些自动实现的方法,这样我们就能够生成任何函数的等效咖喱版本,即使事先不知道它的性质。 更进一步,我们可能想要编写一个更智能的函数版本,根据接收的参数的数量可以不同地工作。 例如,我们可以有一个sum(x,y)
函数,其行为如下所示:
sum(3, 5); // 8; *did you expect otherwise?*
const add3 = sum(3);
add3(5); // 8
sum(3)(5); // 8 -- *as if it were curried*
我们可以用手来实现这种行为。 我们的函数将类似于以下内容:
const sum = (x, y) => {
if (x !== undefined && y !== undefined) {
return x + y;
} else if (x !== undefined && y == undefined) {
return z => sum(x, z);
} else {
return sum;
}
};
让我们回顾一下。 我们手工 curry 的函数有这样的行为:
- 如果我们用两个参数调用它,它将它们相加,并返回和; 这提供了我们的第一个用例,如
sum(3,5)==8
。 - 如果只提供一个参数,它将返回一个新函数。 这个新函数需要一个参数,并将返回该参数和原始参数的和:这个行为是我们在其他两个用例中所期望的,例如
add2(3)==5
或sum(2)(7)==9
。 - 最后,如果没有提供参数,它将返回自己。 这意味着如果我们愿意,我们可以写
sum()(1)(2)
。 (不,我想不出写这篇文章的理由。)
所以,如果我们愿意,我们可以在函数的定义中加入 curry。 但是,您必须承认,必须处理每个函数中的所有特殊情况很容易变得麻烦,而且容易出错。 那么,让我们试着找出一些更通用的方法来实现相同的结果,而不需要任何特定的编码。
局部套用与绑定()
我们可以用bind()
方法来解决 curry 的问题。 这允许我们修复一个参数(如果需要,可以修复多个; 我们这里不需要这样做,但稍后我们将使用它),并提供一个带有固定参数的函数。 当然,许多库(如 Lodash、Underscore、Ramda 等)都提供了这个功能,但我们希望了解如何自己实现它。
Read more on .bind()
at https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_objects/Function/bind—it will be useful since we'll take advantage of this method at other points in this chapter.
我们的实现很短,但需要一些解释:
const curryByBind = fn =>
fn.length === 0 ? fn() : p => curryByBind(fn.bind(null, p));
首先注意,curryByBind()
总是返回一个新函数,它依赖于作为参数的fn
函数。 如果函数没有(更多)参数剩下(当fn.length===0
),因为所有参数都已经固定,我们可以简单地使用fn()
计算它。 否则,对函数进行 curry 的结果将是一个接收单个参数的新函数,它本身将产生一个新的 curry 过的函数,带有另一个固定的参数。 让我们用一个详细的例子来看看它是如何运行的,这个例子使用了我们在本章开始时看到的make3()
函数:
const make3 = (a, b, c) => String(100 * a + 10 * b + c);
// *f1 is a function that will fix make3's 1st parameter* const f1 = curryByBind(make3);
// *f2 is a function that will fix make3's 2nd parameter* const f2 = f1(6);
// *f3 is a function that will fix make3's last parameter* const f3 = f2(5);
// *"658" will be now calculated, since there are* // *no more parameters to fix* const f4 = f3(8);
这段代码的解释如下:
- 第一个函数,
f1()
,还没有收到任何参数。 它的结果是一个单形参的函数,它本身将产生一个 curry 版本的make3()
,其第一个实参固定为给定的值。 - 叫
f1(6)
产生新的一元函数,f2()
,将自己生产的咖喱版本make3()
——第一个参数设置为6
,所以实际上新功能最终会解决第二个参数的make3()
。 - 类似地,调用
f2(5)
会生成第三个一元函数f3()
,它将生成make3()
的一个版本,但是会修复它的第三个参数,因为前两个参数已经修复了。 - 最后,当我们计算
f3(8)
时,这将把make3()
的最后一个参数修复为8
,并且由于没有更多的参数剩下,将调用三次绑定的make3()
函数,并生成结果"658"
。
如果想手工 curry 函数,可以使用 JavaScript 的.bind()
方法。 顺序如下:
const step1 = make3.bind(null, 6);
const step2 = step1.bind(null, 5);
const step3 = step2.bind(null, 8);
step3(); // *"658"*
在每一步中,我们提供一个进一步的参数。 (需要使用null
值来提供上下文。 如果它是一个附加到对象的方法,我们将提供该对象作为.bind()
的第一个参数。 既然不是这样,null
是预期的。) 这相当于我们的代码所做的,除了上次,curryByBind()
做实际的计算,而不是像step3()
那样让您自己做。
测试这种转换相当简单——因为没有很多可能的 curry 方法:
const make3 = (a, b, c) => String(100 * a + 10 * b + c);
describe("with curryByBind", function() {
it("you fix arguments one by one", () => {
const make3a = curryByBind(make3);
const make3b = make3a(1)(2);
const make3c = make3b(3);
expect(make3c).toBe(make3(1, 2, 3));
});
});
你还能测试什么? 也许可以添加只有一个参数的函数,但是没有更多的参数可以尝试。
如果我们想 curry 一个带有可变参数的函数,那么使用fn.length
是不行的; 它只对带有固定数量参数的函数有一个值。 我们可以简单地解决这个问题,通过提供所需的参数数量:
const curryByBind2 = (fn, len = fn.length) =>
len === 0 ? fn() : p => curryByBind2(fn.bind(null, p), len - 1);
const sum2 = (...args) => args.reduce((x, y) => x + y, 0);
sum2.length; // *0;* *curryByBind() wouldn't work*
sum2(1, 5, 3); // 9
sum2(1, 5, 3, 7); // 16
sum2(1, 5, 3, 7, 4); // 20
curriedSum5 = curryByBind2(sum2, 5); // *curriedSum5 will expect 5 parameters*
curriedSum5(1)(5)(3)(7)(4); // *20*
新的curryByBind2()
函数和以前一样工作,但它不依赖于fn.length
,而是使用len
参数,默认为fn.length
,对于参数数量恒定的标准函数。 注意,当len
不为 0 时,返回的函数调用curryByBind2()
时,将len-1
作为最后一个参数——这是有意义的,因为如果一个参数刚刚被修复,那么就少了一个参数需要修复。
在我们的例子中,sum()
函数可以使用任意数量的参数,JavaScript 告诉我们sum.length
为零。 然而,当对函数进行 curry 操作时,如果将len
设置为5
,则会进行 curry 操作,就好像sum()
是一个有五个参数的函数一样——前面代码的最后一行显示了情况确实如此。
和之前一样,测试是相当简单的,因为我们没有变体可以尝试:
const sum2 = (...args) => args.reduce((x, y) => x + y, 0);
describe("with curryByBind2", function() {
it("you fix arguments one by one", () => {
const suma = curryByBind2(sum2, 5);
const sumb = suma(1)(2)(3)(4)(5);
expect(sumb).toBe(sum2(1, 2, 3, 4, 5));
});
it("you can also work with arity 1", () => {
const suma = curryByBind2(sum2, 1);
const sumb = suma(111);
expect(sumb).toBe(sum2(111));
});
});
作为边界情况,我们测试了将 curry 函数的属性设置为1
,但没有更多的可能性。
局部套用与 eval ()
还有另一种有趣的方法来处理函数——通过eval()
创建一个新的函数。 是的——那不安全,危险eval()
! (记住我们之前说过的:这是为了学习,但您最好避免eval()
可能带来的潜在安全问题!) 我们还将使用在第 5 章的部分中编写的range()
函数,Declaratively Programming - A Better Style。
Languages such as LISP have always had the possibility of generating and executing LISP code. JavaScript shares that functionality, but it's not often used—mainly because of the dangers it may entail! However, in our case, since we want to generate new functions, it seems logical to take advantage of this neglected capability.
这个想法很简单:在A bit theory部分(本章早些时候),我们看到我们可以通过使用箭头函数轻松地 curry 一个函数,如下所示:
const make3 = (a, b, c) => String(100 * a + 10 * b + c);
const make3curried = a => b => c => String(100 * a + 10 * b + c);
让我们对第二个版本进行一些修改,以一种有助于我们的方式重写它,正如你们将看到的。 首先,我们可以改变参数的名称,直接调用原始的make3()
函数:
const make3curried = x1 => x2 => x3 => make3(x1, x2, x3);
我们为什么要这么做? 答案很简单:帮助自动生成所需的代码。 我们将使用range()
函数写在使用范围的第五章,编程以声明的方式,一个更好的风格,以避免需要写一个显式的循环:**
const range = (start, stop) =>
new Array(stop - start).fill(0).map((v, i) => start + i);
const curryByEval = (fn, len = fn.length) =>
eval(`${range(0, len).map(i => `x${i}`).join("=>")} =>
${fn.name}(${range(0, len).map(i => `x${i}`).join(",")})`);
这是一段需要消化的代码,实际上,应该将其编码为几行,以使其更易于理解。 让我们看看当应用到make3()
函数作为输入时是如何工作的:
- 函数的作用是:生成一个具有
[0,1,2]
值的数组。 如果不提供len
参数,将使用make3.length
(即3
)。 - 我们使用
map()
生成一个具有["x0","x1","x2"]
值的新数组。 - 我们
join()
数组中的值产生x0=>x1=>x2
,这将是我们eval()
代码的开始。 - 然后添加一个箭头、函数名和一个开括号,使新生成的代码的中间部分成为:
=> make3(
。 - 我们再次使用
range()
、map()
和join()
,但这次是生成一个参数列表:x0,x1,x2
。 - 最后添加一个右括号,应用
eval()
后,得到make3()
的咖喱版。
在所有这些步骤之后,在我们的例子中,得到的函数如下:
curryByEval(make3); // x0=>x1=>x2=> make3(x0,x1,x2)
只有一个问题:如果原始函数没有名称,那么转换就无法工作。 (更多相关内容,请参阅第 3 章、Of lambdas and functions章节。) 我们可以通过包含要 curry 的函数的实际代码来解决函数名问题:
const curryByEval2 = (fn, len = fn.length) =>
eval(`${range(0, len).map(i => `x${i}`).join("=>")} =>
(${fn.toString()})(${range(0, len).map(i => `x${i}`).join(",")})`);
唯一的变化是,我们代替了原始函数名,取而代之的是它的实际代码:
curryByEval2(make3); // x0=>x1=>x2=> ((a,b,c) => 100*a+10*b+c)(x0,x1,x2)
生成的函数令人惊讶,它有一个完整的函数,后面跟着它的参数——但这实际上是有效的 JavaScript! 事实上,除了下面的add()
函数,你还可以在函数定义后面加上参数,如下面的代码的最后一行所示:
const add = (x, y) => x + y;
add(2, 5); // 7
((x, y) => x + y)(2, 5); // *7*
当你想调用一个函数时,你写好它,然后在圆括号中跟着它的参数——这就是我们所做的一切,即使它看起来很奇怪! 现在我们已经完成了 curry 技术,这可能是最著名的 FP 技术,所以让我们转向部分应用,这样您就可以更灵活地编写自己的代码。
部分应用
我们将考虑的第二个转换允许您修复函数的一些参数,创建一个新函数,该函数将接收其余的参数。 让我们用一个毫无意义的例子来说明这一点。 假设你有一个有五个参数的函数。 您可能希望修复第二个和第五个参数,然后部分应用将生成一个新版本的函数,该函数修复了这两个参数,但保留其他三个参数供新的调用使用。 如果您使用三个必需参数调用结果函数,它将通过使用原来的两个固定参数加上新提供的三个参数,生成正确的答案。
The idea of specifying only some of the parameters in function application, producing a function of the remaining parameters, is called projection: you are said to be projecting the function onto the remaining arguments. We will not use this term, but I wanted to cite it, just in case you happen to find it somewhere else.
让我们考虑一个使用fetch()
API 的例子,它被广泛认为是 Ajax 调用的现代方式。 您可能希望获取多个资源,始终为调用指定相同的参数(例如,请求头),只更改 URL 以进行搜索。 因此,通过使用部分应用,您可以创建一个始终提供固定参数的新myFetch()
函数。
You can read more on fetch()
at https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch. According to http://caniuse.com/#search=fetch, you can use it in most browsers, except for (oh, surprise!) Internet Explorer, but you can get around this limitation with a polyfill, such as the one found at https://github.com/github/fetch.
让我们假设我们有一个实现这种应用的partial()
函数,看看我们如何使用它来生成我们的fetch()
新版本:
const myParameters = {
method: "GET",
headers: new Headers(),
cache: "default"
};
const myFetch = partial(fetch, undefined, myParameters);
// *undefined means the first argument for fetch is not yet defined*
// *the second argument for fetch() is set to myParameters*
myFetch("a/first/url")
.then(/* do something */)
.catch(/* on error */);
myFetch("a/second/url")
.then(/* do something else */)
.catch(/* on error */);
如果请求参数是fetch()
的第一个参数,那么 curry 就可以工作。 (稍后我们将详细讨论参数的顺序。) 在部分应用中,您可以替换任何参数,无论是哪个参数,所以在本例中,myFetch()
最终是一个一元函数。 这个新函数将从您希望的任何 URL 获取数据,并始终为GET
操作传递相同的参数集。
带有箭头函数的部分应用
试着用手工来做部分应用,就像我们做咖喱一样,太复杂了。 例如,对于一个有 5 个参数的函数,您必须编写代码,允许用户提供 32 种可能的固定和非固定参数组合中的任何一种,32 等于 2 的 5 次方。 而且,即使您可以简化这个问题,它仍然很难编写和维护。 图 7.2是多种可能组合中的一种:
Figure 7.2: Partial application may let you first provide some parameters, and then provide the rest, to finally get the result.
然而,使用箭头函数执行部分应用要简单得多。 对于前面提到的示例,我们将得到如下代码。 在本例中,我们假设要将第二个参数固定为22
,将第五个参数固定为1960
:
const nonsense = (a, b, c, d, e) => `${a}/${b}/${c}/${d}/${e}`;
const fix2and5 = (a, c, d) => nonsense(a, 22, c, d, 1960);
以这种方式执行部分应用非常简单,尽管我们可能希望找到一个更通用的解决方案。 您可以设置任意数量的参数,方法是在前一个函数的基础上创建一个新函数,但修改更多的参数。 (可使用前面第 6 章、中的包装器。) 例如,您现在可能还想将新fix2and5()
函数的最后一个参数修改为9
,如下面的代码所示; 没有什么更简单:
const fixLast = (a, c) => fix2and5(a, c, 9);
如果您愿意,您也可以编写nonsense(a, 22, c, 9, 1960)
,但事实仍然是使用箭头函数固定参数很简单。 现在我们考虑一个更一般的解。
使用 eval()进行部分应用
如果我们想要对任意参数组合进行局部应用修正,我们必须有一种方法来指定哪些参数是自由的,哪些参数从那一点起是固定的。 一些库,如下划线或 Lodash,使用特殊对象_
来表示省略的参数。 按照这种方式,仍然使用相同的nonsense()
函数,我们可以这样写:
const fix2and5 = _.partial(nonsense, _, 22, _, _, 1960);
我们可以做同样的事情,通过一个全局变量来表示一个挂起的,还没有固定的参数,但是让我们让它更简单,只写undefined
来表示一个丢失的参数。
When checking for undefined
, remember to always use the ===
operator; with ==
, it happens that null==undefined
, and you don't want that. See https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/undefined for more on this.
我们希望编写一个函数,它将部分地应用一些参数,其余的留给将来使用。 我们想编写类似于下面的代码,并生成一个新的函数,就像我们之前用箭头函数所做的那样:
const nonsense = (a, b, c, d, e) => `${a}/${b}/${c}/${d}/${e}`;
const fix2and5 = partialByEval(
nonsense,
undefined,
22,
undefined,
undefined,
1960
);
// *fix2and5 would become* (X0, X2, X3) => nonsense(X0, 22, X2, X3, 1960);
我们可以回到使用eval()
,并计算出如下内容:
const range = (start, stop) =>
new Array(stop - start).fill(0).map((v, i) => start + i);
const partialByEval = (fn, ...args) => {
const rangeArgs = range(0, fn.length);
const leftList = rangeArgs
.map(v => (args[v] === undefined ? `x${v}` : null))
.filter(v => !!v)
.join(",");
const rightList = rangeArgs
.map(v => (args[v] === undefined ? `x${v}` : args[v]))
.join(",");
return eval(`(${leftList}) => ${fn.name}(${rightList})`);
};
让我们一步一步地分解这个函数。 再一次,我们使用我们的range()
函数:
rangeArgs
是一个数组,从 0 到(但不包括)输入函数中的参数数。-
leftList
是一个字符串,表示未应用的变量列表。 在我们的示例中,它应该是"X0,X2,X3"
,因为我们为第二个和第五个参数提供了值。 这个字符串将用于生成箭头函数的左边部分。 -
rightList
是一个字符串,表示调用所提供函数的参数列表。 在我们的例子中,它是"X0,'Z',X2,X3,1960"
。 我们将使用这个字符串来生成箭头函数的右边部分。
在生成了这两个列表之后,代码的其余部分只包括生成适当的字符串并将其交给eval()
以返回一个函数。
If we were doing partial application on a function with a variable number of arguments, we could have substituted args.length
for fn.length
, or provided an extra (optional) parameter with the number to use, as we did in the Currying section of this chapter.
顺便说一下,我特意用这么长的方式来表示这个函数,让它更清楚。 (当我们使用eval()
进行 curry 操作时,我们已经看到了一些类似但更短的代码。) 但是,请注意,您也可能会找到更短、更紧凑、更晦涩的版本,而正是这种代码给 FP 带来了坏名声! 我们的新版本代码可以是:
const partialByEval2 = (fn, ...args) =>
eval(
`(${range(0, fn.length)
.map(v => (args[v] === undefined ? `x${v}` : null))
.filter(v => !!v)
.join(",")}) => ${fn.name}(${range(0, fn.length)
.map(v => (args[v] == undefined ? `x${v}` : args[v]))
.join(",")})`
);
让我们通过编写一些测试来完成这一节。 以下是我们应该考虑的一些事情:
- 当我们做部分应用时,所产生的函数的性质应该减小。
- 当参数的顺序正确时,应该调用原始函数。
我们可以像下面这样写,允许在不同的地方修改参数。 而不是使用间谍或 mock,我们可以直接使用我们拥有的nonsense()
功能,因为它非常有效:
const nonsense = (a, b, c, d, e) => `${a}/${b}/${c}/${d}/${e}`;
describe("with partialByEval()", function() {
it("you could fix no arguments", () => {
const nonsensePC0 = partialByEval(nonsense);
expect(nonsensePC0.length).toBe(5);
expect(nonsensePC0(0, 1, 2, 3, 4)).toBe(nonsense(0, 1, 2, 3, 4));
});
it("you could fix only some initial arguments", () => {
const nonsensePC1 = partialByEval(nonsense, 1, 2, 3);
expect(nonsensePC1.length).toBe(2);
expect(nonsensePC1(4, 5)).toBe(nonsense(1, 2, 3, 4, 5));
});
it("you could skip some arguments", () => {
const nonsensePC2 = partialByEval(
nonsense,
undefined,
22,
undefined,
44
);
expect(nonsensePC2.length).toBe(3);
expect(nonsensePC2(11, 33, 55)).toBe(nonsense(11, 22, 33, 44, 55));
});
it("you could fix only some last arguments", () => {
const nonsensePC3 = partialByEval(
nonsense,
undefined,
undefined,
undefined,
444,
555
);
expect(nonsensePC3.length).toBe(3);
expect(nonsensePC3(111, 222, 333)).toBe(
nonsense(111, 222, 333, 444, 555)
);
});
it("you could fix ALL the arguments", () => {
const nonsensePC4 = partialByEval(nonsense, 6, 7, 8, 9, 0);
expect(nonsensePC4.length).toBe(0);
expect(nonsensePC4()).toBe(nonsense(6, 7, 8, 9, 0));
});
});
我们写了一个部分应用高阶函数,但它没有我们想要的那么灵活。 例如,我们可以在第一个实例中修复一些参数,但随后必须在下一次调用中提供所有其余的参数。 它会更好,如果调用partialByEval()
后,我们得到了一个新的函数,如果我们没有提供所有必需的参数,我们可以得到另一个函数,而另一个,等等,直到所有参数信息并不够了的发生与局部套用。 所以,让我们改变我们做部分应用的方式,考虑另一个解决方案。
使用闭包的部分应用
让我们研究另一种执行部分应用的方法,即使用闭包。 (你可能想在第 1 章,Becoming Functional - Several Questions中讨论这个话题。) 这种执行部分应用的方式会让人想起我们在本章前面写的curry()
函数,并解决了我们在上一节末尾提到的缺乏灵活性的问题。 我们的新措施如下:
const partialByClosure = (fn, ...args) => {
const partialize = (...args1) => (...args2) => {
for (let i = 0; i < args1.length && args2.length; i++) {
if (args1[i] === undefined) {
args1[i] = args2.shift();
}
}
const allParams = [...args1, ...args2];
return (allParams.includes(undefined) || allParams.length < fn.length
? partialize
: fn)(...allParams);
};
return partialize(...args);
};
哇——有点长的代码! 关键是内部的partialize()
功能。 给定一个参数列表(args1
),它生成一个函数,该函数接收第二个参数列表(args2
):
- 首先,它用来自
args2
的值替换args1
中所有可能的未定义值。 - 然后,如果在
args2
中还有任何参数,它也将这些参数添加到args1
的参数中,生成allParams
。 - 最后,如果参数列表不包含任何未定义的值,并且足够长,则调用原始函数。
- 否则,它将自己局部化,以等待更多参数。
举个例子会更清楚。 让我们回到我们信任的make3()
函数,并构建它的部分版本:
const make3 = (a, b, c) => String(100 * a + 10 * b + c);
const f1 = partialByClosure(make3, undefined, 4);
现在让我们写第二个函数:
const f2 = f1(7);
会发生什么呢? 原始的参数列表([undefined, 4]
)与新列表(单个元素——在本例中是[7]
)合并,生成一个函数,该函数现在接收7
和4
作为它的前两个参数。 然而,这还没有准备好,因为原始函数需要三个参数。 我们可以这样写:
const f3 = f2(9);
然后,当前参数列表将与新参数合并,生成[7,4,9]
。 由于现在列表已经完成,将对原始函数进行计算,生成749
作为最终结果。
这段代码的结构与我们之前写的其他高阶函数有重要的相似之处,在curcurry withbind()部分:
-
如果提供了所有的参数,则调用原始函数。
-
否则,如果一些参数仍然是必需的(当 curry 时,这只是一个计数参数的问题; 当执行部分应用时,您还必须考虑有一些未定义的参数的可能性),高阶函数调用自己产生一个新版本的函数,将等待丢失的参数。
让我们通过编写一些测试来结束,这些测试将展示我们在执行部分应用的新方法中所做的改进。 基本上,我们之前做的所有测试都可以工作,但我们还必须尝试按顺序应用参数,因此我们应该在两个或多个应用步骤之后得到最终结果。 然而,由于我们现在可以用任意数量的参数调用我们的中间函数,我们不能测试它们的一致性:对于所有这些中间函数,我们得到function.length===0
。 我们的测试可以如下:
describe("with partialByClosure()", function() {
it("you could fix no arguments", () => {
const nonsensePC0 = partialByClosure(nonsense);
expect(nonsensePC0(0, 1, 2, 3, 4)).toBe(nonsense(0, 1, 2, 3, 4));
});
it("you could fix only some initial arguments, and then some more", () => {
const nonsensePC1 = partialByClosure(nonsense, 1, 2, 3);
const nonsensePC1b = nonsensePC1(undefined, 5);
expect(nonsensePC1b(4)).toBe(nonsense(1, 2, 3, 4, 5));
});
it("you could skip some arguments", () => {
const nonsensePC2 = partialByClosure(
nonsense,
undefined,
22,
undefined,
44
);
expect(nonsensePC2(11, 33, 55)).toBe(nonsense(11, 22, 33, 44, 55));
});
it("you could fix only some last arguments", () => {
const nonsensePC3 = partialByClosure(
nonsense,
undefined,
undefined,
undefined,
444,
555
);
expect(nonsensePC3(111)(222, 333)).toBe(
nonsense(111, 222, 333, 444, 555)
);
});
it("you could simulate currying", () => {
const nonsensePC4 = partialByClosure(nonsense);
expect(nonsensePC4(6)(7)(8)(9)(0)).toBe(nonsense(6, 7, 8, 9, 0));
});
it("you could fix ALL the arguments", () => {
const nonsensePC5 = partialByClosure(nonsense, 16, 17, 18, 19, 20);
expect(nonsensePC5()).toBe(nonsense(16, 17, 18, 19, 20));
});
});
代码比以前更长了,但是测试本身很容易理解。 顺便说一下,倒数第二个测试应该会提醒你做咖喱! 我们现在已经看到了如何进行 curry 和局部应用。 让我们以一种混合的方法——部分 curry 来结束这一章,它包括了这两种技术的各个方面。
部分局部套用
我们要看的最后一个变换是一种 curry 和局部应用的混合。 如果你谷歌它,在一些地方,你会发现它叫做鞭笞,在别人,部分应用,但凑巧的是,它既不符合,所以我坐在栅栏,称之为部分局部套用!**
它的思想是,给定一个函数,修复它的前几个参数,并生成一个新函数来接收其余的参数。 但是,如果给这个新函数的参数更少,它将修复给它的任何参数,并生成一个新的函数来接收其余的参数,直到给出了所有的参数并可以计算出最终结果。 参见图 7.3:
Figure 7.3: Partial currying is a mixture of currying and partial application. You may provide arguments from the left, in any quantity, until all have been provided, and then the result is calculated.
为了查看一个示例,让我们回到在前面几节中使用的nonsense()
函数,如下所示。 假设我们已经有了一个partialCurry()
函数:
const nonsense = (a, b, c, d, e) => `${a}/${b}/${c}/${d}/${e}`;
const pcNonsense = partialCurry(nonsense);
const fix1And2 = pcNonsense(9, 22); // fix1And2 is now a ternary function
const fix3 = fix1And2(60); // fix3 is a binary function
const fix4and5 = fix3(12, 4); // fix4and5 === nonsense(9,22,60,12,4), "9/22/60/12/4"
原功能具有5
的特性。 当我们将部分 curry这个函数,并给它设置9
和22
参数时,它就变成了一个三元函数,因为在原来的 5 个参数中,有两个是固定的。 如果我们取这个三元函数并给它一个参数(60
),结果是另一个函数:在本例中,是一个二进制函数,因为现在我们已经固定了最初的五个参数中的前三个。 最后一个调用提供最后两个参数,然后执行实际计算所需结果的工作。
卷曲与局部应用有一些共同之处,但也有一些不同之处,如下:
- 原始函数被转换成一系列函数,每个函数都产生下一个函数,直到最后一个函数执行它的计算。
- 您总是提供从第一个(最左边的一个)开始的参数,如 curry,但您可以提供多个参数,如部分应用。
- 当对一个函数进行 curry 时,所有的中间函数都是一元的,但是局部 curry 则不需要这样。 但是,如果在每个实例中提供一个参数,那么结果将需要与普通 curry 一样多的步骤。
好了,我们有了定义,现在我们来看看如何实现新的高阶函数; 在本章中,我们可能会重复使用前几节中的一些概念。
使用 bind()进行部分 curry
和我们做咖喱的方法类似,有一种简单的方法来做部分咖喱。 我们将利用这个事实,bind()
实际上可以一次性解决许多争论:
const partialCurryingByBind = fn =>
fn.length === 0
? fn()
: (...pp) => partialCurryingByBind(fn.bind(null, ...pp));
将代码与之前的curryByBind()
函数进行比较,你会看到非常小的差异:
const curryByBind = fn =>
fn.length === 0
? fn()
: p => curryByBind(fn.bind(null, p));
机理是完全一样的。 唯一的区别是,在我们的新函数中,我们可以同时绑定多个参数,而在curryByBind()
中,我们总是只绑定一个参数。 我们可以回顾前面的例子,唯一的区别是我们可以用更少的步骤得到最终结果:
const make3 = (a, b, c) => String(100 * a + 10 * b + c);
const f1 = partialCurryingByBind(make3);
const f2 = f1(6, 5); // *f2 is a function, that fixes make3's first two arguments*
const f3 = f2(8); // *"658" is calculated, since there are no more parameters to fix*
顺便说一下,为了了解现有的可能性,你可以在 curry 的时候修复一些参数,如下所示:
const g1 = partialCurryingByBind(make3)(8, 7);
const g2 = g1(6); // "876"
测试这个函数很容易,我们提供的示例是一个很好的起点。 但是请注意,由于我们允许固定任意数量的参数,因此不能测试中间函数的性质。 我们的测试可以如下所示:
const make3 = (a, b, c) => String(100 * a + 10 * b + c);
describe("with partialCurryingByBind", function() {
it("you could fix arguments in several steps", () => {
const make3a = partialCurryingByBind(make3);
const make3b = make3a(1, 2);
const make3c = make3b(3);
expect(make3c).toBe(make3(1, 2, 3));
});
it("you could fix arguments in a single step", () => {
const make3a = partialCurryingByBind(make3);
const make3b = make3a(10, 11, 12);
expect(make3b).toBe(make3(10, 11, 12));
});
it("you could fix ALL the arguments", () => {
const make3all = partialCurryingByBind(make3);
expect(make3all(20, 21, 22)).toBe(make3(20, 21, 22));
});
it("you could fix one argument at a time", () => {
const make3one = partialCurryingByBind(make3)(30)(31)(32);
expect(make3one).toBe(make3(30, 31, 32));
});
});
现在,让我们考虑具有可变参数数量的函数。 和以前一样,我们必须提供一个额外的值,我们将得到以下实现:
const partialCurryingByBind2 = (fn, len = fn.length) =>
len === 0
? fn()
: (...pp) =>
partialCurryingByBind2(
fn.bind(null, ...pp),
len - pp.length
);
我们可以用一种简单的方法来尝试这个方法,回顾本章前面的 curry 例子,但现在使用部分 curry,如下所示:
const sum = (...args) => args.reduce((x, y) => x + y, 0);
pcSum5 = partialCurryingByBind2(sum2, 5);
// curriedSum5 will expect 5 parameters
pcSum5(1, 5)(3)(7, 4); // 20
当我们用参数(1
,5
)调用新的pcSum5()
函数时,它生成了一个新函数,这个新函数还需要三个参数。 为它提供一个参数(3
),创建了第三个函数,以等待最后两个参数。 最后,当我们向最后一个函数提供最后两个值(7
,4
)时,调用原始函数,计算结果(20)。
我们还可以添加一些测试来替代部分咖喱的方法:
const sum2 = (...args) => args.reduce((x, y) => x + y, 0);
describe("with partialCurryingByBind2", function() {
it("you could fix arguments in several steps", () => {
const suma = partialCurryingByBind2(sum2, 3);
const sumb = suma(1, 2);
const sumc = sumb(3);
expect(sumc).toBe(sum2(1, 2, 3));
});
it("you could fix arguments in a single step", () => {
const suma = partialCurryingByBind2(sum2, 4);
const sumb = suma(10, 11, 12, 13);
expect(sumb).toBe(sum(10, 11, 12, 13));
});
it("you could fix ALL the arguments", () => {
const sumall = partialCurryingByBind2(sum2, 5);
expect(sumall(20, 21, 22, 23, 24)).toBe(sum2(20, 21, 22, 23, 24));
});
it("you could fix one argument at a time", () => {
const sumone = partialCurryingByBind2(sum2, 6)(30)(31)(32)(33)(34)(35);
expect(sumone).toBe(sum2(30, 31, 32, 33, 34, 35));
});
});
尝试不同的特质比只坚持一种要好,所以我们这么做是为了多样化。
使用闭包进行局部 curry
对于部分应用,有一种解决方案可以使用闭包。 既然我们已经讨论了许多必要的细节,让我们直接进入代码:
const partialCurryByClosure = fn => {
const curryize = (...args1) => (...args2) => {
const allParams = [...args1, ...args2];
return (allParams.length < func.length ? curryize : fn)(
...allParams
);
};
return curryize();
};
如果你比较partialCurryByClosure()
和partialByClosure()
,主要的区别在于,与部分局部套用,因为我们总是从左边提供参数,并没有办法跳过一些,你连接任何参数你有新的,并检查是否有足够的。 如果新的参数列表达到了原始函数的预期值,则可以调用它并获得最终结果。 在其他情况下,您只需使用curryize()
来获得一个新的中间函数,它将等待更多参数。
如前所述,如果必须处理带有不同数量参数的函数,可以为部分 curry 函数提供一个额外的参数:
const partialCurryByClosure2 = (fn, len = fn.length) => {
const curryize = (...args1) => (...args2) => {
const allParams = [...args1, ...args2];
return (allParams.length < len ? curryize : fn)(...allParams);
};
return curryize();
};
结果与上一节中的Partial curcurry with bind()完全相同,所以不值得重复它们。 您也可以很容易地更改我们编写的测试,使用partialCurryByClosure()
而不是partialCurryByBind()
,它们将工作。
最终的想法
让我们以另外两个关于 curry 和局部应用的哲学考虑来结束本章,这可能会引起一些讨论:
- 首先,许多库的参数顺序是错误的,这使得它们更难使用。
- 第二,在本章中,我通常不使用高阶函数,而是使用更简单的 JavaScript 代码!
这可能不是你现在所期待的,所以让我们更详细地过一下这两点,这样你就会看到,这不是一个做我说的事情,而不是做的事情…… 或者,就像图书馆一样!
参数的顺序
有一个问题,不仅是下划线或 LoDash 的_.map(list, mappingFunction)
或_.reduce(list, reducingFunction, initialValue)
这样的函数,而且是我们在本书中产生的一些函数,例如demethodize()
的结果,都存在这个问题。 (参见第 6 章Demethodizing - turning methods into functions章节、Producing functions—high -order functions,来复习一下这个高阶函数。) 问题是,参数的顺序并不能真正帮助咖喱。
在对函数进行 curry 处理时,您可能希望存储中间结果。 当我们在下面的代码中做类似的事情时,我们假设您将重用带有固定参数的 curry 函数,这意味着原始函数的第一个参数变化的可能性最小。 现在让我们考虑一个具体的例子。 回答这个问题:您更有可能使用map()
将同一个函数应用到几个不同的数组,还是将几个不同的函数应用到同一个数组? 对于验证或转换,前者更有可能,但这不是我们得到的!
我们可以编写一个简单的函数来反转二元函数的参数,如下所示:
const flipTwo = fn => (p1, p2) => fn(p2, p1);
Note that even if the original fn()
function could receive more or fewer arguments, after applying flipTwo()
to it, the arity of the resulting function will be fixed to 2. We will be taking advantage of this fact in the following section.
这样,您就可以编写如下代码:
const myMap = curry(flipTwo(demethodize(map)));
const makeString = v => String(v);
const stringify = myMap(makeString);
let x = stringify(anArray);
let y = stringify(anotherArray);
let z = stringify(yetAnotherArray);
最常见的情况是,您希望将该函数应用于几个不同的列表,而库函数和我们自己的反方法函数都没有提供这种功能。 然而,通过使用flipTwo()
,我们可以以我们喜欢的方式工作。
In this particular case, we might have solved our problem by using partial application instead of currying, because with that we could fix the second argument to map()
without any further bother. However, flipping arguments to produce new functions that have a different order of parameters is also an often-used technique, and it's important that you're aware of it.
对于像reduce()
这样的情况,它通常接收三个参数(列表、函数和初始值),我们可以这样选择:
const flip3 = fn => (p1, p2, p3) => fn(p2, p3, p1);
const myReduce = partialCurry(flip3(demethodize(Array.prototype.reduce)));
const sum = (x, y) => x + y;
const sumAll = myReduce(sum, 0);
sumAll(anArray);
sumAll(anotherArray);
在这里,我们使用部分 curry 来简化sumAll()
的表达式。 另一种选择是使用普通的咖喱,然后我们将定义sumAll = myReduce(sum)(0)
。
如果愿意,还可以使用更深奥的参数重排函数,但通常只需要这两个函数。 对于非常复杂的情况,您可以选择使用箭头函数(正如我们在定义flipTwo()
和flip3()
时所做的那样),并明确您需要哪种类型的重新排序。
在功能
现在我们已经接近本章的结尾,有一点需要承认:我并不总是像上面所示的那样使用 curry 和局部应用! 不要误解我的意思,我do应用了这些技术,但有时它会使代码更长,更不清晰,而不一定是更好的。 让我告诉你我在说什么。
如果我在写我自己的函数,然后我想 curry 它来修正第一个参数,curry(或部分应用,或部分 curry)与箭头函数相比并没有什么区别。 我必须写下以下内容:
const myFunction = (a, b, c) => { ... };
const myCurriedFunction = curry(myFunction)(fixed_first_argument);
// *and later in the code...*
myCurriedFunction(second_argument)(third_argument);
对函数进行 curry 处理,并在同一行中给出第一个参数,可能会被认为不是很清楚; 另一种方法需要添加一个变量和多一行代码。 以后,未来的召唤也不太好; 然而,局部 curry 使它更简单:myPartiallyCurriedFunction(second_argument, third_argument)
。 在任何情况下,当我将最终代码与箭头函数的使用进行比较时,我认为其他解决方案并没有更好; 对样品进行自己的评价如下:
const myFunction = (a, b, c) => { ... };
const myFixedFirst = (b, c) => myFunction(fixed_first_argument, b, c);
// *and later...*
myFixedFirst(second_argument, third_argument);
我认为 curry 和部分应用是很好的,在我的小库中,去方法的,预先 curry 好的,基本的高阶函数。 我有自己的一组函数,如:
const _plainMap = demethodize(Array.prototype.map);
const myMap = curry(_plainMap, 2);
const myMapX = curry(flipTwo(_plainMap));
const _plainReduce = demethodize(Array.prototype.reduce);
const myReduce = curry(_plainReduce, 3);
const myReduceX = curry(flip3(_plainReduce));
const _plainFilter = demethodize(Array.prototype.filter);
const myFilter = curry(_plainFilter, 2);
const myFilterX = curry(flipTwo(_plainFilter));
// *...and more functions in the same vein*
以下是关于代码需要注意的几点:
- 我有这些功能在一个单独的模块,我只导出
myXXX()
命名的。 - 其他函数都是私有的,我使用前导下划线来提醒自己。
- 我使用
my...
前缀来记住这些是我的函数,而不是普通的 JavaScript 函数。 有些人宁愿保留标准的名字,如map()
或filter()
,但我更喜欢不同的名字。 - 由于大多数 JavaScript 方法都有一个变量,所以我必须在 curry 时指定它。
- 我总是将第三个参数(reduce 的初始值)提供为
reduce()
,因此我为该函数选择的度为 3。 - 当 curry 翻转函数时,你不需要指定参数的数量,因为翻转已经为你完成了。
最后,一切都归结为一个个人决定; 尝试一下我们在本章看过的技巧,看看你更喜欢哪一种!
总结
在本章中,我们考虑了一种生成函数的新方法,通过几种不同的方法将参数固定在一个现有的函数上:curry 法——最初源自计算机理论; 局部应用,更灵活; 局部卷曲,结合了前面两种方法的优点。 使用这些转换,可以简化代码,因为可以生成通用函数的更专门化版本,没有任何麻烦。
在第八章,连接功能——流水线和作文,我们将回到一些概念我们看在纯函数这一章,我们将考虑的方式确保功能不能成为不洁净的偶然,通过寻求方法让他们的论点不可变的, 使它们不可能变异。
问题
7.1。 下面的练习将帮助你理解我们在本章中讨论的一些概念,即使你不使用我们看过的任何函数来解题。 编写一个sumMany()
函数,让你按下列方式对一个不确定的数求和。 注意,当不带参数调用函数时,将返回 sum:
let result = sumMany((9)(2)(3)(1)(4)(3)());
// *22*
7.2。 :编写一个applyStyle()
函数,让您按照以下方式对字符串应用基本样式。 使用卷曲或部分应用:
const makeBold = applyStyle("b");
document.getElementById("myCity").innerHTML =
makeBold("Montevideo");
// <b>Montevideo</b>, *to produce* Montevideo
const makeUnderline = applyStyle("u");
document.getElementById("myCountry").innerHTML =
makeUnderline("Uruguay");
// <u>Uruguay</u>, *to produce* Uruguay
7.3。 通过原型进行 curry:修改Function.prototype
以提供一个curry()
方法,其工作原理与本章中看到的curry()
函数类似。 完成以下代码应该会产生以下结果:
Function.prototype.curry = function() {
// ...*your code goes here...*
};
const sum3 = (a, b, c) => 100 * a + 10 * b + c;
sum3.curry()(1)(2)(4); // *124*
const sum3C = sum3.curry()(2)(2);
sum3C(9); // *229*
7.4。 对经过 curry 的函数进行解 curry:编写一个unCurry(fn,arity)
函数,该函数接收一个(curry 过的)函数及其期望的值作为参数,并返回一个未经 curry 过的fn()
函数; 也就是说,一个函数将接收n参数并产生一个结果(提供预期的精度是必需的,因为您无法自行确定它):
const make3 = (a, b, c) => String(100 * a + 10 * b + c);
const make3c = curry(make3);
console.log(make3c(1)(2)(3)); // 123
const remake3 = uncurry(make3c, 3);
console.log(remake3(1, 2, 3)); // 123
7.5。 :What does the following function, purpose to write in an unhelpful way, actually do?
const what = who => (...why) =>
who.length <= why.length
? who(...why)
: (...when) => what(who)(...why, ...when);
7.6。 再来点咖喱! 这是另一个关于curry()
功能的建议:你能看到它为什么工作吗? 提示:代码与我们在本章中看到的内容相关:
js
const curry = fn => (...args) =>
args.length >= fn.length ? fn(...args) : curry(fn.bind(null, ...args));
****
版权属于:月萌API www.moonapi.com,转载请注明出处