netwjx

混乱与有序

函数的Currying (Javascript 和 Scala)

| 评论

Currying或者Curry, 中文有翻译成科里化. 我最早了解它是在一篇讲Groovy中函数式编程的文章中, 之后又在Python遇到同样的东西. 最近在看Scala的介绍时又看到了, 而且发现Scala设计的明显更好, 然后就成了这篇文章, 使用Javascript作为主要语言是因为我使用Javascript的时间更长, 并且Javascript这门语言的表达能力奇强^-^.

我不确定把Currying记作科里化是否更容易理解, 所以下文还是依旧使用Currying吧.

Currying是函数式编程中一种高阶函数的典型应用, 如果非要把它对应到传统OO中的话, 那么它类似Builder模式, 一般译作构建器模式 建造者模式.

Builder模式可以简单理解为创建一个复杂的对象需要依赖多个参数, 要提供的参数又依赖于不同的方法, 使用Builder模式让每个方法只关注自己提供的参数, 最终根据全部参数创建出对象来. 对象实例最终是拿来调用的, 可以把这个过程想象成调用一个参数很多的函数.

Javascript中完全可以按照传统OO的方式实现Builder模式, 但使用Currying更轻量级 简单, 考虑下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function filter(list, func) {
    var ret = [];
    for (var i = 0; i < list.length; i++) {
        var v = list[i];
        if (func(v)) {
            ret.push(v);
        }
    }
    return ret;
}

function modN(n, x) {
    return x % n === 0;
}

var nums = [1, 2, 3, 4, 5, 6, 7, 8];

console.log(filter(nums, function(x) {
    return modN(2, x);
}));

console.log(filter(nums, function(x) {
    return modN(3, x);
}));

执行输出

1
2
2,4,6,8
3,6

modN函数有2个参数, 示例中可以同时提供所有的参数, 当然这是相当理想的情况. 实际可能不同的参数在不同的阶段提供:

1
2
3
4
5
6
7
8
9
10
11
var p = [2];  // 提供相关参数
console.log(filter(nums, function(x) {
    p[1] = x;
    return modN.apply(this, p);
}));

p[0] = 3;  // 提供相关参数
console.log(filter(nums, function(x) {
    p[1] = x
    return modN.apply(this, p);
}));

这种方式需要有一个变量用来保存参数, 而如果使用Currying可以这样:

1
2
console.log(filter(nums, modN.curry(2)));
console.log(filter(nums, modN.curry(3)));

根据需要可以curry()多个参数或curry()多次. 上文中使用的curry函数的实现:

1
2
3
4
5
6
7
Function.prototype.curry = function() {
    var func = this,
        p = Array.prototype.slice.call(arguments, 0);
    return function() {
        return func.apply(this, p.concat.apply(p, arguments));
    };
};

这是最轻量级 最简单的实现方式, 深入挖掘Javascript语言的表达力应该还会出现更巧妙的设计.

Currying的来由

考虑体积计算公式体积 = 长 x 宽 x 高, 假设已知长为10, 那么这个公式就变成了体积 = 10 x 宽 x 高, 进一步已知宽为7, 那么公式就变为体积 = 10 x 7 x 高, 这种转换即Currying.

这是最通俗的描述, 比较正式的可以参考维基百科Currying词条.

Scala语言中的Currying

之所以要额外提Scala, 是因为它是原生支持Currying的语言, 相对比通过类库支持能提供更巧妙的语法, 参见下面的代码:

A Tour of Scala: Currying 来源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
object CurryTest extends Application {

  // 这种声明就是支持Currying的函数, 每个参数用 ( ) 分隔开
  def modN(n: Int)(x: Int) = ((x % n) == 0)

  // 参数p是有1个Int类型参数的函数, 返回Boolean类型
  def filter(xs: List[Int], p: Int => Boolean): List[Int] =
    if (xs.isEmpty) xs
    else if (p(xs.head)) xs.head :: filter(xs.tail, p)
    else filter(xs.tail, p)

  val nums = List(1, 2, 3, 4, 5, 6, 7, 8)

  // modN2引用的是包含1个Int类型参数, 并返回Boolean类型的函数, 结尾的下划线是Scala中的语法
  val modN2 = modN(2)_
  println(filter(nums, modN2))

  // 不需要赋值的就不需要结尾的下划线了
  println(filter(nums, modN(3)))
}

上述代码只是加了额外的注释, 调整了下顺序, 和来源中的代码等价.

最重要的, Scala是静态类型语言, 开发环境可以提供每次Currying之后的函数提示信息, 并能够做编译时检查, 而Groovy, Python, Javascript只能依赖约定, 错误会在运行时发生, 必须有其它的措施确保同步修改关联的代码.

偏函数 Partial function

可以译作偏函数, 即提供部分参数值的函数, 和Currying很像, 只是另外一种更灵活的语法, 可以不按照参数顺序提供参数.

John Resig的Blog有一篇介绍Partial function和Currying的文章, 贴个简单的代码片段:

Partial Application in JavaScript 代码来源
1
2
3
4
5
  var delay = setTimeout.partial(undefined, 10);

  delay(function(){
    alert( "A call to this function will be temporarily delayed." );
  });

partial()的实现:

Partial Application in JavaScript 代码来源
1
2
3
4
5
6
7
8
9
10
Function.prototype.partial = function(){
  var fn = this, args = Array.prototype.slice.call(arguments);
  return function(){
    var arg = 0;
    for ( var i = 0; i < args.length && arg < arguments.length; i++ )
      if ( args[i] === undefined )
        args[i] = arguments[arg++];
    return fn.apply(this, args);
  };
};

里面还有一个curry函数, 基本和上文中的差不多, 有兴趣的可以点Partial Application in JavaScript

Scala也有Partial function的实现, 依旧是静态类型, 示例代码:

1
2
3
def add(i: Int, j: Int) = i + j
val add5 = add(_: Int, 5)
println(add5(2))

参考[Wikipedia][]的Contrast with partial function applicationPartial function

Javascript中更常见的传递大量参数的方式

Javascript是动态语言, 开发环境无法提供太多提示信息, 上文提到的Currying更适合一些比较稳定的, 不经常变动的API.

实际项目中如果函数参数很多, 并且可能在不同的地方提供参数, 则会使用参数对象的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function modN(opt) {
    opt.num = opt.num || 1;
    opt.x = opt.x || 1;
    return opt.x % opt.num === 0;
}

var opt2 = { num: 2 },
    opt3 = { num: 3 };

console.log(filter(nums, function(x) {
    opt2.x = x;
    return modN(opt2);
}));

console.log(filter(nums, function(x) {
    opt3.x = x;
    return modN(opt3);
}));

这个看起来和Builder模式十分相像, 参数都提供有默认值, numx可以使用更有意义的名称以使阅读性更好一些, 但也付出了不少编码工作.

Firefox 2.0开始支持解构赋值New in JavaScript 1.7: Destructuring assignment, 这个特性可以让实现modN少了一些纠结:

1
2
3
4
5
6
function modN(opt) {
    var { num, x } = opt;
    num = num || 1;
    x = x || 1;
    return x % num === 0;
}

原文中还有解构赋值的很多高级用法, 但是到目前为之还没看到其它浏览器提供支持, 也没有进入ECMAScript标准, 只能在比如Firefox扩展开发时爽爽.

参考资料

评论

Fork me on GitHub