在Javascript中哪个参数顺序与运算符一起作为预先计算的函数和组合?

时间:2016-01-05 11:21:12

标签: javascript parameters functional-programming composition currying

给出一个简单的数学曲线函数来减去数字:

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

对于函数组合,所涉及的每个函数的最后一个参数是至关重要的,因为它分别用先前应用的函数返回的值来馈送。上例中的组成因此读作: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

但是现在直接调用函数的返回值与预期不同:

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的左/右部分用于部分应用的运算符)?

一个可能的解决方案必须考虑到,我只使用一元函数!

3 个答案:

答案 0 :(得分:0)

以下是我阅读你作文的方式:

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中工作的好方法,而且你过于复杂,但这只是我的看法。

答案 1 :(得分:0)

因此,您希望部分应用运算符而无需编写如下代码:

var gte2 = function (x) { return x >= 2; };

这是一个合理的用例,“部分应用运营商”

答案很简单。只写一个curried函数。例如:

// var gte = y => x => x >= y; // ES6 syntax

var gte = function (y) {
    return function (x) {
        return x >= y;
    };
};

var gte2 = gte(2);

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

  1. 将操作符部分应用于左参数。
  2. 将操作符部分应用于正确的参数。
  3. 这提出了两个重要问题:

    1. 默认情况下应该将运算符部分应用于哪个参数?
    2. 我们如何将运算符部分应用于另一个参数?
    3. 幸运的是,我们可以就一件事达成一致:向运营商提供两个参数是没有意义的。

      // Why write the following:
      
      add(2)(3)
      
      // When you can write the following:
      
      2 + 3
      

      我们首先创建curried操作符函数的原因是部分应用。

      因此,同时为函数提供两个参数是没有意义的。

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

      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.
        
      3. 现在,记住这是最好的参数顺序?嗯,这取决于。

        1. 对于commutative操作,参数顺序并不重要。

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

        2. 对于非交换操作,从右到左的参数顺序通常更好,因为它允许大声读出部分应用程序。

          例如,表达式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
          

          当然,这种边缘情况的正确论证顺序是一个争论的问题,但在我看来,从左到右的论证顺序似乎更自然。

        4. 所以,这里有一堆curried运算符函数:

          // 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)
            
          4. 考虑到所有事情,我会说你应该运用常识。

答案 2 :(得分:0)

此回复基于Aadit的回复。

实际上需要在Javascript中完全应用curried操作符函数 - 当用作头等公民时:

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可能是无意义的,但它可以证明完全应用curried运算符函数在Javascript中是有意义的。甚至可能还有其他用例。

然而,Aadit是正确的,像sub(2)(3)之类的东西与currying的目的相矛盾!

那么解决方案怎么样?

  1. 所有 curried运算符函数必须具有从右到左的参数顺序
  2. 引入了一个函数,该函数明确指出将所有参数一次传递给curried函数时的异常使用
  3. 以下是uncurryOp

    // intended for all operator functions
    function uncurryOp(f) {
      return function (x, y) {
        return f(y)(x);
      };
    }
    
    uncurryOp(gt)(2, 4); // false (intuitive)
    

    这不是一个真正适合的解决方案。我认为没有,因为在Javascript中缺少First Class和部分适用的运算符。