如何在JavaScript中正确地理解函数?

时间:2015-01-17 05:38:57

标签: javascript haskell currying lambda-calculus partial-application

我在JavaScript中编写了一个简单的curry函数,可以在大多数情况下正常工作:



const add = curry((a, b, c) => a + b + c);

const add2 = add(2);

const add5 = add2(3);

console.log(add5(5));

<script>
const curried = Symbol("curried");

Object.defineProperty(curry, curried, { value: true });

function curry(functor, ...initArgs) {
    if (arguments.length === 0) return curry;

    if (typeof functor !== "function") {
        const value = JSON.stringify(functor);
        throw new TypeError(`${value} is not a function`);
    }

    if (functor[curried] || initArgs.length >= functor.length)
        return functor(...initArgs);

    const result = (...restArgs) => curry(functor, ...initArgs, ...restArgs);

    return Object.defineProperty(result, curried, { value: true });
}
</script>
&#13;
&#13;
&#13;

但是,它不适用于以下情况:

// length :: [a] -> Number
const length = a => a.length;

// filter :: (a -> Bool) -> [a] -> [a]
const filter = curry((f, a) => a.filter(f));

// compose :: (b -> c) -> (a -> b) -> a -> c
const compose = curry((f, g, x) => f(g(x)));

// countWhere :: (a -> Bool) -> [a] -> Number
const countWhere = compose(compose(length), filter);

根据以下问题countWhere定义为(length .) . filter

What does (f .) . g mean in Haskell?

因此,我应该能够使用countWhere,如下所示:

const odd = n => n % 2 === 1;

countWhere(odd, [1,2,3,4,5]);

但是,它不返回3(数组[1,3,5]的长度),而是返回一个函数。我做错了什么?

3 个答案:

答案 0 :(得分:12)

您的curry功能(以及JavaScript中的most curry functions that people write)存在问题它没有正确处理额外的参数。

curry做什么

假设f是一个函数而f.lengthn。让curry(f)g。我们使用g参数调用m。会发生什么?

  1. 如果m === 0,则返回g
  2. 如果m < n则将f部分应用于m个新参数,并返回一个新的curried函数,该函数接受剩余的n - m个参数。
  3. 否则将f应用于m参数并返回结果。
  4. 这是大多数curry函数所做的,这是错误的。前两种情况是正确的,但第三种情况是错误的。相反,它应该是:

    1. 如果m === 0,则返回g
    2. 如果m < n则将f部分应用于m个新参数,并返回一个新的curried函数,该函数接受剩余的n - m个参数。
    3. 如果m === n,则将f应用于m个参数。如果结果是函数,则curry结果。最后,返回结果。
    4. 如果m > n,则将f应用于第一个n参数。如果结果是函数,则curry结果。最后,将结果应用于剩余的m - n参数并返回新结果。
    5. 大多数curry功能的问题

      请考虑以下代码:

      const countWhere = compose(compose(length), filter);
      
      countWhere(odd, [1,2,3,4,5]);
      

      如果我们使用不正确的curry函数,那么这相当于:

      compose(compose(length), filter, odd, [1,2,3,4,5]);
      

      但是,compose只接受三个参数。最后一个参数被删除:

      const compose = curry((f, g, x) =>f(g(x)));
      

      因此,上述表达式的评估结果为:

      compose(length)(filter(odd));
      

      这进一步评估为:

      compose(length, filter(odd));
      

      compose函数需要多一个参数,这就是它返回函数而不是返回3的原因。要获得正确的输出,您需要写:

      countWhere(odd)([1,2,3,4,5]);
      

      这就是大多数curry函数错误的原因。

      使用正确的curry函数

      的解决方案

      再次考虑以下代码:

      const countWhere = compose(compose(length), filter);
      
      countWhere(odd, [1,2,3,4,5]);
      

      如果我们使用正确的curry函数,那么这相当于:

      compose(compose(length), filter, odd)([1,2,3,4,5]);
      

      评估为:

      compose(length)(filter(odd))([1,2,3,4,5]);
      

      进一步评估(跳过中间步骤):

      compose(length, filter(odd), [1,2,3,4,5]);
      

      结果是:

      length(filter(odd, [1,2,3,4,5]));
      

      生成正确的结果3

      实施正确的curry功能

      在ES6中实现正确的curry功能非常简单:

      const curried = Symbol("curried");
      
      Object.defineProperty(curry, curried, { value: true });
      
      function curry(functor, ...initArgs) {
          if (arguments.length === 0) return curry;
      
          if (typeof functor !== "function") {
              const value = JSON.stringify(functor);
              throw new TypeError(`${value} is not a function`);
          }
      
          if (functor[curried]) return functor(...initArgs);
      
          const arity = functor.length;
          const args = initArgs.length;
      
          if (args >= arity) {
              const result = functor(...initArgs.slice(0, arity));
              return typeof result === "function" || args > arity ?
                  curry(result, ...initArgs.slice(arity)) : result;
          }
      
          const result = (...restArgs) => curry(functor, ...initArgs, ...restArgs);
      
          return Object.defineProperty(result, curried, { value: true });
      }
      

      我不确定curry的实施速度有多快。也许有人可以让它更快。

      使用正确的curry功能

      的含义

      使用正确的curry函数可以直接将Haskell代码转换为JavaScript。例如:

      const id = curry(a => a);
      
      const flip = curry((f, x, y) => f(y, x));
      

      id函数很有用,因为它允许您轻松地部分应用非咖喱函数:

      const add = (a, b) => a + b;
      
      const add2 = id(add, 2);
      

      flip函数很有用,因为它允许您在JavaScript中轻松创建right sections

      const sub = (a, b) => a - b;
      
      const sub2 = flip(sub, 2); // equivalent to (x - 2)
      

      这也意味着您不需要像extended compose function这样的黑客:

      What's a Good Name for this extended `compose` function?

      您可以简单地写一下:

      const project = compose(map, pick);
      

      正如问题所述,如果您想撰写lengthfilter,那么请使用(f .) . g模式:

      What does (f .) . g mean in Haskell?

      另一种解决方案是创建更高阶compose函数:

      const compose2 = compose(compose, compose);
      
      const countWhere = compose2(length, fitler);
      

      由于curry函数的正确实现,这一切都是可能的。

      额外的思考食物

      当我想要编写一系列函数时,我通常使用以下chain函数:

      const chain = compose((a, x) => {
          var length = a.length;
          while (length > 0) x = a[--length](x);
          return x;
      });
      

      这允许您编写如下代码:

      const inc = add(1);
      
      const foo = chain([map(inc), filter(odd), take(5)]);
      
      foo([1,2,3,4,5,6,7,8,9,10]); // [2,4,6]
      

      这相当于以下Haskell代码:

      let foo = map (+1) . filter odd . take 5
      
      foo [1,2,3,4,5,6,7,8,9,10]
      

      它还允许您编写如下代码:

      chain([map(inc), filter(odd), take(5)], [1,2,3,4,5,6,7,8,9,10]); // [2,4,6]
      

      这相当于以下Haskell代码:

      map (+1) . filter odd . take 5 $ [1,2,3,4,5,6,7,8,9,10]
      

      希望有所帮助。

