上篇文章我们谈了高阶函数在函数式语言中的妙用,文末留了一个引子,点出了「柯里化」和「部分应用」两个概念。如果你还没有看过,我非常建议你先看一下 上一篇文章。
这篇文章,我将深入解释这两个概念的细节,并说明两个概念在函数式语言中的价值。
1. 什么是柯里化
我们上篇文章最后给出了一个相当简略的 Haskell 解法,就是 map (*2) [1, 2, 3, 4, 5]。这个解法看起来很直观,可是对于不熟悉函数式写法的朋友,细想一下可能还是会觉得这里的 *2 写法颇有些怪异:从类型的角度,我们知道这里应该是一个函数;可是这个「乘二」为什么可以是一个函数呢?
想了解为何这种写法成立,必须先解释什么是柯里化。简单来讲,柯里化就是把一个多参数函数,转化为了多个串联起来的单一参数函数。
我们写一个最简单的加法函数举个例子:
add :: Int -> Int -> Int
add x y = x + y
在传统非柯里化形式的函数定义中,其类型应包括一个含有两个整数的元组,返回一个整数。也就是(Int, Int) -> Int。如果用非柯里化形式写这个函数,应当写为:add' (x, y) = x + y。这是在其他语言中常见的多参数函数写法。
而在我们的例子中,这个函数实际的函数签名是:Int -> Int -> Int。这其实是经过柯里化处理后的标准函数类型。由于 Haskell 的函数签名是右结合的,柯里化后的函数签名,完整写法是是Int -> (Int -> Int)。
也就是说,原先是两个参数一起输入,柯里化后转变为先输入一个参数,返回一个单一参数的函数;第二个参数输入给这个单一参数函数,最终得到计算结果。
在 Haskell 中,柯里化后的类型形式尽管发生了变化,但是行为和原先是一致的。换言之,现在的add x y = x + y和非柯里化形式add' (x, y) = x + y行为是一致的,都是计算两数之和。
那你可能要问了,既然柯里化后和非柯里化的函数行为完全一致,为什么我们需要这样一个东西呢?
这就要提到我们的下一个概念:柯里化解锁的全新函数使用方式──「部分应用」。
2. 柯里化带来的部分应用
仔细阅读上文对柯里化的说明,我们会发现一个关键点:没有人规定函数必须一次提供所有参数。
也就是说,我们完全可以只提供一部分参数,使用返回的中间函数。而这,就是部分应用的概念。
仍然拿上文最基本的add函数举例:
add :: Int -> Int -> Int
add x y = x + y
add3 :: Int -> Int
add3 = add 3
add3 10 -- 13
add3 20 -- 23
add10 :: Int -> Int
add10 = add 10
add10 10 -- 20
add10 20 -- 30
在这个例子中,add3和add10两个函数就是add函数部分应用的结果。我们可以发现,对于只传入部分参数的情况,add函数返回值的类型的确是一个函数,符合我们的预期。
对于 Haskell 中的多参数函数,我们完全可以一次性只提供部分参数,得到的新函数可以等待其他参数需要传入的时候再做最后计算。
像这样通过部分应用得到的新函数,我们想要多少就可以定义多少。
3. 柯里化和部分应用的价值
这两个概念的核心价值,在于函数式语言对于代码逻辑的一种特殊的复用方式。我们上面举的例子比较简单,但对于更复杂的函数,使用部分应用的形式可以大大简化代码重复的现象。
在 Haskell 中,所有多参数函数都可以被理解为一种高阶函数,因为它们都可以通过部分应用的范式返回另一个函数。
用一个形象的比喻,就是多参数函数可以被理解成一个「函数工厂」,我们通过传入部分参数生成各式各样的新函数,这些函数被应用在了需要的地方。这极大增加的代码的抽象程度和使用的灵活性。
回到我们开头的例子,我想现在大家就理解map (*2) [1, 2, 3, 4, 5]为什么是一种合法且推荐的写法了:
*是一个双参数函数,这里我们只传入了一个参数2,得到了一个新的单一参数函数;而map正好需要第一个传入的参数是单一参数函数,我们通过函数的组合,最终得到了正确的结果。
这里,我们看到了柯里化和部分应用的另一个实用价值:通过灵活控制参数传入的时机,我们可以更自由地组合各个函数,用灵活的方式完成需求。
下一篇文章,我打算谈一谈函数式流程控制的问题,说明代数数据类型(ADT)和模式匹配这一对完美的组合。
如果你对更多函数式语言特性感兴趣,欢迎关注我!