首先,感谢您正在维护的函数式编程库。我一直想自己写一篇,但一直没有时间去做。
考虑到您正在编写函数式编程库这一事实,我假设您了解 Haskell。在 Haskell 中,我们有函数和运算符。函数总是前缀。运算符总是中缀。
Haskell 中的函数可以使用反引号转换为运算符。例如div 6 3
可以写成6 `div` 3
。类似地,可以使用括号将运算符转换为函数。例如2 < 3
可以写成(<) 2 3
。
运算符也可以使用部分来部分应用。有两种类型的部分:左侧部分(例如(2 <)
和(6 `div`)
)和右侧部分(例如(< 3)
和(`div` 3)
)。左侧部分翻译如下:(2 <)
变为(<) 2
。右部分:(< 3)
变为flip (<) 3
.
在 JavaScript 中,我们只有函数。在 JavaScript 中创建运算符没有“好”的方法。您可以编写类似 的代码(2).lt(3)
,但在我看来这是不礼貌的,我强烈建议不要编写这样的代码。
因此,我们可以将普通函数和运算符编写为函数:
div(6, 3) // normal function: div 6 3
lt(2, 3) // operator as a function: (<) 2 3
在 JavaScript 中编写和实现中缀运算符是一件痛苦的事情。因此,我们不会有以下内容:
(6).div(3) // function as an operator: 6 `div` 3
(2).lt(3) // normal operator: 2 < 3
但是部分很重要。让我们从正确的部分开始:
div(3) // right section: (`div` 3)
lt(3) // right section: (< 3)
当我看到时,div(3)
我希望它是一个正确的部分(即它应该表现为(`div` 3)
)。因此,根据最小惊讶原则,这是它应该实现的方式。
现在是左部分的问题。如果div(3)
是右侧部分,那么左侧部分应该是什么样子?在我的拙见中,它应该是这样的:
div(6, _) // left section: (6 `div`)
lt(2, _) // left section: (2 <)
对我来说,这读作“6除以某物”和“2比某物小吗?” 我更喜欢这种方式,因为它是明确的。根据The Zen of Python 的说法,“显式胜于隐式”。
那么这对现有代码有何影响?例如,考虑filter
函数。要过滤列表中的奇数,我们将编写filter(odd, list)
. 对于这样的功能,柯里化是否按预期工作?例如,我们将如何编写一个filterOdd
函数?
var filterOdd = filter(odd); // expected solution
var filterOdd = filter(odd, _); // left section, astonished?
根据最小惊讶的原则,它应该是简单的filter(odd)
。该filter
函数不打算用作运算符。因此,不应强迫程序员将其用作左侧部分。函数和“函数运算符”之间应该有明显的区别。
幸运的是,区分函数和函数运算符非常直观。例如,filter
函数显然不是函数运算符:
filter odd list -- filter the odd numbers from the list; makes sense
odd `filter` list -- odd filter of list? huh?
另一方面,该elem
函数显然是一个函数运算符:
list `elem` n -- element n of the list; makes sense
elem list n -- element list, n? huh?
重要的是要注意这种区别是唯一可能的,因为函数和函数运算符是互斥的。按理说,给定一个函数,它可能是普通函数,也可能是函数运算符,但不能同时是两者。
有趣的是,给定一个二元函数,如果flip
它的参数是二元运算符,反之亦然。例如,考虑 和 的翻转变filter
体elem
:
list `filter` odd -- now filter makes sense an an operator
elem n list -- now elem makes sense as a function
事实上,如果 n 大于 1,这可以推广到任何 n 元函数。你看,每个函数都有一个主参数。简单地说,对于一元函数,这种区别是无关紧要的。然而,对于非一元函数,这种区别很重要。
- 如果函数的主要参数出现在参数列表的末尾,则该函数是一个普通函数(例如
filter odd list
,list
主要参数在哪里)。将主要参数放在列表末尾是函数组合所必需的。
- 如果函数的主要参数出现在参数列表的开头,则该函数是一个函数运算符(例如
list `elem` n
,list
主要参数在哪里)。
- 运算符类似于 OOP 中的方法,主要参数类似于方法的对象。例如
list `elem` n
,将按照list.elem(n)
OOP 编写。OOP 中的链接方法类似于 FP [1]中的函数组合链。
- 函数的主要参数只能在参数列表的开头或结尾。在其他任何地方都没有意义。这个属性对于二元函数来说是完全正确的。因此,翻转二进制函数使它们成为运算符,反之亦然。
- 其余参数与函数一起形成一个不可分割的原子,称为参数列表的主干。例如在
filter odd list
词干是filter odd
。在list `elem` n
茎是(`elem` n)
。
- 词干的顺序和元素必须保持不变,表达式才有意义。这就是为什么
odd `filter` list
而且elem list n
没有任何意义。然而list `filter` odd
并且elem n list
有意义,因为词干没有改变。
回到主题,因为函数和函数运算符是互斥的,你可以简单地对待函数运算符,而不是对待普通函数。
我们希望运营商具有以下行为:
div(6, 3) // normal operator: 6 `div` 3
div(6, _) // left section: (6 `div`)
div(3) // right section: (`div` 3)
我们要定义运算符如下:
var div = op(function (a, b) {
return a / b;
});
函数的定义op
很简单:
function op(f) {
var length = f.length, _; // we want underscore to be undefined
if (length < 2) throw new Error("Expected binary function.");
var left = R.curry(f), right = R.curry(R.flip(f));
return function (a, b) {
switch (arguments.length) {
case 0: throw new Error("No arguments.");
case 1: return right(a);
case 2: if (b === _) return left(a);
default: return left.apply(null, arguments);
}
};
}
该op
函数类似于使用反引号将函数转换为 Haskell 中的运算符。因此,您可以将其添加为 Ramda 的标准库函数。在文档中还提到,运算符的主要参数应该是第一个参数(即它应该看起来像 OOP,而不是 FP)。
[1]顺便说一句,如果 Ramda 允许您像在常规 JavaScript 中链接方法一样编写函数(例如foo(a, b).bar(c)
,而不是compose(bar(c), foo(a, b))
),那将是很棒的。这很困难,但可行。