科里化函数的作用
基本功能
Currying (科里化) 是函数式编程中的常见操作, 其作用是将多参数的函数改写成高阶函数[1]. 例如:
1 | func foo(_ a: Int, _ b: Int) -> Int{ |
语法角度, 科里化的最终目的即是通过创建新的 closure 并 capture 部分参数, 将原本同一层级的输入参数分离开来.
我们可以编写泛型函数, 来统一将特定参数个数的函数进行科里化.
1 | func curried2<T, U, V>( |
同样也有逆向操作.
1 | func uncurried2<T, U, V>( |
Swift 5.9 之后支持了以 value/type packs 的方式编写 variadic generics 函数, 可以用来统一实现任意个数参数的 curried
. 注: 当前 (Xcode 15.0) 针对泛型 closure 可能有一些问题, 下面的 code 在 playground 中不能正确编译, 需要放到 project 里.
1 | // variadic version |
Swift 中的科里化
curried
方法因为把同一级的输入参数进行了区分, 而 uncurried
方法把不同级的参数进行打平, 因此两者均可以帮我们实现函数类型的适配.
一个简单的例子:
1 | struct BankAccount { |
希望对 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 | func methodA(_ x: Int, y: Double) -> Product |
这两个看似较难等同起来的函数, 经过 currying 后的结果:
1 | curried(methodA) // 类型为 (Int) -> (Double) -> Product |
无论他们嵌套多少层, 都可以统一用泛型语义中的 (T) -> U
来表示了.
这样, 配合一些特别定制的高阶函数操作, 就可以用少量的函数实现大量的功能, 在下面这个略显强凑的例子中可以体现科里化的作用.
1 | // 函数库 |
这里函数库 API 设计的核心思想就是通过 currying 统一各位置参数传递的形式, 将 combine
原本需要一并传参的调用方式, 转换成可以使用自定义运算符 <+>
逐级组装的形式, 最后实现了一套特定的 DSL, 而这样的 DSL 在某些方面拥有使用上的优势. 许多开源泛型函数库中, 都有类似的影子.
在早期版本的 Swift (< 3.0) 中, 对于科里化的函数有特殊写法, 现在已经移除 ↩︎