首先,感谢您维护的函数式编程库。我一直想自己写一篇,但一直没有时间。
考虑到您正在编写函数式编程库这一事实,我假设您了解 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))。因此,根据principle of least astonishment,这是应该实现的方式。
现在是左侧部分的问题。如果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 在 OOP 中会写成 list.elem(n)。 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))),那将是很棒的。这很困难,但可行。