一个用于函数组合的库。
我们一直都在使用函数,但函数组合就隐藏在眼前!
例如,当我们使用高阶方法时,比如数组上的 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"
函数是代码的最小构建块。函数组合使我们能够将这些块组合在一起,并用小的、可重用的、可理解的单元构建整个应用程序。
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
和 update
函数对于将函数应用于值非常有用。它们与 inout
和可变对象世界配合良好,将原本命令式的配置语句包装在一个表达式中。
class MyViewController: UIViewController {
let label = updateObject(UILabel()) {
$0.font = .systemFont(ofSize: 24)
$0.textColor = .red
}
}
并且它恢复了我们从方法世界中习惯的从左到右的可读性。
with(42, pipe(incr, square, String.init))
// "1849"
使用 inout
参数。
update(&user, mut(\.name, "Blob"))
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)
这些函数构成了组合的瑞士军刀。它们使我们能够使用现有的不组合的函数和方法(例如,那些接受零个或多个参数的函数和方法)并恢复组合。
例如,让我们将一个接受多个参数的字符串初始化器转换为可以与 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
函数从键路径生成getter 函数。
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
函数从键路径生成setter 函数。
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
函数生成 (Root) -> Root
转换函数,这些函数在给定键路径(或setter 函数)的结构中的 Value
上工作。
over
函数接受一个 (Value) -> Value
转换函数来修改现有值。
let celebrateBirthday = over(\User.age, incr)
// (User) -> User
set
函数用一个全新的值替换现有值。
with(user, set(\.name, "Blob"))
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 = update(
URLRequest(url: url),
mut(\.httpMethod, "POST"),
setHeader("Authorization", "Token " + token),
setHeader("Content-Type", "application/json; charset=utf-8")
)
这是一个 Swift 自带的函数!不幸的是,它仅限于序列对。Overture 定义了 zip
以一次处理最多十个序列,这使得组合几组相关数据变得轻而易举。
let ids = [1, 2, 3]
let emails = ["blob@pointfree.co", "blob.jr@pointfree.co", "blob.sr@pointfree.co"]
let names = ["Blob", "Blob Junior", "Blob Senior"]
zip(ids, emails, names)
// [
// (1, "blob@pointfree.co", "Blob"),
// (2, "blob.jr@pointfree.co", "Blob Junior"),
// (3, "blob.sr@pointfree.co", "Blob Senior")
// ]
通常立即对 zipped 值进行 map
操作。
struct User {
let id: Int
let email: String
let name: String
}
zip(ids, emails, names).map(User.init)
// [
// User(id: 1, email: "blob@pointfree.co", name: "Blob"),
// User(id: 2, email: "blob.jr@pointfree.co", name: "Blob Junior"),
// User(id: 3, email: "blob.sr@pointfree.co", name: "Blob Senior")
// ]
因此,Overture 提供了一个 zip(with:)
助手,它预先接受一个转换函数并且是柯里化的,因此它可以与使用 pipe
的其他函数组合。
zip(with: User.init)(ids, emails, names)
Overture 还扩展了 zip
的概念以处理可选值!这是一种将多个可选值组合在一起的富有表现力的方式。
let optionalId: Int? = 1
let optionalEmail: String? = "blob@pointfree.co"
let optionalName: String? = "Blob"
zip(optionalId, optionalEmail, optionalName)
// Optional<(Int, String, String)>.some((1, "blob@pointfree.co", "Blob"))
zip(with:)
让我们将这些元组转换为其他值。
zip(with: User.init)(optionalId, optionalEmail, optionalName)
// Optional<User>.some(User(id: 1, email: "blob@pointfree.co", name: "Blob"))
使用 zip
可以作为 let
解包的富有表现力的替代方案!
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
}
我应该担心使用自由函数污染全局命名空间吗?
不!Swift 有几个作用域层级来帮助你。
fileprivate
和 private
作用域来限制在单个文件之外公开高度特定的函数。static
成员。Overture.pipe(f, g)
)。你甚至可以从模块的名称自动完成自由函数,因此发现性不必受到影响!自由函数在 Swift 中很常见吗?
可能看起来不像,但自由函数在 Swift 中无处不在,这使得 Overture 非常有用!一些例子
String.init
。String.uppercased
。Optional.some
。map
、filter
和其他高阶方法的临时闭包。max
、min
和 zip
。你可以通过将 Overture 添加为包依赖项来将其添加到 Xcode 项目中。
如果你想在 SwiftPM 项目中使用 Overture,只需将其添加到 Package.swift
中的 dependencies
子句中即可
dependencies: [
.package(url: "https://github.com/pointfreeco/swift-overture", from: "0.5.0")
]
这个库是作为 swift-prelude 的替代品而创建的,这是一个使用中缀运算符的实验性函数式编程库。例如,pipe
不是别的,正是箭头组合运算符 >>>
,这意味着以下是等效的
xs.map(incr >>> square)
xs.map(pipe(incr, square))
我们知道许多代码库不会乐于引入运算符,因此我们希望降低采用函数组合的入门门槛。
这些概念(以及更多)在 Point-Free 中进行了深入探讨,Point-Free 是一个由 Brandon Williams 和 Stephen Celis 主持的探索函数式编程和 Swift 的视频系列。
本集中的想法最初在第 11 集中探讨。
所有模块均在 MIT 许可证下发布。有关详细信息,请参阅 LICENSE。