深入解析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 相关文章推荐
jquery关于图形报表的运用实现代码
Jan 06 Javascript
JQuery扩展插件Validate 2通过参数设置验证规则
Sep 05 Javascript
JavaScript高级程序设计 阅读笔记(四) ECMAScript中的类型转换
Feb 27 Javascript
js获取某月的最后一天日期的简单实例
Jun 22 Javascript
js中的caller和callee属性介绍和例子
Jun 07 Javascript
JS实现霓虹灯文字效果的方法
Aug 06 Javascript
基于HTML模板和JSON数据的JavaScript交互(移动端)
Apr 06 Javascript
Angular 页面跳转时传参问题
Aug 01 Javascript
JS实现DIV高度自适应窗口示例
Feb 16 Javascript
基于vue 开发中出现警告问题去除方法
Jan 25 Javascript
vue实现双向绑定和依赖收集遇到的坑
Nov 29 Javascript
Node.js 实现抢票小工具 &amp; 短信通知提醒功能
Oct 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正则表达匹配中文问题分析小结
2012/03/25 PHP
深入分析使用mysql_fetch_object()以对象的形式返回查询结果
2013/06/05 PHP
thinkphp实现数组分页示例
2014/04/13 PHP
php+ajax实现无刷新动态加载数据技术
2015/04/28 PHP
用javascript动态调整iframe高度的代码
2007/04/10 Javascript
javascript获取当前日期时间及其它操作函数
2011/01/11 Javascript
js 如何实现对数据库的增删改查
2012/11/23 Javascript
Javascript数组与字典用法分析
2014/12/13 Javascript
jquery删除指定子元素代码实例
2015/01/13 Javascript
Three.js学习之Lamber材质和Phong材质
2016/08/04 Javascript
14 个折磨人的 JavaScript 面试题
2016/08/08 Javascript
JavaScript实现简单的四则运算计算器完整实例
2017/04/28 Javascript
Javascript实现一个简单的输入关键字添加标签效果实例
2017/06/01 Javascript
JavaScript数据结构之双向链表定义与使用方法示例
2017/10/27 Javascript
vue slot与传参实例代码讲解
2019/04/28 Javascript
[43:48]Ti4正赛第一天 VG vs NEWBEE 2
2014/07/19 DOTA
Python实现的下载8000首儿歌的代码分享
2014/11/21 Python
Python脚本简单实现打开默认浏览器登录人人和打开QQ的方法
2016/04/12 Python
python numpy 显示图像阵列的实例
2018/07/02 Python
Python2.7版os.path.isdir中文路径返回false的解决方法
2019/06/21 Python
python如何获取列表中每个元素的下标位置
2019/07/01 Python
利用 Flask 动态展示 Pyecharts 图表数据方法小结
2019/09/04 Python
python中查看.db文件中表格的名字及表格中的字段操作
2020/07/07 Python
CSS3盒子模型详解
2013/04/24 HTML / CSS
澳洲的服装老品牌:SABA
2018/02/06 全球购物
澳大利亚家具和家居用品在线商店:Interiors Online
2018/03/05 全球购物
类、抽象类、接口的差异
2016/06/13 面试题
数据员岗位职责
2013/11/19 职场文书
车间操作工岗位职责
2013/12/19 职场文书
圣诞节活动策划方案
2014/06/09 职场文书
传播学专业毕业生自荐书
2014/07/01 职场文书
化学工程专业求职信
2014/08/10 职场文书
2014广电局实施党的群众路线教育实践活动方案思想汇报
2014/09/22 职场文书
团队会宣传标语
2014/10/09 职场文书
办公室务虚会发言材料
2014/10/20 职场文书
2019经典广告词集锦!
2019/07/02 职场文书