你写一个两数相加的函数:
scala
def add(a: Int, b: Int): Int = a + b
add(1, 2) // 3
Scala里还有另一种写法------把参数拆成两个列表:
scala
def addCurried(a: Int)(b: Int): Int = a + b
addCurried(1)(2) // 3
这就是柯里化(Currying)。
很多人第一次看到这种写法,第一反应是:这不是多此一举吗?一个参数列表能写完的事,为什么要拆成两个?
这篇文章就来回答这个问题:柯里化到底有什么用?它的底层是什么?它和偏应用函数是什么关系?

先搞清楚:柯里化到底是什么
柯里化的定义:把一个接受多个参数的函数,变成一串只接受一个参数的函数。
scala
// 普通函数:两个参数
def add(a: Int, b: Int): Int = a + b
// 柯里化:两个参数列表,每个列表一个参数
def addCurried(a: Int)(b: Int): Int = a + b
调用的时候:
scala
add(1, 2) // 3------一次传两个参数
addCurried(1)(2) // 3------传两次,每次一个参数
看起来只是写法不同------但底层完全不一样。
add(1, 2)是一次函数调用,传两个参数。
addCurried(1)(2)是两次函数调用------先传1,返回一个函数;再传2,得到结果。
scala
// addCurried(1) 返回的是什么?------一个函数 Int => Int
val intermediate: Int => Int = addCurried(1)
intermediate(2) // 3
看到了吗?addCurried(1)没有报"参数不够"------它返回了一个新的函数,这个函数记住了a=1,只等你传b。
柯里化的本质:多参数函数 = 单参数函数返回单参数函数。
柯里化的底层:它是高阶函数
柯里化函数在底层是什么?------它就是一个返回函数的高阶函数。
scala
// 柯里化的写法
def addCurried(a: Int)(b: Int): Int = a + b
// 等价于这个返回函数的高阶函数
def addEquivalent(a: Int): Int => Int = {
(b: Int) => a + b
}
这两个函数在底层几乎一模一样------都是先接受a,返回一个接受b的函数。
区别只是写法------def addCurried(a: Int)(b: Int)是语法糖,让你写起来更自然。
柯里化不是什么特殊的东西------它就是"返回函数的高阶函数"的语法糖。
柯里化到底有什么用?
这是最核心的问题。如果只是写法不同,为什么要多此一举?
用途1:天然支持偏应用
普通函数要偏应用,你得用_占位:
scala
def add(a: Int, b: Int): Int = a + b
val addOne = add(1, _: Int) // 要写_: Int
柯里化函数天然就是偏应用的------不用_:
scala
def addCurried(a: Int)(b: Int): Int = a + b
val addOne = addCurried(1) _ // 自然偏应用
// 甚至在某些上下文里_都可以省
用途2:让函数参数更灵活------by-name参数和控制抽象
这是柯里化最有价值的用途------写出像语言内置语法一样的自定义控制结构。
scala
// 自定义一个类似while的控制结构
def myWhile(condition: => Boolean)(body: => Unit): Unit = {
if (condition) {
body
myWhile(condition)(body)
}
}
var i = 0
myWhile(i < 5) {
println(i)
i += 1
}
注意最后那段------myWhile(i < 5) { ... }------花括号写在外面,看起来就像Scala内置的while语法。
为什么能这么写?------因为最后一个参数列表是一个函数参数,Scala允许你把函数参数用花括号写在外面(这叫尾置参数,trailing parameter)。
没有柯里化,你写不出这种像语法一样自然的控制结构。
用途3:类型推断更准确
Scala的类型推断是按参数列表进行的------前一个参数列表的类型,可以帮助推断后一个参数列表的类型。
scala
// foldLeft的签名是柯里化的
def foldLeft[B](z: B)(op: (B, A) => B): B
// 调用的时候,第一个参数列表帮助推断第二个的类型
List(1,2,3).foldLeft(0)(_ + _)
// 第一个0推断B=Int,第二个参数就知道是(Int, Int) => Int
如果foldLeft不是柯里化的------foldLeft(z, op)------类型推断可能推断不出来,你就得手写类型。
用途4:隐式参数
Scala的implicit参数必须单独放在一个参数列表里------这天然就是柯里化的形式:
scala
def foo(a: Int)(implicit b: String): String = a + b
Scala 3的using也是一样:
scala
def foo(a: Int)(using ctx: Context): Result
柯里化和偏应用函数的关系
这两个概念经常被搞混。再说清楚一次:
- 柯里化是函数的定义方式------把参数拆成多个列表
- 偏应用是函数的使用方式------固定一部分参数,得到新函数
它们的关系:柯里化函数天然支持偏应用------因为每个参数列表本来就是"先传一部分"。
scala
// 柯里化定义
def add(a: Int)(b: Int): Int = a + b
// 自然偏应用
val addOne = add(1) _
// 普通函数也能偏应用,但要用_
def add2(a: Int, b: Int): Int = a + b
val addOne2 = add2(1, _: Int)
简单说:柯里化让偏应用变得自然------不用写_: 类型。
几个常见的坑
坑1:不能省括号
scala
addCurried(1)(2) // 可以
addCurried(1, 2) // 报错------柯里化是两个列表,不是一个列表两个参数
坑2:只传第一个列表会得到函数
scala
val f = addCurried(1)
// f是Int => Int,不是Int------你还要再调用一次f(2)
很多人第一次用柯里化的时候,忘了传第二个参数列表,结果得到一个函数而不是值。
坑3:不要滥用柯里化
不是所有函数都该柯里化。如果你的参数总是一起传------没有"先传一部分"的场景------那就用普通参数列表。柯里化是为偏应用和控制抽象服务的,不是为了好看。
什么时候用柯里化?
| 场景 | 用柯里化吗? |
|---|---|
| 需要偏应用------先固定一部分参数 | 用 |
| 写自定义控制结构(myWhile这类) | 用 |
| 需要帮助类型推断(foldLeft这类) | 用 |
| 有implicit/using参数 | 必须用 |
| 参数总是一次传完 | 不用 |
| 简单的普通函数 | 不用 |
判断标准:你有没有"参数分阶段传递"的需求? 有就柯里化,没有就普通写法。
写在最后
我写这篇文章的时候,特意没罗列柯里化有多少种写法------因为写法很简单。
真正值得理解的是:柯里化为什么是函数式编程的一个核心概念?
因为函数式编程里,函数是一等公民------函数可以返回函数。柯里化就是这个特性的直接体现:一个多参数函数,可以看成一串单参数函数的链。
这带来了什么?------参数可以分阶段传递。
你先传配置,得到一个"记住了配置"的函数;以后再传数据。你先传条件,得到一个控制结构;再传执行体。
Java里你做不到------Java的方法必须一次传完所有参数。你想"分阶段",只能用Builder模式,手动写一堆对象。
Scala用柯里化把这件事变成了语言层面的自然能力------不需要Builder,不需要工厂对象,函数本身就支持分阶段。
柯里化不是炫技------它是"函数可以返回函数"这个根本特性的语法表达。它让函数的组合、偏应用、控制抽象都变得自然。
当你写出foldLeft(0)(_ + _)、写出myWhile(cond) { body }的时候,你会明白:参数列表的边界,不只是参数的分组------它是函数变换的边界。