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


Posted in Javascript onMarch 19, 2016

引子
先来看一道小问题:
有人在群里出了到一道题目:
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的功能,但是却给柯里化制造了麻烦。

思路是每一个函数都是有且只有一个参数的函数。如果你想拥有多个参数,你必须定义一系列相互嵌套的函数。讨厌!这样做一次两次还可以,可是需要以这种方式定义需要很多参数的函数的时候,就会变得相当??潞湍延谠亩痢#ǖ?潜鸬P模?一崧砩细嫠吣阋桓霭旆ǎ?/p>

一些函数编程语言,像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上。这是非常不可能的,你在所有方面关注性能。显然,用不用上面的代码自行考虑。
Javascript 相关文章推荐
javascript下给元素添加事件的方法与代码
Aug 13 Javascript
JQUERY设置IFRAME的SRC值的代码
Nov 30 Javascript
原始XMLHttpRequest方法详情回顾
Nov 28 Javascript
js代码实现无缝滚动(文字和图片)
Aug 20 Javascript
cocos2dx骨骼动画Armature源码剖析(三)
Sep 08 Javascript
javascript实现抽奖程序的简单实例
Jun 07 Javascript
js浏览器html5表单验证
Oct 17 Javascript
IE8利用自带的setCapture和releaseCapture解决iframe的拖拽事件方法
Oct 25 Javascript
Angular的$http的ajax的请求操作(推荐)
Jan 10 Javascript
简单的渐变轮播插件
Jan 12 Javascript
浅谈jquery中ajax跨域提交的时候会有2次请求的问题
Nov 10 jQuery
Vue.js实现的表格增加删除demo示例
May 22 Javascript
Linux下为Node.js程序配置MySQL或Oracle数据库的方法
Mar 19 #Javascript
分享js粘帖屏幕截图到web页面插件screenshot-paste
Aug 21 #Javascript
JQuery用户名校验的具体实现
Mar 18 #Javascript
基于javascript实现页面加载loading效果
Sep 15 #Javascript
JQuery fileupload插件实现文件上传功能
Mar 18 #Javascript
javascript移动开发中touch触摸事件详解
Mar 18 #Javascript
使用JavaScript为Kindeditor自定义按钮增加Audio标签
Mar 18 #Javascript
You might like
PHP类中Static方法效率测试代码
2010/10/17 PHP
getElementById在任意一款浏览器中都可以用吗的疑问回复
2007/05/13 Javascript
JavaScript 获取当前时间戳的代码
2010/08/05 Javascript
javascript 三种方法实现获得和设置以及移除元素属性
2013/03/20 Javascript
Jquery Uploadify上传带进度条的简单实例
2014/02/12 Javascript
LABjs、RequireJS、SeaJS的区别
2014/03/04 Javascript
AngularJS入门教程(二):AngularJS模板
2014/12/06 Javascript
nodejs爬虫抓取数据乱码问题总结
2015/07/03 NodeJs
javascript实现数组内值索引随机化及创建随机数组的方法
2015/08/10 Javascript
Bootstrap每天必学之折叠
2016/04/12 Javascript
Bootstrap学习笔记之css组件(3)
2016/06/07 Javascript
AngularJS过滤器filter用法实例分析
2016/11/04 Javascript
js判断一个字符串是以某个字符串开头的简单实例
2016/12/27 Javascript
Angular.js之作用域scope'@','=','&amp;'实例详解
2017/02/28 Javascript
微信小程序中用WebStorm使用LESS
2017/03/08 Javascript
微信小程序实现弹出菜单功能
2018/06/12 Javascript
Vue动态组件和异步组件原理详解
2019/05/06 Javascript
Cordova(ionic)项目实现双击返回键退出应用
2019/09/17 Javascript
javascript网页随机点名实现过程解析
2019/10/15 Javascript
[49:29]LGD vs Winstrike 2018国际邀请赛小组赛BO2 第一场 8.17
2018/08/18 DOTA
[33:17]OG vs VGJ.T 2018国际邀请赛小组赛BO2 第二场 8.18
2018/08/19 DOTA
[36:05]完美世界DOTA2联赛循环赛 Forest vs DM 第一场 11.06
2020/11/06 DOTA
python调用百度地图WEB服务API获取地点对应坐标值
2019/01/16 Python
Python 字典中的所有方法及用法
2020/06/10 Python
Html5 Geolocation获取地理位置信息实例
2016/12/09 HTML / CSS
ProBikeKit新西兰:自行车套件,跑步和铁人三项装备
2017/04/05 全球购物
全球最受追捧的运动服品牌领先数字目的地:Stylerunner
2020/11/25 全球购物
传播学专业毕业生自荐信
2013/11/04 职场文书
技术学校毕业生求职信分享
2013/12/02 职场文书
学校感恩节活动策划方案
2014/10/06 职场文书
2015共产党员公开承诺书
2015/01/22 职场文书
利用Matlab绘制各类特殊图形的实例代码
2021/07/16 Python
vue实现可以快进后退的跑马灯组件
2022/04/08 Vue.js
Python OpenCV实现图形检测示例详解
2022/04/08 Python
笔记本自带的win11如何跳过联网激活?
2022/04/20 数码科技
Win11 21h2可以升级22h2吗?看看你的电脑符不符合要求
2022/07/07 数码科技