1335634-3083f915ae598d6e

2017年11月 国子监

柯里化的概念


在函数式编程(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

这里 prefixlambdaargsxseparator:expressionx + 1

其中prefix是可选的, 再比如Java的 Lambda 表达式中就没有:

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表达式的规范呢?
可以理解为第二种写法为第一种写法的简写,是语法糖,第一种单个参数的写法是脱糖后的形式。