Currying

Currying (javascript.info)

Currying is an advanced technique of working with functions. It’s used not only in JavaScript, but in other languages as well.

柯里化是一个用于函数的高阶技术。在 JS及其他语言中都有着很好的使用。

Currying is a transformation of functions that translates a function from callable as f(a, b, c) into callable as f(a)(b)(c).

柯里化是一个函数的转化方法,他能把一个函数的使用从f(a,b,c)变成f(a)(b)(c)

Currying doesn’t call a function. It just transforms it.

柯里化不调用函数,只转化函数。

Let’s see an example first, to better understand what we’re talking about, and then practical applications.

我们先看一个案例来更好的理解柯里化,然后再实际应用。

We’ll create a helper function curry(f) that performs currying for a two-argument f. In other words, curry(f) for two-argument f(a, b) translates it into a function that runs as f(a)(b):

现在我们创造一个curry(f)函数来执行两参数函数f的柯里化。换句话说,curry(f)把两参数函数f(a,b)转化成f(a)(b)

function curry(f) { // curry(f) does the currying transform
  return function(a) {
    return function(b) {
      return f(a, b);
    };
  };
}

// usage
function sum(a, b) {
  return a + b;
}

let curriedSum = curry(sum);

alert( curriedSum(1)(2) ); // 3

As you can see, the implementation is straightforward: it’s just two wrappers.

  • The result of curry(func) is a wrapper function(a).
  • When it is called like curriedSum(1), the argument is saved in the Lexical Environment, and a new wrapper is returned function(b).
  • Then this wrapper is called with 2 as an argument, and it passes the call to the original sum.

如你所见,实现非常简单直接:只是两个包装器

  • curry(func)的结果是包装器function(a)
  • 当像curriedSum(1)调用时,上下文环境保存参数,并返回一个新的包装器function(b)
  • 2作为参数调用新的这个包装器时,传递调用原始函数sum

More advanced implementations of currying, such as _.curry from lodash library, return a wrapper that allows a function to be called both normally and partially:

更高阶的柯里化实现,例如lodash库的_.curry,返回一个允许函数被正常调用或者以偏函数的形式调用的一个包装器:

function sum(a, b) {
  return a + b;
}

let curriedSum = _.curry(sum); // using _.curry from lodash library

alert( curriedSum(1, 2) ); // 3, still callable normally
alert( curriedSum(1)(2) ); // 3, called partially

Currying? What for?

柯里化?有什么用处?

To understand the benefits we need a worthy real-life example.

我们需要一个有价值的现实例子才能知道他的好处。

For instance, we have the logging function log(date, importance, message) that formats and outputs the information. In real projects such functions have many useful features like sending logs over the network, here we’ll just use alert:

假如我们现在有个用于格式化然后输出信息的日志函数log(date,importance,message)。在实际项目中,这种函数会有许多有用的功能点:比如通过网络发送日志,这里我们只使用alert:

function log(date, importance, message) {
  alert(`[${date.getHours()}:${date.getMinutes()}] [${importance}] ${message}`);
}

Let’s curry it!

我们来柯里化它!

log = _.curry(log);

After that log works normally:

柯里化后,原函数运行正常:

log(new Date(), "DEBUG", "some debug"); // log(a, b, c)

…But also works in the curried form:

柯里化的调用也同样正常:

log(new Date())("DEBUG")("some debug"); // log(a)(b)(c)

Now we can easily make a convenience function for current logs:

现在我们能很方便的制造一个用于即时日志的环境函数:

// logNow will be the partial of log with fixed first argument
let logNow = log(new Date());

// use it
logNow("INFO", "message"); // [HH:mm] INFO message

Now logNow is log with fixed first argument, in other words “partially applied function” or “partial” for short.

We can go further and make a convenience function for current debug logs:

logNow是第一个参数固定的log,也就是说“部分应用函数”,简称偏函数。

这样我们能更进一步,制造一个用于即时DEBUG日志的环境函数:

let debugNow = logNow("DEBUG");

debugNow("message"); // [HH:mm] DEBUG message

