Home  >  Article  >  Web Front-end  >  JavaScript function currying

JavaScript function currying

小云云
小云云Original
2017-12-06 16:11:271421browse

What is currying?

In computer science, Currying (English: Currying), also translated as Currying or Currying, is to transform a function that accepts multiple parameters into a function that accepts a single parameter. (the first parameter of the original function) and return a new function that accepts the remaining parameters and returns the result. This technique was named by Christopher Strachey after the logician Haskell Gary, although it was invented by Moses Schönfinkel and Gottlob Frege.

Intuitively, currying claims that if you fix certain parameters, you will get a function that accepts the remaining parameters.
In theoretical computer science, currying provides a way to study functions with multiple parameters in simple theoretical models, such as lambda calculus that only accepts a single parameter.
The dual of function currying is Uncurrying, a method of using anonymous single-parameter functions to implement multi-parameter functions.

Easy to understand

The concept of Currying is actually very simple. Only pass a part of the parameters to the function to call it and let it return a function to handle the remaining parameters.

If we need to implement a function that finds the sum of three numbers:

<span style="font-size: 16px;">function add(x, y, z) {<br>  return x + y + z;<br>}<br>console.log(add(1, 2, 3)); // 6<br></span>
<span style="font-size: 16px;">var add = function(x) {<br>  return function(y) {<br>    return function(z) {<br>      return x + y + z;<br>    }<br>  }<br>}<br><br>var addOne = add(1);<br>var addOneAndTwo = addOne(2);<br>var addOneAndTwoAndThree = addOneAndTwo(3);<br><br>console.log(addOneAndTwoAndThree);<br></span>

Here we define an add function, It takes one parameter and returns a new function. After calling add, the returned function remembers the first parameter of add through a closure. Calling it all at once is a bit tedious, but fortunately we can use a special curry helper function to make the definition and calling of such functions easier.

Using the ES6 arrow function, we can implement the above add like this:

<span style="font-size: 16px;">const add = x => y => z => x + y + z;<br></span>

It seems that using the arrow function is much clearer.

Partial function?

Let’s take a look at this function:

