科里化函数的作用


科里化函数的作用

基本功能

Currying (科里化) 是函数式编程中的常见操作, 其作用是将多参数的函数改写成高阶函数[1]. 例如:

1
2
3
4
5
6
7
8
9
func foo(_ a: Int, _ b: Int) -> Int{
a + b
}

func curriedFoo(_ a: Int) -> (Int) -> Int {
return { b in
a + b
}
}

语法角度, 科里化的最终目的即是通过创建新的 closure 并 capture 部分参数, 将原本同一层级的输入参数分离开来.

我们可以编写泛型函数, 来统一将特定参数个数的函数进行科里化.

1
2
3
4
5
6
7
8
9
10
11
12
func curried2<T, U, V>(
_ originalFunc: @escaping (T, U) -> V
) -> (T) -> (U) -> V {
return { (param1: T) in
return { (param2: U) in
originalFunc(param1, param2)
}
}
}

let curriedFoo = curried2(foo)
curriedFoo(3)(4)

同样也有逆向操作.

1
2
3
4
5
6
7
func uncurried2<T, U, V>(
_ originalFunc: @escaping (T) -> (U) -> V
) -> (T, U) -> V {
return { lhs, rhs in
originalFunc(lhs)(rhs)
}
}

Swift 5.9 之后支持了以 value/type packs 的方式编写 variadic generics 函数, 可以用来统一实现任意个数参数的 curried. 注: 当前 (Xcode 15.0) 针对泛型 closure 可能有一些问题, 下面的 code 在 playground 中不能正确编译, 需要放到 project 里.

1
2
3
4
5
6
7
8
9
10
// variadic version
func curried<First, each T, Result>(
_ originalFunc: @escaping (First, repeat each T) -> Result
) -> (First) -> (repeat each T) -> Result {
return { (first: First) in
return { (remainings: repeat each T) -> Result in
return originalFunc(first, repeat each remainings)
}
}
}

Swift 中的科里化

curried 方法因为把同一级的输入参数进行了区分, 而 uncurried 方法把不同级的参数进行打平, 因此两者均可以帮我们实现函数类型的适配.

一个简单的例子:

1
2
3
4
5
6
7
8
9
struct BankAccount {
let value: Int

func compare(another account: BankAccount) -> Bool {
value > account.value
}
}

let accounts = [BankAccount(value: 100), BankAccount(value: 200), BankAccount(value: 300)]

希望对 accounts 进行排序.

在 Swift 中对一个成员函数的引用有多种形式, 假设类型 T 有成员函数 foo(A, B)

  • 对于特定的 T 类型的对象 t, 调用 t.foo(a, b) 称为 full application
  • 如果不提供成员函数的显式参数, 只提供隐式目标对象参数 self, 即 t.foo, 称为 partial application
  • 如果连目标对象也不提供, 即 T.foo, 称为 non-applied

上述 foo 函数 partial application 后表达式的类型为 (A, B) -> Void, 而 non-applied 形式的类型并非 (T, A, B) -> Void, 而是它的科里化形式 (T) -> (A, B) -> Void.

因此, 为了解决上问排序问题, 不必显式创建一个临时的 closure, 而是可以直接通过 uncurried 方法, 将对成员函数的 non-applied 形式转化成 sort 需要的形式即可.

1
let sorted = accounts.sorted(by: uncurried2(BankAccount.compare))

泛型函数库中的应用

正由于科里化可以把同一级别的参数转换成不同级别, 这反而某种程度上说, 可以把各种参数个数不同的方法转换到同一形式的签名下. 例如:

1
2
func methodA(_ x: Int, y: Double) -> Product
func methodB(_ x: String, y: Int, z: Double) -> Product

这两个看似较难等同起来的函数, 经过 currying 后的结果:

1
2
curried(methodA)  // 类型为 (Int) -> (Double) -> Product
curried(methodB) // 类型为 (String) -> (Int) -> (Double) -> Product

无论他们嵌套多少层, 都可以统一用泛型语义中的 (T) -> U 来表示了.

这样, 配合一些特别定制的高阶函数操作, 就可以用少量的函数实现大量的功能, 在下面这个略显强凑的例子中可以体现科里化的作用.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 函数库
struct Product { ... }
struct Step<T> { ... }
// 自定义运算符
infix operator <+>: SequencePrecedence
// 脚手架函数
func <+> <T, R>(_ function: Step<(T) -> R>, _ step: Step<T>) -> Step<R> { ... }
func prepare<T>(_ function: T) -> Step<T> { ... }

// 各种预置或自定义的组件
let a: Step<Int>
let b: Step<String>
let c: Step<Double>

// 各种预置或自定义的流程
func combine(a: Int, b: String, c: Double) -> Product { ... }

// 用户编写的代码:
let totalStep = prepare(curried3(combine)) <+> a <+> b <+> c

这里函数库 API 设计的核心思想就是通过 currying 统一各位置参数传递的形式, 将 combine 原本需要一并传参的调用方式, 转换成可以使用自定义运算符 <+> 逐级组装的形式, 最后实现了一套特定的 DSL, 而这样的 DSL 在某些方面拥有使用上的优势. 许多开源泛型函数库中, 都有类似的影子.


  1. 在早期版本的 Swift (< 3.0) 中, 对于科里化的函数有特殊写法, 现在已经移除 ↩︎