🎼 Overture
一个用于函数组合的库。
目录
动机
我们经常使用函数,但函数组合无处不在!
例如,当我们使用数组上的高阶方法,如map
时,我们使用函数。
[1, 2, 3].map { $0 + 1 }
// [2, 3, 4]
如果我们想修改这个简单的闭包,在增加值后平方它,事情开始变得混乱。
[1, 2, 3].map { ($0 + 1) * ($0 + 1) }
// [4, 9, 16]
函数允许我们识别和提取可重用的代码。让我们定义几个组成上述行为的函数。
func incr(_ x: Int) -> Int {
return x + 1
}
func square(_ x: Int) -> Int {
return x * x
}
定义了这些函数后,我们可以直接将它们传递给map
!
[1, 2, 3]
.map(incr)
.map(square)
// [4, 9, 16]
这次重构更容易理解,但性能较差:我们正在两次遍历数组,并在过程中创建了一个中间副本!虽然我们可以使用lazy
来融合这些调用,但让我们采取更通用的方法:函数组合!
[1, 2, 3].map(pipe(incr, square))
// [4, 9, 16]
pipe
函数将其他函数粘合在一起!它可以接受超过两个参数,甚至可以更改类型!
[1, 2, 3].map(pipe(incr, square, String.init))
// ["4", "9", "16"]
函数组合让我们可以从更小的部分构建新的函数,这使得我们能够在其他上下文中提取和重用逻辑。
let computeAndStringify = pipe(incr, square, String.init)
[1, 2, 3].map(computeAndStringify)
// ["4", "9", "16"]
computeAndStringify(42)
// "1849"
函数是最小的代码构建块。函数组合让我们能够将这些块组合起来,并从小的、可重用的、可理解的单元构建整个应用程序。
示例
pipe
在Overture中最基本的构建块。它将现有函数合并在一起。也就是给定一个函数 (A) -> B
和一个函数 (B) -> C
,pipe
将返回一个新的 (A) -> C
函数。
let computeAndStringify = pipe(incr, square, String.init)
computeAndStringify(42)
// "1849"
[1, 2, 3].map(computeAndStringify)
// ["4", "9", "16"]
with
with
函数用于对值应用函数。它与 inout
和可变对象世界配合良好,将在表达式内包含命令式配置。
class MyViewController: UIViewController {
let label = with(UILabel()) {
$0.font = .systemFont(ofSize: 24)
$0.textColor = .red
}
}
并且它恢复了我们从方法世界所熟悉的自左向右的可读性。
with(42, pipe(incr, square, String.init))
// "1849"
concat
concat
函数与单类型组合。这包括以下函数签名的组合:
(A) -> A
(inout A) -> Void
<A: AnyObject>(A) -> Void
使用 concat
,我们可以从小部分构建强大的配置函数。
let roundedStyle: (UIView) -> Void = {
$0.clipsToBounds = true
$0.layer.cornerRadius = 6
}
let baseButtonStyle: (UIButton) -> Void = {
$0.contentEdgeInsets = UIEdgeInsets(top: 12, left: 16, bottom: 12, right: 16)
$0.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
}
let roundedButtonStyle = concat(
baseButtonStyle,
roundedStyle
)
let filledButtonStyle = concat(roundedButtonStyle) {
$0.backgroundColor = .black
$0.tintColor = .white
}
let button = with(UIButton(type: .system), filledButtonStyle)
curry
、flip
和 zurry
这些函数构成了组合的瑞士军刀。它们使我们可以取得那些不组合的现有函数和方法(例如,那些取零个或多个参数的函数),并恢复组合。
例如,让我们将一个接受多个参数的字符串初始化器转换为一个可以与 pipe
组合的东西。
String.init(data:encoding:)
// (Data, String.Encoding) -> String?
我们使用 curry
将多参数函数转换为单个输入的函数,并返回新的函数来收集沿途更多的输入。
curry(String.init(data:encoding:))
// (Data) -> (String.Encoding) -> String?
并且我们使用 flip
来颠倒参数的顺序。多参数函数和方法通常先将数据放在第一位,然后再是配置,但我们可以先应用配置,然后再有数据,而 flip
允许我们做到这一点。
flip(curry(String.init(data:encoding:)))
// (String.Encoding) -> (Data) -> String?
现在我们有一个高度可重用、可组合的构建块,我们可以用它来构建管道。
let stringWithEncoding = flip(curry(String.init(data:encoding:)))
// (String.Encoding) -> (Data) -> String?
let utf8String = stringWithEncoding(.utf8)
// (Data) -> String?
Swift 还将方法公开为静态、非绑定函数。这些函数已处于柯里化形式。我们只需要将它们进行 flip
操作,使它们更加有用!
String.capitalized
// (String) -> (Locale?) -> String
let capitalized = flip(String.capitalized)
// (Locale?) -> (String) -> String
["hello, world", "and good night"]
.map(capitalized(Locale(identifier: "en")))
// ["Hello, World", "And Good Night"]
而 zurry
为接受零参数的函数和方法恢复了组合。
String.uppercased
// (String) -> () -> String
flip(String.uppercased)
// () -> (String) -> String
let uppercased = zurry(flip(String.uppercased))
// (String) -> String
["hello, world", "and good night"]
.map(uppercased)
// ["HELLO, WORLD", "AND GOOD NIGHT"]
get
get
函数生成从键路径生成的 获取器函数。
get(\String.count)
// (String) -> Int
["hello, world", "and good night"]
.map(get(\.count))
// [12, 14]
我们甚至可以通过使用 pipe
函数将其他函数组合到 get
中。这里我们构建了一个函数,该函数将整数递增、平方、转换为字符串,然后获取字符串的字符数。
pipe(incr, square, String.init, get(\.count))
// (Int) -> Int
prop
prop
函数生成从键路径生成的 设置器函数。
let setUserName = prop(\User.name)
// ((String) -> String) -> (User) -> User
let capitalizeUserName = setUserName(capitalized(Locale(identifier: "en")))
// (User) -> User
let setUserAge = prop(\User.age)
let celebrateBirthday = setUserAge(incr)
// (User) -> User
with(User(name: "blob", age: 1), concat(
capitalizeUserName,
celebrateBirthday
))
// User(name: "Blob", age: 2)
over
和 set
over
和 set
函数生成对给定键路径(或 设置器函数)中的一个结构中的 Value
进行操作的 (Root) -> Root
转换函数。
over
函数接受一个将修改现有值的 (Value) -> Value
转换函数。
let celebrateBirthday = over(\User.age, incr)
// (User) -> User
set
函数用全新的值替换现有值。
with(user, set(\.name, "Blob"))
mprop
、mver
和 mut
mprop
、mver
和 mut
函数是 prop
、over
和 set
的可变版本。
let guaranteeHeaders = mver(\URLRequest.allHTTPHeaderFields) { $0 = $0 ?? [:] }
let setHeader = { name, value in
concat(
guaranteeHeaders,
{ $0.allHTTPHeaderFields?[name] = value }
)
}
let request = with(URLRequest(url: url), concat(
mut(\.httpMethod, "POST"),
setHeader("Authorization", "Token " + token),
setHeader("Content-Type", "application/json; charset=utf-8")
))
zip
和 zip(with:)
这是 Swift 提供的函数!不幸的是,它仅适用于序列对。Overture 将 zip
定义为一次可处理多达十组序列,这使得合并多个相关数据集变得非常简单。
let ids = [1, 2, 3]
let emails = ["[email protected]", "[email protected]", "[email protected]"]
let names = ["Blob", "Blob Junior", "Blob Senior"]
zip(ids, emails, names)
// [
// (1, "[email protected]", "Blob"),
// (2, "[email protected]", "Blob Junior"),
// (3, "[email protected]", "Blob Senior")
// ]
通常立即在压缩后的值上使用 map
。
struct User {
let id: Int
let email: String
let name: String
}
zip(ids, emails, names).map(User.init)
// [
// User(id: 1, email: "[email protected]", name: "Blob"),
// User(id: 2, email: "[email protected]", name: "Blob Junior"),
// User(id: 3, email: "[email protected]", name: "Blob Senior")
// ]
因此,Overture 提供了一个 zip(with:)
辅助函数,它接受一个转换函数作为前参数,并使用柯里化(currying),因此它可以与其他函数一起使用 pipe
。
zip(with: User.init)(ids, emails, names)
Overture 还将 zip
的概念扩展到可选值(optionals)!它是一种结合多个可选值的高效方式。
let optionalId: Int? = 1
let optionalEmail: String? = "[email protected]"
let optionalName: String? = "Blob"
zip(optionalId, optionalEmail, optionalName)
// Optional<(Int, String, String)>.some((1, "[email protected]", "Blob"))
而且,zip(with:)
允许我们将这些元组转换为其他值。
zip(with: User.init)(optionalId, optionalEmail, optionalName)
// Optional<User>.some(User(id: 1, email: "[email protected]", name: "Blob"))
使用 zip
可以是 let
-解包(unwrapping)的另一种表达方式!
let optionalUser = zip(with: User.init)(optionalId, optionalEmail, optionalName)
// vs.
let optionalUser: User?
if let id = optionalId, let email = optionalEmail, let name = optionalName {
optionalUser = User(id: id, email: email, name: name)
} else {
optionalUser = nil
}
常见问题解答(FAQ)
-
我应该担心会污染全局命名空间,使用自由函数(free functions)吗?
不用担心!Swift 有许多作用域层次结构来帮助您。
- 您可以使用
fileprivate
和private
作用域,限制超出现有文件的高特定函数的公开。 - 您可以将函数定义为类型内部的
static
成员。 - 您可以用模块的名称来限定函数(例如,
Overture.pipe(f, g)
)。您甚至可以从模块名中自动完成自由函数,因此发现性(discoverability)不必受损!
- 您可以使用
-
自由函数在 Swift 中很常见吗?
虽然看起来可能不是这样,但自由函数在 Swift 中无处不在,这使得 Overture 非常有用!下面是一些例子
- 初始化器,如
String.init
。 - 未绑定方法,如
String.uppercased
。 - 带关联值的枚举实例,如
Optional.some
。 - 我们传递给
map
、filter
和其他高阶方法的临时闭包。 - 顶级标准库函数,如
max
、min
和zip
。
- 初始化器,如
安装
Carthage
如果您使用 Carthage,您可以在 Cartfile
中添加以下依赖项
github "pointfreeco/swift-overture" ~> 0.3
CocoaPods
如果你的项目使用 CocoaPods,只需在 Podfile
中添加以下内容
pod 'Overture', '~> 0.3'
SwiftPM
如果你想在使用 SwiftPM 的项目中使用 Overture,只需在 Package.swift
中添加一个 dependencies
子句
dependencies: [
.package(url: "https://github.com/pointfreeco/swift-overture.git", from: "0.3.1")
]
Xcode 子项目
将 Overture 复制到子模块内,或者下载它,并将 Overture.xcodeproj
拖到你的项目中。
🎶 序言
这个库是作为对 swift-prelude 的替代品而创建的,它使用中缀运算符提供了这些工具(以及更多)。例如,pipe
实际上是箭头组合运算符 >>>
,这意味着以下内容是等价的
xs.map(incr >>> square)
xs.map(pipe(incr, square))
我们都知道,很多代码库可能不会舒服地引入运算符,所以我们希望降低拥抱函数组合的门槛。
想了解更多吗?
这些概念(以及更多)在 Point-Free 中得到充分探讨,这个视频系列探讨了函数式编程和 Swift,由 Brandon Williams 和 Stephen Celis 主持。
本期节目中的想法最初在 第 11 集中 进行了探讨。
许可证
所有模块均以 MIT 许可证发布。有关详细信息,请参阅 LICENSE 文件。