深入解析JavaScript中函数的Currying柯里化

引子
先来看一道小问题:
有人在群里出了到一道题目:
var s = sum(1)(2)(3) ....... 最后 alert(s) 出来是6 
var s = sum(1)(2)(3)(4) ....... 最后 alert(s) 出来是10 
问sum怎么实现?
刚看到题目,我第一反应是sum返回的是一个function,但是没有最终实现,印象中看到过类似的原理,但是记不清了。
 
后来同事说,这个是叫柯里化,
实现方法比较巧妙:

function sum(x){
 var y = function(x){
  return sum(x+y)
 }
 y.toString = y.valueOf = function(){
  return x;
 }
 return y;
}

下面我们就深入来看一下currying柯里化~

什么是柯里化?

柯里化是这样的一个转换过程,把接受多个参数的函数变换成接受一个单一参数(注:最初函数的第一个参数)的函数,如果其他的参数是必要的,返回接受余下的参数且返回结果的新函数。

当我们这么说的时候,我想柯里化听起来相当简单。JavaScript中是怎么实现的呢?
假设我们要写一个函数,接受3个参数。

var sendMsg = function (from, to, msg) {
 alert(["Hello " + to + ",", msg, "Sincerely,", "- " + from].join("\n"));
};

现在,假定我们有柯里化函数,能够把传统的JavaScript函数转换成柯里化后的函数:

var sendMsgCurried = curry(sendMsg);
// returns function(a,b,c)

var sendMsgFromJohnToBob = sendMsgCurried("John")("Bob");
// returns function(c)

sendMsgFromJohnToBob("Come join the curry party!");
//=> "Hello Bob, Come join the curry party! Sincerely, - John"

手动柯里化

在上面的例子中,我们假定拥有神秘的curry函数。我会实现这样的函数,但是现在,我们首先看看为什么这样的函数是如此必要。
举个例子,手动柯里化一个函数并不困难,但是确实有点啰嗦:

// uncurried
var example1 = function (a, b, c) {

// do something with a, b, and c
};

// curried
var example2 = function(a) {
 return function (b) {
  return function (c) {

// do something with a, b, and c
  };
 };
};

在JavaScript,即使你不指定一个函数所有的参数,函数仍将被调用。这是个非常实用JavaScript的功能,但是却给柯里化制造了麻烦。

思路是每一个函数都是有且只有一个参数的函数。如果你想拥有多个参数,你必须定义一系列相互嵌套的函数。讨厌!这样做一次两次还可以,可是需要以这种方式定义需要很多参数的函数的时候,就会变得相当啰嗦和难于阅读。(但是别担心,我会马上告诉你一个办法)

一些函数编程语言,像Haskell和OCaml,语法中内置了函数柯里化。在这些语言中,举个例子,每个函数是拥有一个参数的函数,并且只有一个参数。你可能会认为这种限制麻烦胜过好处,但是语言的语法就是这样,这种限制几乎无法察觉。

举个例子,在OCaml,你可以用两种方式定义上面example:

let example1 = fun a b c ->

// (* do something with a, b, c *)

let example2 = fun a ->
 fun b ->
  fun c ->

// (* do something with a, b, c *)

很容易看出这两个例子和上面的那两个例子是如何的相似。

区别,然而,是否在OCaml也是做了同样的事情。OCaml,没有拥有多个参数的函数。但是,在一行中声明多个参数就是嵌套定义单参函数“快捷方式”。

类似的 ,我们期待调用柯里化函数句法上和OCaml中调用多参函数类似。我们期望这样调用上面的函数:

example1 foo bar baz
example2 foo bar baz

而在JavaScript,我们采用明显不同的方式:

example1(foo, bar, baz);
example2(foo)(bar)(baz);

在OCaml这类语言中,柯里化是内置的。在JavaScript,柯里化虽然可行(高阶函数),但是语法上是不方便的。这也是为什么我们决定编写一个柯里化函数来帮我们做这些繁琐的事情,并使得我们的代码简洁。

创建一个curry辅助函数