<span style="font-size: 16px;">function ajax(url, data, callback) {<br>  // ..<br>}<br></span>

There is a scenario where we need to initiate HTTP requests to multiple different interfaces, as follows: Two methods:

  • When calling the ajax() function, pass in the global URL constant.

  • #Create a function reference with preset URL parameters.

Next we create a new function, which still initiates an ajax() request internally. In addition, while waiting to receive the other two actual parameters, we manually add ajax ()The first actual parameter is set to the API address you care about.

For the first approach, we may produce the following calling method:

<span style="font-size: 16px;">function ajaxTest1(data, callback) {<br>  ajax('http://www.test.com/test1', data, callback);<br>}<br><br>function ajaxTest2(data, callback) {<br>  ajax('http://www.test.com/test2', data, callback);<br>}<br></span>

For these two similar functions, we can also extract The following pattern appears:

<span style="font-size: 16px;">function beginTest(callback) {<br>  ajaxTest1({<br>    data: GLOBAL_TEST_1,<br>  }, callback);<br>}<br></span>

I believe you have seen this pattern: we apply the actual parameters to the formal parameters at the function call-site. As you can see, we only apply some of the arguments initially—specifically, applying them to the URL parameters—and apply the rest later.

The above concept is the definition of a partial function. A partial function is a process of reducing the number of function parameters; the number of parameters here refers to the number of formal parameters that are expected to be passed in. We reduced the number of parameters of the original function ajax() from 3 to 2 through ajaxTest1().

We define a partial() function like this:

<span style="font-size: 16px;">function partial(fn, ...presetArgs) {<br>  return function partiallyApplied(...laterArgs) {<br>    return fn(...presetArgs, ...laterArgs);<br>  }<br>}<br></span>

The partial() function receives the fn parameter to represent the actual parameter used by us. (partially apply) function. Then, after the fn formal parameter, the presetArgs array collects the actual parameters passed in later and saves them for later use.

We create and return a new internal function (for clarity, we name it partiallyApplied(..)). In this function, the laterArgs array collects all the actual parameters. .

Using arrow functions is more concise:

<span style="font-size: 16px;">var partial =<br>  (fn, ...presetArgs) =><br>    (...laterArgs) =><br>      fn(...presetArgs, ...laterArgs);<br></span>

Using this mode of partial functions, we reconstruct the previous code:

<span style="font-size: 16px;">function ajax(url, data, callback) {<br>  // ..<br>}<br><br>var ajaxTest1 = partial(ajax, 'http://www.test.com/test1');<br>var ajaxTest2 = partial(ajax, 'http://www.test.com/test1');<br></span>

Think about the beginTest() function again. How should we use partial() to reconstruct it?

<span style="font-size: 16px;">function ajax(url, data, callback) {<br>  // ..<br>}<br><br>// 版本1<br>var beginTest = partial(ajax, 'http://www.test.com/test1', {<br>  data: GLOBAL_TEST_1,<br>});<br><br>// 版本2<br>var ajaxTest1 = partial(ajax, 'http://www.test.com/test1');<br>var beginTest = partial(ajaxTest1, {<br>  data: GLOBAL_TEST_1,<br>});<br></span>

Pass one at a time

I believe you have seen the advantages of version 2 over version 1 in the above example, yes , currying is: the process of converting a function with multiple parameters into a function one at a time. Each time a function is called, it accepts only one argument and returns a function until all arguments are passed.

The process of converting a function that takes multiple arguments into a function that takes them one at a time.

Each time the function is called it only accepts one argument and returns a function that takes one argument until all arguments are passed.

假设我们已经创建了一个柯里化版本的ajax()函数curriedAjax():

<span style="font-size: 16px;">curriedAjax('http://www.test.com/test1')<br>  ({<br>    data: GLOBAL_TEST_1,<br>  })<br>  (function callback(data) {<br>    // dosomething<br>  });<br></span>

我们将三次调用分别拆解开来,这也许有助于我们理解整个过程:

<span style="font-size: 16px;">var ajaxTest1 = curriedAjax('http://www.test.com/test1');<br><br>var beginTest = ajaxTest1({<br>  data: GLOBAL_TEST_1,<br>});<br><br>var ajaxCallback = beginTest(function callback(data) {<br>  // dosomething<br>});<br></span>

实现柯里化

那么,我们如何来实现一个自动的柯里化的函数呢?

<span style="font-size: 16px;">var currying = function(fn) {<br>  var args = [];<br><br>  return function() {<br>    if (arguments.length === 0) {<br>      return fn.apply(this, args); // 没传参数时,调用这个函数<br>    } else {<br>      [].push.apply(args, arguments); // 传入了参数,把参数保存下来<br>      return arguments.callee; // 返回这个函数的引用<br>    }<br>  }<br>}<br></span>

调用上述currying()函数:

<span style="font-size: 16px;">var cost = (function() {<br>  var money = 0;<br>  return function() {<br>    for (var i = 0; i < arguments.length; i++) {<br>      money += arguments[i];<br>    }<br>    return money;<br>  }<br>})();<br><br>var cost = currying(cost);<br><br>cost(100); // 传入了参数,不真正求值<br>cost(200); // 传入了参数,不真正求值<br>cost(300); // 传入了参数,不真正求值<br><br>console.log(cost()); // 求值并且输出600<br></span>

上述函数是我之前的JavaScript设计模式与开发实践读书笔记之闭包与高阶函数所写的currying版本,现在仔细思考后发现仍旧有一些问题。

我们在使用柯里化时,要注意同时为函数预传的参数的情况。

因此把上述柯里化函数更改如下:

<span style="font-size: 16px;">var currying = function(fn) {<br>  var args = Array.prototype.slice.call(arguments, 1);<br><br>  return function() {<br>    if (arguments.length === 0) {<br>      return fn.apply(this, args); // 没传参数时,调用这个函数<br>    } else {<br>      [].push.apply(args, arguments); // 传入了参数,把参数保存下来<br>      return arguments.callee; // 返回这个函数的引用<br>    }<br>  }<br>}<br></span>

使用实例:

<span style="font-size: 16px;">var cost = (function() {<br>  var money = 0;<br>  return function() {<br>    for (var i = 0; i < arguments.length; i++) {<br>      money += arguments[i];<br>    }<br>    return money;<br>  }<br>})();<br><br>var cost = currying(cost, 100);<br>cost(200); // 传入了参数,不真正求值<br>cost(300); // 传入了参数,不真正求值<br><br>console.log(cost()); // 求值并且输出600<br></span>

你可能会觉得每次都要在最后调用一下不带参数的cost()函数比较麻烦,并且在cost()函数都要使用arguments参数不符合你的预期。我们知道函数都有一个length属性,表明函数期望接受的参数个数。因此我们可以充分利用预传参数的这个特点。

借鉴自mqyqingfeng:

<span style="font-size: 16px;">function sub_curry(fn) {<br>  var args = [].slice.call(arguments, 1);<br>  return function() {<br>    return fn.apply(this, args.concat([].slice.call(arguments)));<br>  };<br>}<br><br>function curry(fn, length) {<br><br>  length = length || fn.length;<br><br>  var slice = Array.prototype.slice;<br><br>  return function() {<br>    if (arguments.length < length) {<br>      var combined = [fn].concat(slice.call(arguments));<br>      return curry(sub_curry.apply(this, combined), length - arguments.length);<br>    } else {<br>      return fn.apply(this, arguments);<br>    }<br>  };<br>}<br></span>

在上述函数中,我们在currying的返回函数中,每次把arguments.length和fn.length作比较,一旦arguments.length达到了fn.length的数量,我们就去调用fn(return fn.apply(this, arguments);)

验证:

<span style="font-size: 16px;">var fn = curry(function(a, b, c) {<br>  return [a, b, c];<br>});<br><br>fn("a", "b", "c") // ["a", "b", "c"]<br>fn("a", "b")("c") // ["a", "b", "c"]<br>fn("a")("b")("c") // ["a", "b", "c"]<br>fn("a")("b", "c") // ["a", "b", "c"]<br></span>

bind方法的实现

使用柯里化,能够很方便地借用call()或者apply()实现bind()方法的polyfill。

<span style="font-size: 16px;">Function.prototype.bind = Function.prototype.bind || function(context) {<br>  var me = this;<br>  var args = Array.prototype.slice.call(arguments, 1);<br>  return function() {<br>    var innerArgs = Array.prototype.slice.call(arguments);<br>    var finalArgs = args.concat(innerArgs);<br>    return me.apply(contenxt, finalArgs);<br>  }<br>}<br></span>

上述函数有的问题在于不能兼容构造函数。我们通过判断this指向的对象的原型属性,来判断这个函数是否通过new作为构造函数调用,来使得上述bind方法兼容构造函数。

Function.prototype.bind() by MDN如下说到:

绑定函数适用于用new操作符 new 去构造一个由目标函数创建的新的实例。当一个绑定函数是用来构建一个值的,原来提供的 this 就会被忽略。然而, 原先提供的那些参数仍然会被前置到构造函数调用的前面。

这是基于MVC的JavaScript Web富应用开发的bind()方法实现:

<span style="font-size: 16px;">Function.prototype.bind = function(oThis) {<br>  if (typeof this !== "function") {<br>    throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");<br>  }<br><br>  var aArgs = Array.prototype.slice.call(arguments, 1),<br>    fToBind = this,<br>    fNOP = function() {},<br>    fBound = function() {<br>      return fToBind.apply(<br>        this instanceof fNOP && oThis ? this : oThis || window,<br>        aArgs.concat(Array.prototype.slice.call(arguments))<br>      );<br>    };<br><br>  fNOP.prototype = this.prototype;<br>  fBound.prototype = new fNOP();<br><br>  return fBound;<br>};<br></span>

反柯里化(uncurrying)

可能遇到这种情况:拿到一个柯里化后的函数,却想要它柯里化之前的版本,这本质上就是想将类似f(1)(2)(3)的函数变回类似g(1,2,3)的函数。

下面是简单的uncurrying的实现方式:

<span style="font-size: 16px;">function uncurrying(fn) {<br>  return function(...args) {<br>    var ret = fn;<br><br>    for (let i = 0; i < args.length; i++) {<br>      ret = ret(args[i]); // 反复调用currying版本的函数<br>    }<br><br>    return ret; // 返回结果<br>  };<br>}<br></span>

注意,不要以为uncurrying后的函数和currying之前的函数一模一样,它们只是行为类似!

<span style="font-size: 16px;">var currying = function(fn) {<br>  var args = Array.prototype.slice.call(arguments, 1);<br><br>  return function() {<br>    if (arguments.length === 0) {<br>      return fn.apply(this, args); // 没传参数时,调用这个函数<br>    } else {<br>      [].push.apply(args, arguments); // 传入了参数,把参数保存下来<br>      return arguments.callee; // 返回这个函数的引用<br>    }<br>  }<br>}<br><br>function uncurrying(fn) {<br>  return function(...args) {<br>    var ret = fn;<br><br>    for (let i = 0; i < args.length; i++) {<br>      ret = ret(args[i]); // 反复调用currying版本的函数<br>    }<br><br>    return ret; // 返回结果<br>  };<br>}<br><br>var cost = (function() {<br>  var money = 0;<br>  return function() {<br>    for (var i = 0; i < arguments.length; i++) {<br>      money += arguments[i];<br>    }<br>    return money;<br>  }<br>})();<br><br>var curryingCost = currying(cost);<br>var uncurryingCost = uncurrying(curryingCost);<br>console.log(uncurryingCost(100, 200, 300)()); // 600<br></span>

柯里化或偏函数有什么用?

无论是柯里化还是偏应用,我们都能进行部分传值,而传统函数调用则需要预先确定所有实参。如果你在代码某一处只获取了部分实参,然后在另一处确定另一部分实参,这个时候柯里化和偏应用就能派上用场。

另一个最能体现柯里化应用的的是,当函数只有一个形参时,我们能够比较容易地组合它们(单一职责原则(Single responsibility principle))。因此,如果一个函数最终需要三个实参,那么它被柯里化以后会变成需要三次调用,每次调用需要一个实参的函数。当我们组合函数时,这种单元函数的形式会让我们处理起来更简单。

归纳下来,主要为以下常见的三个用途:

  • 延迟计算

  • 参数复用

  • Dynamic generation function

##The above content is the JavaScript function Currying explanation, I hope it will be helpful to everyone.

Related recommendations:

Detailed examples of js currying

Anti-currying in JS

Detailed explanation of JavaScript function currying

The above is the detailed content of JavaScript function currying. For more information, please follow other related articles on the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn