' lt ', ' lte ', ' gt '和' gte '的翻转版本的好名字



我已经在一个名为Ramda的Javascript FP库上工作了一段时间我在命名方面有点小问题。(你听过那句老话,对吧?"在计算机科学中只有两个难题:缓存无效、命名和离一错误。")

在这个库中,(几乎)每个有多个形参的函数都自动柯里化了。这对于大多数用例都很有效。但是一些非交换二元算子的函数存在一些问题。问题是,英文名字往往倾向于暗示一些不同的东西,当使用curry时所发生的事情。例如,

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,当使用两个参数调用时,它实际上不起作用。我们已经讨论过这个问题了,但没有好的结论。

其他人处理过这个问题并提出了好的解决方案吗?或者即使没有,对于这些函数的翻转版本的名称有什么好的建议吗?


更新

有人建议关闭这个,因为"不清楚你在问什么",我猜这个问题真的在解释中丢失了一点。简单的问题是:

对于翻转版的lt,一个好的、直观的名字会是什么?

首先,感谢您正在维护的函数式编程库。我一直想自己写一篇,但我一直没有时间去做。

考虑到你正在编写一个函数式编程库,我假设你知道Haskell。在Haskell中我们有函数和操作符。函数总是前缀的。操作符总是中缀。

Haskell中的函数可以使用反引号转换为操作符。例如,div 6 3可以写成6 `div` 3。类似地,可以使用括号将操作符转换为函数。例如,2 < 3可以写成(<) 2 3

操作符也可以部分地使用section。有两种类型的区域:左区域(如(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

然而,section很重要。让我们从右边开始:

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 <)

对我来说,这读起来是“除以某物”“ 2小于某物”我更喜欢这种方式,因为它是明确的。根据Python的禅意,& rdquo;显式优于隐式。


那么这对现有代码有什么影响呢?例如,考虑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,那么它就变成了一个二进制运算符,反之亦然。例如,考虑filterelem的翻转变体:

list `filter` odd -- now filter makes sense an an operator
elem n list -- now elem makes sense as a function

事实上,这可以推广到任何n元函数,只要n大于1。你看,每个函数都有一个主参数。一般来说,对于一元函数,这种区别是无关紧要的。但是对于非一元函数,这种区别很重要。

  1. 如果函数的主要参数出现在参数列表的末尾,则该函数是一个普通函数(例如filter odd list,其中list是主要参数)。将主参数放在列表的末尾对于函数组合是必要的。
  2. 如果函数的主要参数出现在参数列表的开头,则该函数是函数操作符(例如list `elem` n,其中list是主要参数)。
  3. 操作符类似于OOP中的方法,主参数类似于方法的对象。例如,list `elem` n将在OOP中写成list.elem(n)。OOP中的方法链接类似于FP[1]中的函数组合链。
  4. 函数的主参数只能在参数列表的开头或结尾。如果它在别的地方就没有意义了。这个性质对于二元函数是完全正确的。因此,翻转二进制函数使它们成为操作符,反之亦然。
  5. 剩下的实参和函数组成一个不可分割的原子,称为实参列表的主干。例如filter odd list,词干为filter oddlist `elem` n中的词干为(`elem` n)
  6. 词干的顺序和元素必须保持不变,表达式才有意义。这就是为什么odd `filter` listelem list n没有任何意义。然而,list `filter` oddelem 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))),那将是很棒的。这很难,但却是可行的。

我们都知道编程中的命名是一件严肃的事情,特别是当涉及到curry形式的函数时。正如Aadit在他的回应中所做的那样,用程序化的方法处理这个问题是一个有效的解决方案。但是,我看到了他的实现中的两个问题:

  • 它在Javascript中引入了带有左/右节的函数操作符,这不是语言
  • 的一部分
  • 它需要一个丑陋的占位符或undefinedhack来实现

Javascript没有柯里运算符,因此没有左右节。一个惯用的Javascript解决方案应该考虑到这一点。

问题原因

柯里函数没有实参的概念,因为每个函数调用只需要一个实参。您可以部分地或完全地应用没有任何帮助的柯里化函数:

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!

我们用翻转的参数定义了concatsub,因此部分应用程序可以按预期工作。但这显然不适用于完整的应用程序。

手动解决方案

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的左侧section。请注意,像addlt这样的函数操作符不需要左节版本,因为前者是可交换的,后者是谓词,它们有逻辑对应:

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一样。

最新更新