理论上我们期望可以有一个方便的方式转换普通老式的JavaScript函数(多个参数)到完全柯里化的函数。

这个想法不是我独有的,其他的人已经实现过了,例如在wu.js 库中的.autoCurry()函数(尽管你关心的是我们自己的实现方式)。

首先,让我们创建一个简单的辅助函数 .sub_curry:

function sub_curry(fn
/*, variable number of args */
) {
 var args = [].slice.call(arguments, 1);
 return function () {
  return fn.apply(this, args.concat(toArray(arguments)));
 };
}

让我们花点时间看看这个函数的功能。相当简单。sub_curry接受一个函数fn作为它的第一个参数,后面跟着任何数目的输入参数。返回的是一个函数,这个函数返回fn.apply执行结果,参数序列合并了该函数最初传入参数的,加上fn调用的时候传入参数的。

看例子:

var fn = function(a, b, c) { return [a, b, c]; };

// these are all equivalent
fn("a", "b", "c");
sub_curry(fn, "a")("b", "c");
sub_curry(fn, "a", "b")("c");
sub_curry(fn, "a", "b", "c")();
//=> ["a", "b", "c"]

很明显,这并不是我门想要的,但是看起来有点柯里化的意思了。现在我们将定义柯里化函数curry:

function curry(fn, length) {

// capture fn's # of parameters
 length = length || fn.length;
 return function () {
  if (arguments.length < length) {

// not all arguments have been specified. Curry once more.
   var combined = [fn].concat(toArray(arguments));
   return length - arguments.length > 0
    ? curry(sub_curry.apply(this, combined), length - arguments.length)
    : sub_curry.call(this, combined );
  } else {

// all arguments have been specified, actually call function
   return fn.apply(this, arguments);
  }
 };
}

这个函数接受两个参数,一个函数和要“柯里化”的参数数目。第二个参数是可选的,如果省略,默认使用Function.prototype.length 属性,就是为了告诉你这个函数定义了几个参数。

最终,我们能够论证下面的行为:

var fn = curry(function(a, b, c) { return [a, b, c]; });

// these are all equivalent
fn("a", "b", "c");
fn("a", "b", "c");
fn("a", "b")("c");
fn("a")("b", "c");
fn("a")("b")("c");
//=> ["a", "b", "c"]

我知道你在想什么…

等等…什么?!

难道你疯了?应该是这样!我们现在能够在JavaScript中编写柯里化函数,表现就如同OCaml或者Haskell中的那些函数。甚至,如果我想要一次传递多个参数,我可以向我从前做的那样,用逗号分隔下参数就可以了。不需要参数间那些丑陋的括号,即使是它是柯里化后的。

这个相当有用,我会立即马上谈论这个,可是首先我要让这个Curry函数前进一小步。

柯里化和“洞”(“holes”)

尽管柯里化函数已经很牛了,但是它也让你必须花费点小心思在你所定义函数的参数顺序上。终究,柯里化的背后思路就是创建函数,更具体的功能,分离其他更多的通用功能,通过分步应用它们。

当然这个只能工作在当最左参数就是你想要分步应用的参数!

为了解决这个,在一些函数式编程语言中,会定义一个特殊的“占位变量”。通常会指定下划线来干这事,如过作为一个函数的参数被传入,就表明这个是可以“跳过的”。是尚待指定的。

这是非常有用的,当你想要分步应用(partially apply)一个特定函数,但是你想要分布应用(partially apply)的参数并不是最左参数。

举个例子,我们有这样的一个函数:

var sendAjax = function (url, data, options) {
/* ... */
 }

也许我们想要定义一个新的函数,我们部分提供SendAjax函数特定的Options,但是允许url和data可以被指定。

当然了,我们能够相当简单的这样定义函数:

var sendPost = function (url, data) {
 return sendAjax(url, data, { type: "POST", contentType: "application/json" });
};

或者,使用使用约定的下划线方式,就像下面这样:

var sendPost = sendAjax( _ , _ , { type: "POST", contentType: "application/json" });

注意两个参数以下划线的方式传入。显然,JavaScript并不具备这样的原生支持,于是我们怎样才能这样做呢?

