
什么是柯里化?
柯里化(Currying)是一种把"接收多个参数的函数",转换成 "一系列每次只接收一个参数的函数" 的技术。
- 定义:将 f(a, b, c) 转化为 f(a)(b)(c)
- 本质:利用闭包保存参数。
- 作用:固定部分参数以复用代码、延迟计算、方便函数组合。
为什么要柯里化?
-
柯里化的核心优势在于 参数复用 (让代码更具声明式特点,可读性更高) 和 延迟执行。
- 参数复用:
javascript// 假设有一个通用的正则验证函数 check(reg, txt) const check = reg => txt => reg.test(txt); // 我们可以生成具体的验证函数 const checkEmail = check(/^\S+@\S+$/); const checkPhone = check(/^1\d{10}$/); // 以后使用时,只需要传具体的内容,逻辑非常清晰 checkEmail('test@gmail.com'); checkPhone('13800138000');- 延迟执行: 你不需要一次性集齐所有参数。你可以先传一部分参数,让程序"记住"这种状态,等到合适的时机(剩下的参数准备好时)再计算最终结果。
- 函数组合: 在函数式编程中,我们经常需要把多个简单的函数组合成一个复杂的函数(比如 f(g(x)))。因为柯里化后的函数总是只接受一个参数,这使得它们非常容易像积木一样拼在一起。
-
提高函数复用性
-
更符合函数式编程(无状态、可组合、易测试)
-
让代码语义更清晰
jsrequest("GET")("/api/user")({ timeout: 1000 });
柯里化在 Haskell、Lisp 等纯函数式语言中是默认行为,Scala 也是语言层面原生支持,而在 JavaScript、Python 等语言中则是一种强大的编程技巧。
Scala(原生):参数列表天然支持多段、类型系统知道这是柯里化函数:
scala
def add(a: Int)(b: Int) = a + b
add(1)(2)
Haskell(彻底原生):所有函数默认柯里化、多参数只是语法糖:
haskell
add a b = a + b
add 1 2
(add 1) 2
Java 方法的柯里化
Java 不是原生支持柯里化的语言,依赖 Java 8 引入的 Lambda 表达式 和 函数式接口(Function)来模拟,间接实现。
由于 Java 可读性、类型冗长问题,柯里化不是 Java 的强项。由于语言层面的限制,Java 没有自动类型推导,像(Scala / Kotlin),没有函数是"一等公民"的完整体验,泛型 + Function 嵌套非常啰嗦,导致 Java 不"推崇"柯里化,Java 更推崇偏函数(准确说是偏应用),参考后续文章。Java 设计哲学偏:命令式、显式、可维护性 > 抽象美感。
不推荐:业务代码、CRUD、大多数后端服务逻辑使用柯里化,推荐:构建 DSL、高阶函数库(传入函数作为参数或返回函数的函数)、Stream / Pipeline 中的函数组合、需要"延迟绑定参数"使用柯里化。
java
import java.util.function.Function;
public class LogDemo {
public static void main(String[] args) {
// 定义一个柯里化的日志函数
// 第一层:接收日志级别 (level)
// 第二层:接收具体消息 (message)
Function<String, Function<String, String>> logFormatter =
level -> message -> "[" + level + "] " + message;
// 场景:我们需要频繁打印 ERROR 级别的日志
// 我们可以"固定"第一个参数,生成一个专门打印 Error 的函数
Function<String, String> errorLogger = logFormatter.apply("ERROR");
Function<String, String> infoLogger = logFormatter.apply("INFO");
// 以后使用时,只需要传消息内容
System.out.println(errorLogger.apply("数据库连接失败"));
System.out.println(errorLogger.apply("空指针异常"));
System.out.println(infoLogger.apply("系统启动成功"));
}
}
如果参数多,类型定义会变得非常长且难看:
java
Function<Integer,
Function<Integer,
Function<Integer, Integer>>> sum3 =
a -> b -> c -> a + b + c;
int r = sum3.apply(1).apply(2).apply(3);
缺点(相比 JS 或 Haskell):
- 啰嗦:类型签名非常长(
Function<T, Function<...>>),可读性差。 - 调用繁琐:必须显式调用
.apply(),不能像 JS 那样直接括号func(a)(b),而是func.apply(a).apply(b)。 - 性能:每次调用都会创建新的 Function 对象,在极端高性能敏感的场景下可能有微小的开销(通常可忽略)。
接下来看看我最喜欢的两门语言 Go 和 Kotlin 的柯里化。
Go 函数的柯里化
Go 语言没有原生的柯里化语法或机制,只能通过闭包和高阶函数来手动模拟。
普通手写柯里化版本:
比如一个 logger:
go
func Log(level, module, msg string) {
fmt.Println(level, module, msg)
}
柯里化它:
go
func CurryLog(level string) func(string) func(string) {
return func(module string) func(string) {
return func(msg string) {
fmt.Println(level, module, msg)
}
}
}
使用:
go
info := CurryLog("INFO")
err := CurryLog("ERROR")
dbInfo := info("DB")
dbErr := err("DB")
dbInfo("connected")
dbInfo("query failed")
dbErr("add failed")
dbErr("update failed")
半通用转换器版本:
写一个辅助函数,将任意符合特定签名的双参数函数转换为柯里化形式。
go
package main
import "fmt"
// Curry 是一个通用转换器
// 它接收一个形式为 func(A, B) C 的函数
// 返回 func(A) func(B) C
func Curry[A, B, C any](f func(A, B) C) func(A) func(B) C {
return func(a A) func(B) C {
return func(b B) C {
return f(a, b)
}
}
}
func Multiply(a, b int) int {
return a * b
}
func main() {
// 将普通函数转换为柯里化函数
curriedMult := Curry(Multiply)
// 创建一个"双倍器"
doubler := curriedMult(2)
fmt.Println(doubler(5))
fmt.Println(doubler(10))
}
遗憾的是,Go 没有像 JS 那样的可变参数,不能柯里化任意长度参数列表的函数。
还有一个用泛型加反射实现的通用版本(不推荐):
go
package curry
import "reflect"
func Curry(fn any) func(any) any {
fnVal := reflect.ValueOf(fn)
fnType := fnVal.Type()
if fnType.Kind() != reflect.Func {
panic("Curry expects a function")
}
arity := fnType.NumIn()
var curry func(args []reflect.Value) func(any) any
curry = func(args []reflect.Value) func(any) any {
return func(arg any) any {
newArgs := append(args, reflect.ValueOf(arg))
if len(newArgs) < arity {
return curry(newArgs)
}
// 参数齐了,调用函数
results := fnVal.Call(newArgs)
// 只支持 0 或 1 返回值
if len(results) == 0 {
return nil
}
return results[0].Interface()
}
}
return curry(nil)
}
使用示例:
go
func Add(a, b, c int) int {
return a + b + c
}
func main() {
c := Curry(Add)
f1 := c(1).(func(any) any)
f2 := f1(2).(func(any) any)
result := f2(3).(int)
fmt.Println(result)
}
支持不同类型参数:
go
func Mix(a int, b string, c float64) string {
return fmt.Sprintf("%d-%s-%.2f", a, b, c)
}
c := Curry(Mix)
r := c(1).(func(any) any)("go").(func(any) any)(3.14).(string)
⚠️ 注意:这个版本不优雅、不安全、不惯用,只能算"技术演示级别",不适合生产。技术上可实现(反射 + any),工程上不值得,Go 风格不推荐。
-
它完全丢失静态类型
你看到的类型链是:
gofunc(any) anyGo 编译器:
- 无法检查参数类型
- 无法检查返回类型
- IDE 无法补全
-
性能很差
- reflect.ValueOf
- reflect.Call
- interface{} 装箱/拆箱
在热点路径上是灾难级别。
-
错误全在运行时爆炸
goc("hello") // panic不是编译期错误,而是 runtime panic。
柯里化在 Go 中的缺点:
虽然能做,但在 Go 中滥用柯里化会被视为 非惯用做法,原因如下:
-
类型签名复杂:
- 普通:func(int, int, int) int
- 柯里化:func(int) func(int) func(int) int
- 这对阅读代码的人来说是折磨,而且 Go 不支持类型推导省略函数签名。
-
性能损耗:
每次调用柯里化函数都会涉及到内存分配(闭包对象)和函数调用开销。在高性能要求的场景(如紧密循环)中,这比直接传参要慢。
-
调试困难:
堆栈跟踪会因为层层嵌套的匿名函数变得很深且难以阅读。
为什么 Go 不原生支持柯里化?
这是 Go 的设计取向决定的:
Go 追求的是:
- 显式
- 简单
- 可读
- 易于静态分析和优化
而柯里化依赖:
- 隐式闭包
- 层层返回函数
- 动态函数组合
这些在 Go 的设计哲学里被认为是可读性和可维护性成本过高。
为什么泛型救不了 curry ?
Go 泛型不能:
- 解构函数参数列表
- 操作 variadic type list
- 根据函数签名生成新函数类型
Go 没有 Type-Level Programming。
Go 里真正"惯用"的写法
Go 程序员更倾向写:
go
func WithDB(db *DB) func(Request) Response {
return func(req Request) Response {
...
}
}
这是 Go 风格的依赖注入。
这本质就是:
柯里化 + 偏应用
只是 Go 不用这个词。
什么时候用?
- 不要在简单的逻辑计算中使用(不要为了炫技而柯里化)。
- 推荐使用场景:中间件设计、依赖注入、参数预设(偏函数应用)、接口适配器。
Kotlin 函数的柯里化
Kotlin 不原生支持自动柯里化(不像 Haskell),但语言特性非常适合手动柯里化,而且写法相当自然。
手动柯里化
kotlin
// 普通多参数函数
fun sum(x: Int, y: Int, z: Int) = x + y + z
// 柯里化
fun sumCurried(x: Int) = { y: Int -> { z: Int -> x + y + z } }
// 带返回值写法
// fun sumCurried(x: Int): (Int) -> (Int) -> Int = { y -> { z -> x + y + z } }
// 或者函数表达式写法
// val sumCurried: (Int) -> (Int) -> Int = { x -> { y -> { z -> x + y + z } } }
// val sumCurried = { x: Int -> { y: Int -> { z: Int -> x + y + z } } }
// 调用
val sum1And = sumCurried(1)
val sum1And2And = sum1And(2)
var res = sum1And2And(3)
// 或者链式调用
res = sumCurried(1)(2)(3)
println(res)
通用柯里化扩展函数
泛型柯里化工具函数(非常实用)
kotlin
fun <X, Y, Z, R> ((X, Y, Z) -> R).curry() = { x: X -> { y: Y -> { z: Z -> this(x, y, z) } } }
// 使用
val sum = { x: Int, y: Int, z: Int -> x + y + z }
val curriedSum = sum.curry()
val res = curriedSum(1)(2)(3)
println(res)
扩展:还有反柯里化函数:
kotlin
fun <X, Y, Z, R> ((X) -> (Y) -> (Z) -> R).uncurry() = { x: X, y: Y, z: Z -> this(x)(y)(z) }
使用 Arrow 库柯里化 (推荐)
如果你在 Kotlin 中大量使用函数式编程,建议使用 Arrow 库(Kotlin 事实上的标准 FP 库)。它内置了强大的柯里化支持。
kotlin
import arrow.core.curried
val add = { a: Int, b: Int -> a + b }
val curriedAdd = add.curried()
总结
- 柯里化将 (A, B) -> C 转化为 (A) -> (B) -> C。
- 实现方式:利用 Kotlin 的高阶函数,返回 Lambda 嵌套。
- 核心价值:实现参数复用和延迟执行,通过固定部分参数来创建更具体的函数(偏函数)。
JS 函数的柯里化
JavaScript 不是原生柯里化(Auto-curried)语言,在语言层面也没有像 Haskell 或 ML 那样"默认自动柯里化"的机制。 在 JavaScript 中,柯里化只是一种设计模式或编程技巧,而不是语言的默认行为。
虽然 JS 不是默认柯里化的,但现代 JavaScript (ES6+) 提供了很多语法糖,使得手动实现柯里化变得非常简便和优雅。只能说 JS 的动态类型和现代的简洁优雅的语法特性,使其特别容易实现柯里化。
A. 箭头函数
这是 JS 对柯里化最友好的语法支持。通过嵌套箭头函数,可以极其简洁地定义柯里化函数。
js
// 现在的写法(极其接近数学定义)
const add = x => y => z => x + y + z;
// 使用
add(1)(2)(3);
B. Function.prototype.bind (偏函数应用)
虽然 bind 严格来说实现的是偏函数应用 (Partial Application),但它常被用来达到类似柯里化的效果(固定部分参数)。
js
function add(x, y) {
return x + y;
}
// 锁死第一个参数 x 为 10
const addTen = add.bind(null, 10);
console.log(addTen(5));
C. 通用柯里化转换函数实现自动柯里化
如果你想让普通的 JS 函数变成"不够参数就返回函数,够了就执行"的状态,需要写一个包装器:
js
const curry = (fn) => {
return function curried(...args) {
if (args.length >= fn.length) {
return fn(...args);
}
return (...more) => curried(...args, ...more);
};
};
const add = (a, b, c) => a + b + c;
const curriedAdd = curry(add);
curriedAdd(1)(2)(3);
curriedAdd(1, 2)(3);
curriedAdd(1)(2, 3);
总结:
- JS 不是原生柯里化语言:默认调用 f(a) 时,如果 f 需要两个参数,JS 不会返回函数,而是报错或计算出错误结果(NaN)。
- 没有专门的关键字:没有类似 curry function 这样的关键字。
- 语法支持不错:箭头函数 (a => b => c) 让手动写柯里化函数变得很轻松,这是目前 JS 中柯里化的主流实现方式。