柯里化的概念
在函数式编程(Functional Programming)相关的文章中,经常能看到柯里化 (Currying)这个名词。它是数学家柯里(Haskell Curry)提出的。
柯里化,用一句话解释就是,把一个多参数的函数转化为单参数函数的方法。
这是一个两个参数的普通函数:
function plus(x, y){
return x + y
}
plus(1, 2) // 输出 3
经过柯里化后这个函数变成这样:
function plus(y){
return function (x){
return x + y
}
}
plus(1)(2) // 输出 3
复合函数与柯里化
刚开始接触的时候,第一感觉和泰勒级数 (Taylor Series) 展开有些相似,然后我就去高数的书本里边翻,然而并没有 :) 实际上,编程相关的文章,只要提到柯里化,就是指的把一个多参数的函数转化为单参数函数的方法,只不过,文章中顺便提及了柯里化的由来,再加上 Haskell
高大上的编程语言,总觉着这得是个多复杂的公式。
复合函数,中数学课本就有(忘记是高中还是初中了,反正大学学的都忘记了 :p),形式上是这样:
$$F(x) = f(g(x))$$
可以这么理解这个函数,从西安去北京,可以乘坐直达车,也可以从郑州中转。$$F(x)$$就是整个旅程,$$g(x)$$是从西安到郑州,$$f(x)$$是从郑州到北京。如果把公式用编程语言表示,就是这样:
function travel(point2){
let dest = '北京'
return function(point1){
return point1 + '->' + point2
+ '->' + dest
}
}
travel('郑州')('西安') //输出 西安->郑州->北京
柯里化的作用
- 惰性求值 (Lazy Evaluation)
从上文的代码来看,柯里化收的函数是分步执行的,第一次调用返回的是一个函数,第二次调用的时候才会进行计算。起到延时计算的作用,通过延时计算求值,称之为惰性求值。
- 动态生成函数
假如实际编程中需要求不通数的若干次幂(整数),可能需要求2次幂,也能需要4次幂或者其他次幂,如果不用柯里化,那么需要求几次幂,就得写几个对应方法。通过柯里化,可以写在一个方法中:
function power(n){
return function (number){
let result = 1;
for(let i = 0; i < n; ++i){
result *= number;
}
return result;
}
}
需要求平方的时候,可以直接生成一个求平方的方法。
let p2 = power(2);
p2(4) // 输出16
p2(5) // 输出25
同样,需要求立方也可以直接生成一个求立方的方法,不用每个幂次都写一个方法。
let p3 = power(3); //求立方
p3(2) // 输出8
p3(4) // 输出64
柯里化与闭包
闭包 (Closure) 在前端圈讨论的非常的多,不过多数为刚入门的同学。闭包的概念,看专业的解释非常的复杂:
闭包包含自由(未绑定到特定对象)变量;这些变量不是在这个代码块内或者任何全局上下文中定义的,而是在定义代码块的环境中定义(局部变量)。“闭包” 一词来源于以下两者的结合:要执行的代码块(由于自由变量被包含在代码块中,这些自由变量以及它们引用的对象没有被释放)和为自由变量提供绑定的计算环境(作用域)。
这一大段话浓缩成一句话是,在一个函数内部定义一个局部变量,并通过一个函数将其返回。很明细上文提到的动态生成函数中用的例子就是一个闭包函数。
从形式来看,柯里化和闭包非常的相似,这二者有什么关系呢?
没什么关系,只不过看起来有些像。事实上,网络上大多的文章讨论柯里化与闭包都是分开来谈的,几乎没有明确的说明,他俩究竟有什么关系。首先,这是两个不通的概念,通过定义来看,作用也不同。只不过都用到了匿名函数。
柯里化与Lambda表达式
柯里化也经常与$$λ$$ (Lambda)表达式一起使用。提到Lambda表达式就必须得提到匿名函数。
匿名函数,在底层语言中不是现成的,比如C语言中就没有匿名函数,因为C语言在函数调用之前都得声明一下,或者把函数定义在调用之前,所以也就谈不上匿名函数了:p,C++ 11中才引入了Lambda表达式,支持匿名函数。所以一些资料有时也把匿名函数直接等同于Lambda表达式。
有种观点认为:
从纯粹的语义上,柯里化就是Lambda表达式的一个糖
这里其实就是把Lambda当作是匿名函数了。严格的来说,不是所有的匿名函数都是Lambda表达式。C# 2.0中引入了委托与匿名函数,3.0之后才引入Lambda表达式。
list.Where( delegate( object item ) {
return item != null;
});
这个是委托的写法
list.Where( item => item != null );
这个才是Lambda的写法(这里补充一点,经过自己的实践,其实在.NET 2.0的平台也可以使用Lambda表达式,只不过编译的时候调用的是高版本的编译器,所以在.NET 2.0的工程中使用高版本的C#特性,编译和运行都不会有问题)
Lambda表达式有固定的规范,写成 lambda x . body
(prefix
args
separator
expression
)表示一个参数参数为 x
的函数,它的返回值为 body
的计算结果。在不同的语言中Lambda表达式的形式也是不同的,比如Python:
lambda x : x + 1
prefix
是可选的, 再比如Java
item -> item != null
这么说来,柯里化与Lambda表达式也没有关系。
柯里化在Lambda表达式中应用
从上文来看,纯正的Lambda表达式的规范,是只有个一个入参的,如果多个参数怎么办呢?
柯里化的作用就能体现出来了,比如要实现一个两个数求和的表达式,用纯正的Lambda表达式,可以写成这样:
let foo = (x) => {
return y => x + y
}
foo(1)(2) //输出 3
这么显然有些麻烦,实践中是这么写的
let foo = (x, y) => x + y
foo(1, 2) //输出 3
是不是实践中的写法不符合Lambda表达式的规范呢?
可以理解为第二种写法为第一种写法的简写,是语法糖,第一种单个参数的写法是脱糖后的形式。