问题描述
我已经在一个名为 Ramda 的 Javascript FP 库上工作了一段时间strong>,我在命名事物时遇到了一些小问题.(你听说过那句老话,对吧?计算机科学中只有两个难题:缓存失效、命名事物和一对一错误.")
在这个库中,(几乎)每个具有多个参数的函数都会自动柯里化.这适用于大多数用例.但是一些非交换二元运算符的函数存在一些问题.问题是英文名称通常倾向于暗示与应用柯里化时发生的情况不同的事情.例如,
var div10 =divide(10);
听起来应该是一个将其参数除以 10 的函数.但实际上它将其参数分为 10,如果你看一下就很清楚了定义:
vardivide = curry(function(a, b) {返回a/b;});
而不是预期:
div10(50);//=>5//不!
事实上,你得到了
div10(50);//=>0.2//正确,但令人惊讶!
我们通过记录与人们可能期望的差异来处理这个问题,并创建 divideBy
,它只是 flip(divide)
和 subtractN
,这是 flip(subtract)
.但是我们还没有找到类似 lt
:
R.lt = curry(function(a, b) {返回一个 <乙;});
或其表亲 lte
、gt
和 gte
.
我自己的直觉是
map(lt(5), [8, 6, 7, 5, 3, 0, 9]);//=>[假,假,假,假,真,真,假]
当然,它实际上返回了
//=>[真、真、真、假、假、假、真]
所以我想为 lt
及其同类执行相同的文档和指向备用名称例程.但是一直没找到好听的名字.唯一真正的候选者是 ltVal
并且在使用两个参数调用时它并没有真正起作用.我们讨论了这个问题,但没有好的结论.
其他人是否处理过这个问题并提出了好的解决方案?或者即使没有,对于这些函数的翻转版本的名称有什么好的建议吗?
更新
有人建议关闭这个,因为不清楚你在问什么",我猜这个问题确实在解释中有点丢失.简单的问题是:
对于lt
的翻转版本来说,什么是好的、直观的名称?
首先,感谢您正在维护的函数式编程库.我一直想自己写一个,但一直没有时间这样做.
考虑到您正在编写函数式编程库这一事实,我假设您了解 Haskell.在 Haskell 中,我们有函数和运算符.函数总是前缀.运算符总是中缀.
Haskell 中的函数可以使用反引号转换为运算符.例如div 6 3
可以写成6 `div` 3
.类似地,可以使用括号将运算符转换为函数.例如 2 可以写成
(.
也可以使用节部分应用运算符.有两种类型的部分:左部分(例如
(2 <)
和 (6 `div`)
)和右部分(例如 ( 和
(`div` 3)
).左侧部分翻译如下:(2 变为
(.右侧部分:
( 变为
flip (.
在 JavaScript 中,我们只有函数.在 JavaScript 中没有“好” 方法来创建运算符.您可以编写类似
(2).lt(3)
的代码,但以我的拙见,这是不礼貌的,我强烈建议不要编写这样的代码.
所以我们可以把普通的函数和运算符写成函数:
div(6, 3)//普通函数:div 6 3lt(2, 3)//运算符作为函数: (<) 2 3
在 JavaScript 中编写和实现中缀运算符是一件痛苦的事情.因此,我们不会有以下内容:
(6).div(3)//作为运算符的函数:6 `div` 3(2).lt(3)//普通运算符:2
然而,部分很重要.让我们从正确的部分开始:
div(3)//右边部分:(`div` 3)lt(3)//右侧部分:(< 3)
当我看到
div(3)
时,我希望它是一个正确的部分(即它应该表现为 (`div` 3)
).因此,根据最少惊讶原则,这是应该实施的方式.>
现在是左侧部分的问题.如果
div(3)
是右侧部分,那么左侧部分应该是什么样的?在我看来,它应该是这样的:
div(6, _)//左边部分:(6 `div`)lt(2, _)//左边部分:(2 <)
对我来说,这读作“除以6”和“比什么小2?”我更喜欢这种方式,因为它是明确的.根据Python 之禅,“Explicit 优于”
那么这对现有代码有何影响?例如,考虑
filter
函数.要过滤列表中的奇数,我们将编写 filter(odd, list)
.对于这样的函数,柯里化是否按预期工作?例如,我们将如何编写 filterOdd
函数?
var filterOdd = filter(odd);//预期的解决方案var filterOdd = filter(odd, _);//左侧部分,惊讶吗?
根据最小惊讶原则,它应该简单地是
filter(odd)
.filter
函数不能用作运算符.因此不应强迫程序员将其用作左侧部分.函数和“函数运算符”之间应该有明确的区别.
幸运的是,区分函数和函数运算符非常直观.例如,
filter
函数显然不是函数运算符:
filterodd list -- 过滤列表中的奇数;说得通奇数`filter`列表——列表的奇数过滤器?嗯?
另一方面,
elem
函数显然是一个函数运算符:
list `elem` n -- 列表的第 n 个元素;说得通elem list n -- 元素列表,n?嗯?
需要注意的是,这种区别之所以可行,是因为函数和函数运算符是互斥的.按理说,给定一个函数,它可能是一个普通函数,也可能是一个函数运算符,但不能同时是两者.
有趣的是,给定一个二元函数,如果你
翻转
它的参数,它就会变成一个二元运算符,反之亦然.例如考虑 filter
和 elem
的翻转变体:
list `filter`odd -- 现在 filter 是一个操作符elem n list -- 现在 elem 作为一个函数有意义
事实上,这可以推广到任何 n 元函数,如果 n 大于 1.你看,每个函数都有一个主要参数.简单地说,对于一元函数,这种区别是无关紧要的.然而,对于非一元函数,这种区别很重要.
如果函数的主要参数出现在参数列表的末尾,则该函数是一个普通函数(例如
filterodd list
其中list
是主要参数).将主要参数放在列表末尾是函数组合所必需的.如果函数的主要参数出现在参数列表的开头,则该函数是一个函数运算符(例如
list `elem` n
,其中list
是主要论点).运算符类似于 OOP 中的方法,主要参数类似于方法的对象.例如,
list `elem` n
在 OOP 中会被写成list.elem(n)
.OOP 中的链接方法类似于 FP 中的函数组合链.函数的主要参数只能位于参数列表的开头或结尾.把它放在其他地方是没有意义的.这个性质对于二元函数是完全正确的.因此,翻转二元函数使它们成为运算符,反之亦然.
其余的参数与函数一起形成一个不可分割的原子,称为参数列表的词干.例如,在
filterodd list
中,词干是filterodd
.在list `elem` n
中,词干是(`elem` n)
.词干的顺序和元素必须保持不变,表达式才能有意义.这就是为什么
odd `filter` list
和elem list n
没有任何意义的原因.然而,list `filter`odd
和elem n list
是有意义的,因为词干是不变的.
回到主要话题,由于函数和函数运算符是相互排斥的,您可以简单地以不同于对待普通函数的方式对待函数运算符.
我们希望运算符具有以下行为:
div(6, 3)//普通操作符:6 `div` 3div(6, _)//左边部分:(6 `div`)div(3)//右侧部分:(`div` 3)
我们要定义运算符如下:
var div = op(function (a, b) {返回a/b;});
op
函数的定义很简单:
function op(f) {var 长度 = f.length, _;//我们希望下划线未定义if (length
op
函数类似于在 Haskell 中使用反引号将函数转换为运算符.因此,您可以将其添加为 Ramda 的标准库函数.在文档中还提到运算符的主要参数应该是第一个参数(即它应该看起来像 OOP,而不是 FP).
顺便说一句,如果 Ramda 允许您像在常规 JavaScript 中链接方法一样组合函数(例如
foo(a, b).bar(c)
而不是 compose(bar(c), foo(a, b))
).这很难,但可行.
I've been working for some time on a Javascript FP library called Ramda, and I'm having a slight problem with naming things. (You've heard the old line, right? "There are only two hard problems in Computer Science: cache invalidation, naming things, and off-by-one errors.")
In this library, (almost) every function of more than one parameter is automatically curried. And this works well for most use-cases. But there are some issues with a few functions which are non-commutative binary operators. The issue is that the English names often tend to imply something different than what happens when currying is applied. For example,
var div10 = divide(10);
sounds like it should be a function that divides its parameter by 10. But in fact it divides its parameter into 10, which is pretty clear if you look at the definition:
var divide = curry(function(a, b) {
return a / b;
});
So instead the expected:
div10(50); //=> 5 // NO!!
In fact, you get
div10(50); //=> 0.2 // Correct, but surprising!
We handle this by documenting the difference from people's possible expectations, and creating
divideBy
, which is just flip(divide)
and subtractN
, which is flip(subtract)
. But we haven't found a good equivalent for functions such as lt
:
R.lt = curry(function(a, b) {
return a < b;
});
or its cousins
lte
, gt
, and gte
.
My own intuition would be that
map(lt(5), [8, 6, 7, 5, 3, 0, 9]);
//=> [false, false, false, false, true, true, false]
But of course, it actually returns
//=> [true, true, true, false, false, false, true]
So I'd like to do the same document-and-point-to-alternate-name routine for
lt
and its ilk. But I haven't been able to find a good name. The only real candidate has been ltVal
and that doesn't really work when called with both arguments. We did discuss this issue, but had no good conclusions.
Have others dealt with this and come up with good solutions? Or even if not, any good suggestions for a name for the flipped versions of these functions?
Update
Someone suggested that this be closed because 'unclear what you were asking', and I guess the question really was lost a bit in the explanation. The simple question is:
What would be a good, intuitive name for a flipped version of
lt
?
解决方案
First of all, kudos on the functional programming library that you are maintaining. I've always wanted to write one myself, but I've never found the time to do so.
Considering the fact that you are writing a functional programming library I'm going to assume that you know about Haskell. In Haskell we have functions and operators. Functions are always prefix. Operators are always infix.
Functions in Haskell can be converted into operators using backticks. For example
div 6 3
can be written as 6 `div` 3
. Similarly operators can be converted into functions using parentheses. For example 2 < 3
can be written as (<) 2 3
.
Operators can also be partially applied using sections. There are two types of sections: left sections (e.g.
(2 <)
and (6 `div`)
) and right sections (e.g. (< 3)
and (`div` 3)
). Left sections are translated as follows: (2 <)
becomes (<) 2
. Right sections: (< 3)
becomes flip (<) 3
.
In JavaScript we only have functions. There is no “good” way to create operators in JavaScript. You can write code like
(2).lt(3)
, but in my humble opinion it is uncouth and I would strongly advise against writing code like that.
So trivially we can write normal functions and operators as functions:
div(6, 3) // normal function: div 6 3
lt(2, 3) // operator as a function: (<) 2 3
Writing and implementing infix operators in JavaScript is a pain. Hence we won't have the following:
(6).div(3) // function as an operator: 6 `div` 3
(2).lt(3) // normal operator: 2 < 3
However sections are important. Let's start with the right section:
div(3) // right section: (`div` 3)
lt(3) // right section: (< 3)
When I see
div(3)
I would expect it to be a right section (i.e. it should behave as (`div` 3)
). Hence, according to the principle of least astonishment, this is the way it should be implemented.
Now comes the question of left sections. If
div(3)
is a right section then what should a left section look like? In my humble opinion it should look like this:
div(6, _) // left section: (6 `div`)
lt(2, _) // left section: (2 <)
To me this reads as “divide 6 by something” and “is 2 lesser than something?” I prefer this way because it is explicit. According to The Zen of Python, “Explicit is better than implicit.”
So how does this affect existing code? For example, consider the
filter
function. To filter the odd numbers in a list we would write filter(odd, list)
. For such a function does currying work as expected? For example, how would we write a filterOdd
function?
var filterOdd = filter(odd); // expected solution
var filterOdd = filter(odd, _); // left section, astonished?
According to the principle of least astonishment it should simply be
filter(odd)
. The filter
function is not meant to be used as an operator. Hence the programmer should not be forced to use it as a left section. There should be a clear distinction between functions and “function operators”.
Fortunately distinguishing between functions and function operators is pretty intuitive. For example, the
filter
function is clearly not a function operator:
filter odd list -- filter the odd numbers from the list; makes sense
odd `filter` list -- odd filter of list? huh?
On the other hand the
elem
function is clearly a function operator:
list `elem` n -- element n of the list; makes sense
elem list n -- element list, n? huh?
It's important to note that this distinction is only possible because functions and function operators are mutually exclusive. It stands to reason that given a function it may either be a normal function or else a function operator, but not both.
It's interesting to note that given a binary function if you
flip
its arguments then it becomes a binary operator and vice versa. For example consider the flipped variants of filter
and elem
:
list `filter` odd -- now filter makes sense an an operator
elem n list -- now elem makes sense as a function
In fact this could be generalized for any n-arity function were n is greater than 1. You see, every function has a primary argument. Trivially, for unary functions this distinction is irrelevant. However for non-unary functions this distinction is important.
If the primary argument of the function comes at the end of the argument list then the function is a normal function (e.g.
filter odd list
wherelist
is the primary argument). Having the primary argument at the end of the list is necessary for function composition.If the primary argument of the function comes at the beginning of the argument list then the function is a function operator (e.g.
list `elem` n
wherelist
is the primary argument).Operators are analogous to methods in OOP and the primary argument is analogous to the object of the method. For example
list `elem` n
would be written aslist.elem(n)
in OOP. Chaining methods in OOP is analogous to function composition chains in FP.The primary argument of the function may only be either at the beginning or at the end of the argument list. It wouldn't make sense for it to be anywhere else. This property is vacuously true for binary functions. Hence flipping binary functions makes them operators and vice-versa.
The rest of the arguments along with the function form an indivisible atom called the stem of the argument list. For example in
filter odd list
the stem isfilter odd
. Inlist `elem` n
the stem is(`elem` n)
.The order and the elements of the stem must remain unchanged for the expression to make sense. This is why
odd `filter` list
andelem list n
don't make any sense. Howeverlist `filter` odd
andelem n list
make sense because the stem is unchanged.
Coming back to the main topic, since functions and function operators are mutually exclusive you could simply treat function operators differently than the way you treat normal functions.
We want operators to have the following behavior:
div(6, 3) // normal operator: 6 `div` 3
div(6, _) // left section: (6 `div`)
div(3) // right section: (`div` 3)
We want to define operators as follows:
var div = op(function (a, b) {
return a / b;
});
The definition of the
op
function is simple:
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);
}
};
}
The
op
function is similar to using backticks to convert a function into a operator in Haskell. Hence you could add it as a standard library function for Ramda. Also mention in the docs that the primary argument of an operator should be the first argument (i.e. it should look like OOP, not FP).
On a side note it would be awesome if Ramda allowed you to compose functions as though it was chaining methods in regular JavaScript (e.g.
foo(a, b).bar(c)
instead of compose(bar(c), foo(a, b))
). It's difficult, but doable.
这篇关于`lt`、`lte`、`gt` 和 `gte` 的翻转版本的好名字?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!