回过头让我们把curry函数变得智能一点…

首先我们把我们的“占位符”定义成一个全局变量。

var _ = {};

我们把它定义成对象字面量{},便于我们可以通过===操作符来判等。

不管你喜不喜欢,为了简单一点我们就使用_来做“占位符”。现在我们就可以定义新的curry函数,就像下面这样:

function curry (fn, length, args, holes) {
 length = length || fn.length;
 args = args || [];
 holes = holes || [];
 return function(){
  var _args = args.slice(0),
   _holes = holes.slice(0),
   argStart = _args.length,
   holeStart = _holes.length,
   arg, i;
  for(i = 0; i < arguments.length; i++) {
   arg = arguments[i];
   if(arg === _ && holeStart) {
    holeStart--;
    _holes.push(_holes.shift());
// move hole from beginning to end
   } else if (arg === _) {
    _holes.push(argStart + i);
// the position of the hole.
   } else if (holeStart) {
    holeStart--;
    _args.splice(_holes.shift(), 0, arg);
// insert arg at index of hole
   } else {
    _args.push(arg);
   }
  }
  if(_args.length < length) {
   return curry.call(this, fn, length, _args, _holes);
  } else {
   return fn.apply(this, _args);
  }
 }
}

实际代码还是有着巨大不同的。 我们这里做了一些关于这些“洞”(holes)参数是什么的记录。概括而言,运行的职责是相同的。

展示下我们的新帮手,下面的语句都是等价的:

var f = curry(function(a, b, c) { return [a, b, c]; });
var g = curry(function(a, b, c, d, e) { return [a, b, c, d, e]; });

// all of these are equivalent
f("a","b","c");
f("a")("b")("c");
f("a", "b", "c");
f("a", _, "c")("b");
f( _, "b")("a", "c");
//=> ["a", "b", "c"]

// all of these are equivalent
g(1, 2, 3, 4, 5);
g(_, 2, 3, 4, 5)(1);
g(1, _, 3)(_, 4)(2)(5);
//=> [1, 2, 3, 4, 5]

疯狂吧?!

我为什么要关心?柯里化能够怎么帮助我?

你可能会停在这儿思考…

这看起来挺酷而且…但是这真的能帮助我编写更好的代码?

这里有很多原因关于为什么函数柯里化是有用的。

函数柯里化允许和鼓励你分隔复杂功能变成更小更容易分析的部分。这些小的逻辑单元显然是更容易理解和测试的,然后你的应用就会变成干净而整洁的组合,由一些小单元组成的组合。

为了给一个简单的例子,让我们分别使用Vanilla.js, Underscore.js, and “函数化方式” (极端利用函数化特性)来编写CSV解析器。

Vanilla.js (Imperative)

//+ String -> [String]
var processLine = function (line){
 var row, columns, j;
 columns = line.split(",");
 row = [];
 for(j = 0; j < columns.length; j++) {
  row.push(columns[j].trim());
 }
};

//+ String -> [[String]]
var parseCSV = function (csv){
 var table, lines, i;
 lines = csv.split("\n");
 table = [];
 for(i = 0; i < lines.length; i++) {
  table.push(processLine(lines[i]));
 }
 return table;
};
Underscore.js

//+ String -> [String]
var processLine = function (row) {
 return _.map(row.split(","), function (c) {
  return c.trim();
 });
};

//+ String -> [[String]]
var parseCSV = function (csv){
 return _.map(csv.split("\n"), processLine);
};

函数化方式

//+ String -> [String]
var processLine = compose( map(trim) , split(",") );

//+ String -> [[String]]
var parseCSV = compose( map(processLine) , split("\n") );

所有这些例子功能上是等价的。我有意的尽可能的简单的编写这些。

想要达到某种效果是很难的,但是主观上这些例子,我真的认为最后一个例子,函数式方式的,体现了函数式编程背后的威力。

关于curry性能的备注

一些极度关注性能的人可以看看这里,我的意思是,关注下所有这些额外的事情?

