Javascript 中的哪个参数顺序与运算符作为预柯里化函数和组合?
Which parameter order with operators as pre-curried functions and composition in Javascript?
给定一个简单的数学柯里化函数,用于减去数字:
function sub(x) {
return function (y) {
return x - y;
};
};
sub(3)(2); // 1
函数签名与获得的结果完全一致。一涉及到函数组合情况就变了:
function comp(f) {
return function (g) {
return function (x) {
return f(g(x));
};
};
};
function gte(x) {
return function (y) {
return x >= y;
};
};
comp(gte(2))(sub(3)) (4); // true
对于函数组合,所涉及的每个函数的最后一个参数是至关重要的,因为它分别由先前应用的函数提供的值 return 提供。因此,上例中的组合读作:4 - 3 >= 2
将产生 false
。事实上,组合背后的计算是:2 >= 3 - 4
,它产生 true
.
我可以轻松重写 sub
和 gte
以获得所需的结果:
function sub(y) {
return function (x) {
return x - y;
};
};
function gte(y) {
return function (x) {
return x >= y;
};
};
comp(gte(2))(sub(3)) (4); // false
但现在直接调用函数的return值与预期不同:
sub(3)(2); // -1 (but reads like 1)
gte(2)(3); // true (but reads like false)
我可以为每个调用切换参数或为每个案例定义部分应用函数:
function flip(f) {
return function (x) {
return function (y) {
return f(y)(x);
};
};
}
flip(gte)(2)(3); // false
var gteFlipped = flip(gte);
gteFlipped(2)(3); // false
这两种变体显然都很麻烦,而且可读性都不高。
哪个参数顺序更好?或者是否有一种机制可以同时使用这两种方法,具体取决于各自的要求(例如 Haskell 的 left/right 部分适用于部分应用的运算符部分)?
必须考虑一个可能的解决方案,我只使用一元函数!
我是这样读你作文的:
comp(gte(2))(sub(3)) (4);
gte(2) = function(y) { return 2 >= y; } // (x = 2)
sub(3) = function(y) { return 3 - y; } // (x = 3)
// Therefore:
comp(gte(2))(sub(3)) = function(x) {
var f = function(y) { return 2 >= y; };
var g = function(y) { return 3 - y; };
return f(g(x));
};
// Now call with (x = 4):
x = 4
g(4) = 3 - 4 = -1
f(-1) = (2 >= -1) = true
所以总之,看来你的预期是错误的。也许你确实有一些倒退的东西,但老实说我不知道是什么。但是,我确实认为这不是在 JavaScript 中工作的好方法,而且您使事情过于复杂,但这只是我的意见。
所以您想部分应用运算符而不必编写如下代码:
var gte2 = function (x) { return x >= 2; };
这是一个合理的用例,“部分应用运算符”。
答案很简单。只需编写一个柯里化函数。例如:
// var gte = y => x => x >= y; // ES6 syntax
var gte = function (y) {
return function (x) {
return x >= y;
};
};
var gte2 = gte(2);
不幸的是,我们可以通过两种方式部分应用二元运算符:
- 将运算符部分应用于左侧参数。
- 将运算符部分应用于正确的参数。
这提出了两个重要问题:
- 默认情况下运算符应该部分应用于哪个参数?
- 我们如何将运算符部分应用于另一个参数?
幸运的是,我们可以就一件事达成一致:向运算符提供两个参数是没有意义的。
// Why write the following:
add(2)(3)
// When you can write the following:
2 + 3
我们首先创建柯里化运算符函数的原因是为了部分应用。
因此,向函数“一次”提供两个参数是没有意义的。
此限制的后果是什么?这意味着:
我们可以选择我们想要的任何参数顺序。
// This is correct:
var sub = x => y => x - y;
// So is this:
var sub = y => x => x - y;
只要给出一个参数就可以理解。
// Consider this operator function:
var sub = y => x => x - y;
// This makes sense:
sub(1) // means (x => x - 1)
// However, this doesn't make sense:
sub(2)(3) // expected (2 - 3) but actually (3 - 2)
// But that's OK because it only needs to make sense given one argument.
现在,请记住哪个是最佳参数顺序?好吧,这取决于。
对于 commutative 操作,参数顺序无关紧要。
例如,加法和乘法都是可交换的。因此,a + b = b + a
用于加法,a * b = b * a
用于乘法。
对于 non-commutative 操作,right-to-left 参数顺序通常更好,因为它允许部分应用程序被大声读出。
例如,表达式 lt(2)
通常表示 x => x < 2
而不是 x => 2 < x
。
为什么这是一般情况?好吧,在 JavaScript 中,函数名位于参数之前。因此,name(arg)
自然读作 x => x name arg
而不是 x => arg name x
。
但是,第二条规则也有一些例外。最突出的是除法:
div(10) // is read out loud as divide 10 by x
// it is not read out loud as divide x by 10
当然,这种边缘情况的正确参数顺序是一个有争议的问题,但在我看来,left-to-right 参数顺序似乎更自然。
所以,这里有一堆柯里化的运算符函数:
// Commutative operators:
var add = x => y => x + y;
var mul = x => y => x * y;
// Right-to-left operators:
var lt = y => x => x < y;
var gt = y => x => x > y;
var lte = y => x => x <= y;
var gte = y => x => x >= y;
var sub = y => x => x - y;
// Left-to-right operators:
var div = x => y => x / y;
现在,第二个问题是我们如何将运算符部分应用于 “其他” 参数?
唯一的方法是创建一个参数顺序翻转的新函数。
幸运的是,我们不需要为每个运算符创建一个新函数:
对于交换运算符,参数顺序无关紧要。因此:
flip(add) = add
flip(mul) = mul
对于关系运算符,我们不需要创建额外的函数:
flip(lt) = gt
flip(gt) = lt
flip(lte) = gte
flip(gte) = lte
我们只需要为 sub
和 div
创建翻转运算符函数:
var subFrom = x => y => x - y; // subFrom(5) means (y => 5 - y)
var divBy = y => x => x / y; // divBy(10) means (x => x / 10)
综合考虑,我会说你应该使用你的常识。
此回复基于 Aadit 的回复。
在 Javascript 中实际上需要完全应用柯里化运算符函数 - 当用作第一个 Class 公民时:
function between(ops) {
return function (left) {
return function (right) {
return function (n) {
// At this point one should use the native Javascript operators
// but they can't be passed to the function, since operators are not First Class.
return ops[0](left)(n) && ops[1](right)(n);
};
};
};
}
function lte(y) { return function (x) { return x <= y; }; }
function gt(y) { return function (x) { return x > y; }; }
between([gt, lte])(2)(4)(4); // true
// is evaluated as: gt(2)(4) && lte(4)(4) === true; (confusing)
between
可能是无稽之谈,但它证明了完全应用柯里化运算符函数在 Javascript 中是有意义的。甚至可能还有其他用例。
尽管如此,Aadit 是对的,sub(2)(3)
与柯里化的目的相矛盾!
那么解决方案应该是什么样子的?
- 所有 柯里化运算符函数必须有一个right-to-left 参数顺序
- 引入了一个函数,它明确指出一次将所有参数传递给柯里化函数时的异常用法
这里是uncurryOp
:
// intended for all operator functions
function uncurryOp(f) {
return function (x, y) {
return f(y)(x);
};
}
uncurryOp(gt)(2, 4); // false (intuitive)
这不是一个真正合适的解决方案。我认为没有,因为 Javascript.
中缺少第一个 Class 和部分适用的运算符
给定一个简单的数学柯里化函数,用于减去数字:
function sub(x) {
return function (y) {
return x - y;
};
};
sub(3)(2); // 1
函数签名与获得的结果完全一致。一涉及到函数组合情况就变了:
function comp(f) {
return function (g) {
return function (x) {
return f(g(x));
};
};
};
function gte(x) {
return function (y) {
return x >= y;
};
};
comp(gte(2))(sub(3)) (4); // true
对于函数组合,所涉及的每个函数的最后一个参数是至关重要的,因为它分别由先前应用的函数提供的值 return 提供。因此,上例中的组合读作:4 - 3 >= 2
将产生 false
。事实上,组合背后的计算是:2 >= 3 - 4
,它产生 true
.
我可以轻松重写 sub
和 gte
以获得所需的结果:
function sub(y) {
return function (x) {
return x - y;
};
};
function gte(y) {
return function (x) {
return x >= y;
};
};
comp(gte(2))(sub(3)) (4); // false
但现在直接调用函数的return值与预期不同:
sub(3)(2); // -1 (but reads like 1)
gte(2)(3); // true (but reads like false)
我可以为每个调用切换参数或为每个案例定义部分应用函数:
function flip(f) {
return function (x) {
return function (y) {
return f(y)(x);
};
};
}
flip(gte)(2)(3); // false
var gteFlipped = flip(gte);
gteFlipped(2)(3); // false
这两种变体显然都很麻烦,而且可读性都不高。
哪个参数顺序更好?或者是否有一种机制可以同时使用这两种方法,具体取决于各自的要求(例如 Haskell 的 left/right 部分适用于部分应用的运算符部分)?
必须考虑一个可能的解决方案,我只使用一元函数!
我是这样读你作文的:
comp(gte(2))(sub(3)) (4);
gte(2) = function(y) { return 2 >= y; } // (x = 2)
sub(3) = function(y) { return 3 - y; } // (x = 3)
// Therefore:
comp(gte(2))(sub(3)) = function(x) {
var f = function(y) { return 2 >= y; };
var g = function(y) { return 3 - y; };
return f(g(x));
};
// Now call with (x = 4):
x = 4
g(4) = 3 - 4 = -1
f(-1) = (2 >= -1) = true
所以总之,看来你的预期是错误的。也许你确实有一些倒退的东西,但老实说我不知道是什么。但是,我确实认为这不是在 JavaScript 中工作的好方法,而且您使事情过于复杂,但这只是我的意见。
所以您想部分应用运算符而不必编写如下代码:
var gte2 = function (x) { return x >= 2; };
这是一个合理的用例,“部分应用运算符”。
答案很简单。只需编写一个柯里化函数。例如:
// var gte = y => x => x >= y; // ES6 syntax
var gte = function (y) {
return function (x) {
return x >= y;
};
};
var gte2 = gte(2);
不幸的是,我们可以通过两种方式部分应用二元运算符:
- 将运算符部分应用于左侧参数。
- 将运算符部分应用于正确的参数。
这提出了两个重要问题:
- 默认情况下运算符应该部分应用于哪个参数?
- 我们如何将运算符部分应用于另一个参数?
幸运的是,我们可以就一件事达成一致:向运算符提供两个参数是没有意义的。
// Why write the following:
add(2)(3)
// When you can write the following:
2 + 3
我们首先创建柯里化运算符函数的原因是为了部分应用。
因此,向函数“一次”提供两个参数是没有意义的。
此限制的后果是什么?这意味着:
我们可以选择我们想要的任何参数顺序。
// This is correct: var sub = x => y => x - y; // So is this: var sub = y => x => x - y;
只要给出一个参数就可以理解。
// Consider this operator function: var sub = y => x => x - y; // This makes sense: sub(1) // means (x => x - 1) // However, this doesn't make sense: sub(2)(3) // expected (2 - 3) but actually (3 - 2) // But that's OK because it only needs to make sense given one argument.
现在,请记住哪个是最佳参数顺序?好吧,这取决于。
对于 commutative 操作,参数顺序无关紧要。
例如,加法和乘法都是可交换的。因此,
a + b = b + a
用于加法,a * b = b * a
用于乘法。对于 non-commutative 操作,right-to-left 参数顺序通常更好,因为它允许部分应用程序被大声读出。
例如,表达式
lt(2)
通常表示x => x < 2
而不是x => 2 < x
。为什么这是一般情况?好吧,在 JavaScript 中,函数名位于参数之前。因此,
name(arg)
自然读作x => x name arg
而不是x => arg name x
。但是,第二条规则也有一些例外。最突出的是除法:
div(10) // is read out loud as divide 10 by x // it is not read out loud as divide x by 10
当然,这种边缘情况的正确参数顺序是一个有争议的问题,但在我看来,left-to-right 参数顺序似乎更自然。
所以,这里有一堆柯里化的运算符函数:
// Commutative operators:
var add = x => y => x + y;
var mul = x => y => x * y;
// Right-to-left operators:
var lt = y => x => x < y;
var gt = y => x => x > y;
var lte = y => x => x <= y;
var gte = y => x => x >= y;
var sub = y => x => x - y;
// Left-to-right operators:
var div = x => y => x / y;
现在,第二个问题是我们如何将运算符部分应用于 “其他” 参数?
唯一的方法是创建一个参数顺序翻转的新函数。
幸运的是,我们不需要为每个运算符创建一个新函数:
对于交换运算符,参数顺序无关紧要。因此:
flip(add) = add flip(mul) = mul
对于关系运算符,我们不需要创建额外的函数:
flip(lt) = gt flip(gt) = lt flip(lte) = gte flip(gte) = lte
我们只需要为
sub
和div
创建翻转运算符函数:var subFrom = x => y => x - y; // subFrom(5) means (y => 5 - y) var divBy = y => x => x / y; // divBy(10) means (x => x / 10)
综合考虑,我会说你应该使用你的常识。
此回复基于 Aadit 的回复。
在 Javascript 中实际上需要完全应用柯里化运算符函数 - 当用作第一个 Class 公民时:
function between(ops) {
return function (left) {
return function (right) {
return function (n) {
// At this point one should use the native Javascript operators
// but they can't be passed to the function, since operators are not First Class.
return ops[0](left)(n) && ops[1](right)(n);
};
};
};
}
function lte(y) { return function (x) { return x <= y; }; }
function gt(y) { return function (x) { return x > y; }; }
between([gt, lte])(2)(4)(4); // true
// is evaluated as: gt(2)(4) && lte(4)(4) === true; (confusing)
between
可能是无稽之谈,但它证明了完全应用柯里化运算符函数在 Javascript 中是有意义的。甚至可能还有其他用例。
尽管如此,Aadit 是对的,sub(2)(3)
与柯里化的目的相矛盾!
那么解决方案应该是什么样子的?
- 所有 柯里化运算符函数必须有一个right-to-left 参数顺序
- 引入了一个函数,它明确指出一次将所有参数传递给柯里化函数时的异常用法
这里是uncurryOp
:
// intended for all operator functions
function uncurryOp(f) {
return function (x, y) {
return f(y)(x);
};
}
uncurryOp(gt)(2, 4); // false (intuitive)
这不是一个真正合适的解决方案。我认为没有,因为 Javascript.
中缺少第一个 Class 和部分适用的运算符