答案 1 :(得分:1)

除了数学定义

  

currying是将具有n参数的函数转换为n函数序列,每个函数都接受一个参数。因此,arity从n-ary转换为n * 1-ary

影响编程的影响是什么? 抽象超过arity

const comp = f => g => x => f(g(x));
const inc = x => x + 1;
const mul = y => x => x * y;
const sqr = x => mul(x)(x);

comp(sqr)(inc)(1); // 4
comp(mul)(inc)(1)(2); // 4

comp需要两个函数fg以及一个任意参数x。因此g必须是一元函数(只有一个形式参数的函数)和f,因为它的返回值为gcomp(sqr)(inc)(1)工作的任何人都不会感到惊讶。 sqrinc都是一元的。

但是mul显然是一个二元函数。这怎么样才能起作用?因为currying抽象了mul的arity。你现在可以想象一下强大的功能是什么。

在ES2015中,我们可以使用箭头功能简洁地预先调整我们的功能:

const map = (f, acc = []) => xs => xs.length > 0
 ? map(f, [...acc, f(xs[0])])(xs.slice(1))
 : acc;

map(x => x + 1)([1,2,3]); // [2,3,4]

尽管如此,我们需要一个程序化的咖喱功能来控制我们无法控制的所有功能。由于我们了解到currying主要是指对arity的抽象,我们的实现不能依赖于Function.length

const curryN = (n, acc = []) => f => x => n > 1
 ? curryN(n - 1, [...acc, x])(f)
 : f(...acc, x);

const map = (f, xs) => xs.map(x => f(x));
curryN(2)(map)(x => x + 1)([1,2,3]); // [2,3,4]

明确地将arity传递给curryN有一个很好的副作用,我们也可以讨论可变参数函数:

const sum = (...args) => args.reduce((acc, x) => acc + x, 0);
curryN(3)(sum)(1)(2)(3); // 6

仍然存在一个问题:我们的咖喱解决方案无法处理方法。好的,我们可以轻松地重新定义我们需要的方法:

const concat = ys => xs => xs.concat(ys);
const append = x => concat([x]);

concat([4])([1,2,3]); // [1,2,3,4]
append([4])([1,2,3]); // [1,2,3,[4]]

另一种方法是以一种可以处理多参数函数和方法的方式调整curryN

const curryN = (n, acc = []) => f => x => n > 1 
 ? curryN(n - 1, [...acc, x])(f)
 : typeof f === "function"
  ? f(...acc, x)
  : x[f](...acc);

curryN(2)("concat")(4)([1,2,3]); // [1,2,3,4]

我不知道这是否是正确的方法来解决Javascript中的函数(和方法)。这是一种可能的方式。

修改

naomik指出,通过使用默认值,咖喱功能的内部API部分暴露。因此,实现的咖喱功能的简化以其稳定性为代价。为了避免API泄漏,我们需要一个包装函数。我们可以使用U组合器(类似于Naomik&Y的解决方案):

const U = f => f(f);
const curryN = U(h => acc => n => f => x => n > 1
 ? h(h)([...acc, x])(n-1)(f)
 : f(...acc, x))([]);

缺点:实现更难以阅读并且性能下降。

答案 2 :(得分:-1)

//---Currying refers to copying a function but with preset parameters

function multiply(a,b){return a*b};

var productOfSixNFiveSix = multiply.bind(this,6,5);

console.log(productOfSixNFive());

//The same can be done using apply() and call()

var productOfSixNFiveSix = multiply.call(this,6,5);

console.log(productOfSixNFive);

var productOfSixNFiveSix = multiply.apply(this,[6,5]);

console.log(productOfSixNFive);