通常,是这样,使用柯里化会有一些开销。取决于你正在做的是什么,可能会或不会,以明显的方式影响你。也就是说,我敢说几乎大多数情况,你的代码的拥有性能瓶颈首先来自其他原因,而不是这个。

有关性能,这里有一些事情必须牢记于心:

  • 存取arguments对象通常要比存取命名参数要慢一点
  • 一些老版本的浏览器在arguments.length的实现上是相当慢的
  • 使用fn.apply( … ) 和 fn.call( … )通常比直接调用fn( … ) 稍微慢点
  • 创建大量嵌套作用域和闭包函数会带来花销,无论是在内存还是速度上
  • 在大多是web应用中,“瓶颈”会发生在操控DOM上。这是非常不可能的,你在所有方面关注性能。显然,用不用上面的代码自行考虑。
(0)

相关推荐

  • 浅谈JS中的bind方法与函数柯里化

    绑定函数bind()最简单的用法是创建一个函数,使这个函数不论怎么调用都有同样的this值.不同于call和apply只是单纯地设置this的值后传参,它还会将所有传入bind()方法中的实参(第一个参数之后的参数)与this一起绑定. 关于这个特性看<JS权威指南>原文的例子: var sum = function(x,y) { return x + y }; var succ = sum.bind(null, 1); //让this指向null,其后的实参也会作为实参传入被绑定的函数sum

  • js function定义函数使用心得

    1.最基本的作为一个本本分分的函数声明使用. 复制代码 代码如下: function func(){} 或 var func=function(){}; 2.作为一个类构造器使用: 复制代码 代码如下: function class(){} class.prototype={}; var item=new class(); 3.作为闭包使用: 复制代码 代码如下: (function(){ //独立作用域 })(); 4.可以作为选择器使用: 复制代码 代码如下: var addEvent=ne

  • 深入剖析JavaScript中的函数currying柯里化

    curry化来源与数学家 Haskell Curry的名字 (编程语言 Haskell也是以他的名字命名).   柯里化通常也称部分求值,其含义是给函数分步传递参数,每次传递参数后部分应用参数,并返回一个更具体的函数接受剩下的参数,这中间可嵌套多层这样的接受部分参数函数,直至返回最后结果. 因此柯里化的过程是逐步传参,逐步缩小函数的适用范围,逐步求解的过程. 柯里化一个求和函数 按照分步求值,我们看一个简单的例子 var concat3Words = function (a, b, c) { r

  • javascript中有趣的反柯里化深入分析

    写在前面的话:国内对前端的研究在某些方面也不逊色于国外,这篇文章虽然看不太懂,但我很欣赏这种深入研究的精神! 反科里化的话题来自javascript之父Brendan Eich去年的一段twitter. 近几天研究了一下,觉得这个东东非常有意思,分享一下.先忘记它的名字,看下它能做什么. 不要小看这个功能,试想下,我们在写一个库的时候,时常会写这样的代码,拿webQQ的Jx库举例. 我们想要的,其实只是借用Array原型链上的一些函数.并没有必要去显式的构造一个新的函数来改变它们的参数并且重新运

  • js 函数式编程学习笔记

    (1)平常写的函数大多是接受值,合并值,返回值,比如经常写的for循环: function printArray(array){ for(var i=0;i<array.length;i++){ print(array[i]); } } 但是如果我们想做print之外的事情呢?怎么办?再写一个相似的,未免显得浪费,我们可以这样 function forEach(array,action){ for(var i=0;i<array.length;i++){ action(array[i]); }

  • Javascript闭包与函数柯里化浅析

    闭包和柯里化都是JavaScript经常用到而且比较高级的技巧,所有的函数式编程语言都支持这两个概念,因此,我们想要充分发挥出JavaScript中的函数式编程特征,就需要深入的了解这两个概念,闭包事实上更是柯里化所不可缺少的基础. 一.柯里化的概念 在计算机科学中,柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术.这个技术由Christopher Strachey以逻辑学家 Haskell Curry 命名的,尽管

  • javascript中利用柯里化函数实现bind方法

    柯理化函数思想:一个js预先处理的思想:利用函数执行可以形成一个不销毁的作用域的原理,把需要预先处理的内容都储存在这个不销毁的作用域中,并且返回一个小函数,以后我们执行的都是小函数,在小函数中把之前预先存储的值进行相关的操作处理即可: 柯里化函数主要起到预处理的作用: bind方法的作用:把传递进来的callback回调方法中的this预先处理为上下文context; /** * bind方法实现原理1 * @param callback [Function] 回调函数 * @param con

  • 深入学习 JavaScript中的函数调用

    定义 可能很多人在学习 JavaScript 过程中碰到过函数参数传递方式的迷惑,本着深入的精神,我想再源码中寻找些答案不过在做这件事之前,首先明确几个概念.抛弃掉值传递.引用传递等固有叫法,回归英文: call by reference && call by value && call by sharing 分别是我们理解的 C++ 中的引用传递,值传递.第三种比较迷惑,官方解释是 receives the copy of the reference to object

  • javascript中利用柯里化函数实现bind方法【推荐】

    • 柯理化函数思想:一个js预先处理的思想:利用函数执行可以形成一个不销毁的作用域的原理,把需要预先处理的内容都储存在这个不销毁的作用域中,并且返回一个小函数,以后我们执行的都是小函数,在小函数中把之前预先存储的值进行相关的操作处理即可: • 柯里化函数主要起到预处理的作用: • bind方法的作用:把传递进来的callback回调方法中的this预先处理为上下文context; /** * bind方法实现原理1 * @param callback [Function] 回调函数 * @par

  • js中匿名函数的N种写法

    匿名函数没有实际名字,也没有指针,怎么执行滴? 其实大家可以看看小括号的意义就应该可以理解.小括号有返回值,也就是小括号内的函数或者表达式的返回值,所以说小括号内的function返回值等于小括号的返回值,不难理解 (function(){})()可以将没有名字的函数执行了把- 关于匿名函数写法,很发散~ 最常见的用法: 复制代码 代码如下: (function() { alert('water'); })(); 当然也可以带参数: 复制代码 代码如下: (function(o) { alert

  • JavaScript装饰器函数(Decorator)实例详解

    本文实例讲述了JavaScript装饰器函数(Decorator).分享给大家供大家参考,具体如下: 装饰器函数(Decorator)用于给对象在运行期间动态的增加某个功能,职责等.相较通过继承的方式来扩充对象的功能,装饰器显得更加灵活,首先,我们可以动态给对象选定某个装饰器,而不用hardcore继承对象来实现某个功能点.其次:继承的方式可能会导致子类繁多,仅仅为了增加某一个单一的功能点,显得有些多余了. 下面给出几个常用的装饰器函数示例,相关代码请查看github. 1 动态添加onload

  • JavaScript函数柯里化原理与用法分析

    本文实例讲述了JavaScript函数柯里化原理与用法.分享给大家供大家参考,具体如下: 柯里化是这样的一个转换过程,把接受多个参数的函数变换成接受一个单一参数(译注:最初函数的第一个参数)的函数,如果其他的参数是必要的,返回接受余下的参数且返回结果的新函数. 也就是说是固定部分参数,返回一个接受剩余参数的函数,也称为部分计算函数,目的是为了缩小适用范围,创建一个针对性更强的函数. 例如,我想创建一个做自我介绍的函数,每个人只要输入自己姓名.性别.年龄即可.但是当A使用这个函数时,每次调用,都必

  • JavaScript函数柯里化详解

    什么是柯里化 柯里化是这样的一个转换过程,把接受多个参数的函数变换成接受一个单一参数(译注:最初函数的第一个参数)的函数,如果其他的参数是必要的,返回接受余下的参数且返回结果的新函数. 柯理化函数思想:一个js预先处理的思想:利用函数执行可以形成一个不销毁的作用域的原理,把需要预先处理的内容都储存在这个不销毁的作用域中,并且返回一个小函数,以后我们执行的都是小函数,在小函数中把之前预先存储的值进行相关的操作处理即可: 柯里化函数主要起到预处理的作用: bind方法的作用:把传递进来的callba

随机推荐