我已经在一个名为 Ramda 的Javascript FP库上工作了一段时间,而且我在命名方面遇到了一些问题。 (你已经听过旧行了,对吗?"计算机科学中只有两个难题:缓存失效,命名事物和逐个错误。")
在这个库中,(几乎)多个参数的每个函数都会自动进行curry。这适用于大多数用例。但是有一些问题是一些函数是非交换二元运算符。问题在于,英语名称往往意味着与应用currying时所发生的不同。例如,
var div10 = divide(10);
声音就像它应该是一个将其参数除以10的函数。但实际上它将参数划分为 10,如果你看一下这很明显定义:
var divide = curry(function(a, b) {
return a / b;
});
所以改为预期:
div10(50); //=> 5 // NO!!
事实上,你得到了
div10(50); //=> 0.2 // Correct, but surprising!
我们通过记录人们可能的期望差异来创建divideBy
,这只是flip(divide)
和subtractN
,即flip(subtract)
。但我们还没有发现lt
R.lt = curry(function(a, b) {
return a < b;
});
或其堂兄lte
,gt
和gte
。
我自己的直觉就是那个
map(lt(5), [8, 6, 7, 5, 3, 0, 9]);
//=> [false, false, false, false, true, true, false]
但当然,它实际上会返回
//=> [true, true, true, false, false, false, true]
所以我想为lt
及其同类做同样的文档和点到备用名称例程。但我还没有找到一个好名字。唯一真正的候选人是ltVal
,并且在使用两个参数调用时并不能真正起作用。我们做了 discuss this issue ,但没有得出任何好结论。
让其他人处理这个并提出好的解决方案吗?或者即使没有,对这些函数的翻转版本的名称有什么好的建议吗?
更新
有人建议将其关闭,因为“不清楚你在问什么”,我猜这个问题确实 在解释中丢失了一点。简单的问题是:
lt
的翻转版本的直观名称是什么?
答案 0 :(得分:26)
首先,您要维护的函数式编程库值得称道。我一直想自己写一个,但我没有时间这么做。
考虑到你正在编写一个函数式编程库,我将假设您了解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中创建运算符没有“good”方法。您可以编写像(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 <)
对我而言,这就是“除以某种东西”和“是2比某些东西还小?”我更喜欢这种方式,因为它是明确的。根据{{3}},“明确比隐含更好。”
那么这对现有代码有何影响?例如,考虑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-arity函数,这可以推广。你会看到,每个函数都有一个主要参数。平凡地说,对于一元函数来说,这种区别是无关紧要的。然而,对于非一元函数,这种区别很重要。
filter odd list
,其中list
是主要参数)。在列表末尾有主要参数是函数组合所必需的。list `elem` n
,其中list
是主要参数。)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)
而不是{{1} })。这很难,但可行。
答案 1 :(得分:1)
我们都知道编程中的命名是严肃的事情,特别是涉及到咖喱形式的功能。使用Aadit在他的回答中做的程序化方法来处理这个问题是一个有效的解决方案。但是,我发现他的实施存在两个问题:
undefined
黑客才能实现Javascript没有curried运算符,因此没有左侧或右侧部分。一个惯用的Javascript解决方案应该考虑到这一点。
Curried函数没有arity的概念,因为每个函数调用只需要一个参数。您可以部分或完全应用curried函数而无需任何帮助程序:
const add = y => x => x + y;
const add2 = add(2); // partial application
add(2)(3); // complete application
通常函数的最后一个参数主要是一个,因为它是通过函数组合传递的(类似于允许方法链接的对象)。因此,当您部分应用函数时,您希望传递其初始参数:
const comp = f => g => x => f(g(x));
const map = f => xs => xs.map(x => f(x));
const inc = x => x + 1;
const sqr = x => x * x;
comp(map(inc)) (map(sqr)) ([1,2,3]); // [2,5,10]
在这方面,操作员功能是特殊的。它们是二进制函数,可将两个参数减少为单个返回值。由于并非每个运算符都是可交换的(a - b!== b - a),因此参数顺序很重要。因此,运算符函数没有主要参数。但人们习惯于根据应用程序的类型以某种方式读取表达式:
const concat = y => xs => xs.concat(y);
const sub = y => x => x - y;
// partial application:
const concat4 = concat(4);
const sub4 = sub(4);
concat4([1,2,3]); // [1,2,3,4] - OK
sub4(3); // -1 - OK
// complete application:
concat([1,2,3]) (4); // [4,1,2,3] - ouch!
sub(4) (3); // -1 - ouch!
我们使用翻转的参数定义了concat
和sub
,以便部分应用程序按预期工作。这显然不适用于完整的申请。
const flip = f => y => x => f(x) (y);
const concat_ = flip(concat);
const sub_ = flip(sub);
concat_(xs) (4); // [1,2,3,4] - OK
sub_(4) (3); // 1 - OK
concat_
和sub_
对应于Haskell中的左侧部分。请注意,像add
或lt
这样的函数运算符不需要左节版本,因为前者是可交换的,后者是谓词,它们具有逻辑对应物:
const comp2 = f => g => x => y => f(g(x) (y));
const map = f => xs => xs.map(x => f(x));
const flip = f => y => x => f(x) (y);
const not = x => !x;
const notf2 = comp2(not);
const lt = y => x => x < y;
const gt = flip(lt);
const lte = notf2(gt);
const gte = notf2(lt);
map(lt(5)) ([8, 6, 7, 5, 3, 0, 9]);
// [false, false, false, false, true, true, false]
map(gte(5)) ([8, 6, 7, 5, 3, 0, 9]);
// [true, true, true, true, false, false, true]
我们应该使用命名约定解决这个命名问题,然后使用程序化解决方案以非惯用方式扩展Javascript。命名约定并不理想......好吧,就像Javascript一样。