🎼 序曲

CI

一个用于函数组合的库。

目录

动机

我们一直都在使用函数,但函数组合就隐藏在眼前!

例如,当我们使用高阶方法时,比如数组上的 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) -> Cpipe 将返回一个全新的 (A) -> C 函数。

let computeAndStringify = pipe(incr, square, String.init)

computeAndStringify(42)
// "1849"

[1, 2, 3].map(computeAndStringify)
// ["4", "9", "16"]

withupdate

withupdate 函数对于将函数应用于值非常有用。它们与 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

concat 函数与单一类型组合。这包括以下函数签名的组合

使用 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)

curryflipzurry

这些函数构成了组合的瑞士军刀。它们使我们能够使用现有的不组合的函数和方法(例如,那些接受零个或多个参数的函数和方法)并恢复组合。

例如,让我们将一个接受多个参数的字符串初始化器转换为可以与 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 函数从键路径生成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

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)

overset

overset 函数生成 (Root) -> Root 转换函数,这些函数在给定键路径(或setter 函数)的结构中的 Value 上工作。

over 函数接受一个 (Value) -> Value 转换函数来修改现有值。

let celebrateBirthday = over(\User.age, incr)
// (User) -> User

set 函数用一个全新的值替换现有值。

with(user, set(\.name, "Blob"))

mpropmvermut

mpropmvermut 函数是 propoverset可变变体。

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")
)

zipzip(with:)

这是一个 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
}

FAQ

安装

你可以通过将 Overture 添加为包依赖项来将其添加到 Xcode 项目中。

https://github.com/pointfreeco/swift-overture

如果你想在 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 WilliamsStephen Celis 主持的探索函数式编程和 Swift 的视频系列。

本集中的想法最初在第 11 集中探讨。

video poster image

许可证

所有模块均在 MIT 许可证下发布。有关详细信息,请参阅 LICENSE