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.

我可以轻松重写 subgte 以获得所需的结果:

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

不幸的是,我们可以通过两种方式部分应用二元运算符:

  1. 将运算符部分应用于左侧参数。
  2. 将运算符部分应用于正确的参数。

这提出了两个重要问题:

  1. 默认情况下运算符应该部分应用于哪个参数?
  2. 我们如何将运算符部分应用于另一个参数?

幸运的是,我们可以就一件事达成一致:向运算符提供两个参数是没有意义的。

// Why write the following:

add(2)(3)

// When you can write the following:

2 + 3

我们首先创建柯里化运算符函数的原因是为了部分应用。

因此,向函数“一次”提供两个参数是没有意义的

此限制的后果是什么?这意味着:

  1. 我们可以选择我们想要的任何参数顺序。

    // This is correct:
    
    var sub = x => y => x - y;
    
    // So is this:
    
    var sub = y => x => x - y;
    
  2. 只要给出一个参数就可以理解。

    // 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.
    

现在,请记住哪个是最佳参数顺序?好吧,这取决于。

  1. 对于 commutative 操作,参数顺序无关紧要。

    例如,加法和乘法都是可交换的。因此,a + b = b + a 用于加法,a * b = b * a 用于乘法。

  2. 对于 non-commutative 操作,right-to-left 参数顺序通常更好,因为它允许部分应用程序被大声读出。

    例如,表达式 lt(2) 通常表示 x => x < 2 而不是 x => 2 < x

    为什么这是一般情况?好吧,在 JavaScript 中,函数名位于参数之前。因此,name(arg) 自然读作 x => x name arg 而不是 x => arg name x

  3. 但是,第二条规则也有一些例外。最突出的是除法:

    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;

现在,第二个问题是我们如何将运算符部分应用于 “其他” 参数?

唯一的方法是创建一个参数顺序翻转的新函数。

幸运的是,我们不需要为每个运算符创建一个新函数:

  1. 对于交换运算符,参数顺序无关紧要。因此:

    flip(add) = add
    flip(mul) = mul
    
  2. 对于关系运算符,我们不需要创建额外的函数:

    flip(lt)  = gt
    flip(gt)  = lt
    flip(lte) = gte
    flip(gte) = lte
    
  3. 我们只需要为 subdiv 创建翻转运算符函数:

    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) 与柯里化的目的相矛盾!

那么解决方案应该是什么样子的?

  1. 所有 柯里化运算符函数必须有一个right-to-left 参数顺序
  2. 引入了一个函数,它明确指出一次将所有参数传递给柯里化函数时的异常用法

这里是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 和部分适用的运算符