So:

  1. We didn’t lose anything after currying: log is still callable normally.
  2. We can easily generate partial functions such as for today’s logs.

所以:

  1. 柯里化后我们没有失去任何东西:log仍然可以正常调用
  2. 我们还能很容易的制造比如今日日志的部分函数

Advanced curry implementation

高阶柯里化实现

In case you’d like to get in to the details, here’s the “advanced” curry implementation for multi-argument functions that we could use above.

It’s pretty short:

如果你想了解更多细节,下面是用于多参数函数的“高阶”柯里化实现,我们也可以把它用于上面的示例。

它相当短:

function curry(func) {

  return function curried(...args) {
    if (args.length >= func.length) {
      return func.apply(this, args);
    } else {
      return function(...args2) {
        return curried.apply(this, args.concat(args2));
      }
    }
  };

}

Usage examples:

使用案例:

function sum(a, b, c) {
  return a + b + c;
}

let curriedSum = curry(sum);

alert( curriedSum(1, 2, 3) ); // 6, still callable normally
alert( curriedSum(1)(2,3) ); // 6, currying of 1st arg
alert( curriedSum(1)(2)(3) ); // 6, full currying

The new curry may look complicated, but it’s actually easy to understand.

新的curry看起来很复杂,但是实际上很容易理解。

The result of curry(func) call is the wrapper curried that looks like this:

调用curry(func)的结果是包装器curried,代码如下:

// func is the function to transform
function curried(...args) {
  if (args.length >= func.length) { // (1)
    return func.apply(this, args);
  } else {
    return function(...args2) { // (2)
      return curried.apply(this, args.concat(args2));
    }
  }
};

When we run it, there are two if execution branches:

  1. If passed args count is the same or more than the original function has in its definition (func.length) , then just pass the call to it using func.apply.
  2. Otherwise, get a partial: we don’t call func just yet. Instead, another wrapper is returned, that will re-apply curried providing previous arguments together with the new ones.

Then, if we call it, again, we’ll get either a new partial (if not enough arguments) or, finally, the result.

当我们运行它时,有两个if执行分支:

  1. 如果传递的args数量与原始函数定义的(func.length)一样或着更多,就直接使用func.apply传递调用到原函数
  2. 否则,得到一个偏函数:我们至今还没有调用func。相反,返回另一个包装器,它将提供先前的参数加上新的参数重复应用curried

然后,如果我们再次调用它,(如果没有足够的参数)将得到一个新的偏函数,最终得到结果。

Fixed-length functions only

仅可用于固定长度函数

The currying requires the function to have a fixed number of arguments.

A function that uses rest parameters, such as f(...args), can’t be curried this way.

柯里化需要一个固定长度参数的函数。

一个使用剩余参数,比如f(...args)是不能以这种方式柯里化的。

A little more than currying

比柯里化更多一点

By definition, currying should convert sum(a, b, c) into sum(a)(b)(c).

But most implementations of currying in JavaScript are advanced, as described: they also keep the function callable in the multi-argument variant.

根据定义,柯里化应该把sum(a,b,c)转化成sum(a)(b)(c)

但是JS中的大部分柯里化实现要更高级一点,如上所述:还保持了原函数的多参数调用方式。

Summary

总结

Currying is a transform that makes f(a,b,c) callable as f(a)(b)(c). JavaScript implementations usually both keep the function callable normally and return the partial if the arguments count is not enough.

Currying allows us to easily get partials. As we’ve seen in the logging example, after currying the three argument universal function log(date, importance, message) gives us partials when called with one argument (like log(date)) or two arguments (like log(date, importance)).

柯里化将一个f(a,b,c)函数的调用转化为f(a)(b)(c)。JS里的实现通常不仅保持原函数的正常调用,也会在参数数量不足时返回偏函数。

柯里化使我们很容易生成偏函数。正如我们在日志案例中看到的,在对三参数函数log(date,importance,message)柯里化后我们得以调用一参数函数log(date)或者两参数log(date,importance)的偏函数。

posted @ 2022-09-19 18:20  沧浪浊兮  阅读(30)  评论(0编辑  收藏  举报