Overture 0.5.0

Overture 0.5.0

Stephen CelisBrandon Williams维护。



Overture 0.5.0

🎼Overture

Swift 5 iOS/macOS CI Linux CI @pointfreeco

一个用于函数组合的库。

目录

动机

我们经常使用函数,但函数组合无处不在!

例如,当我们使用数组上的高阶方法,如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"]

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)

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 函数生成从键路径生成的 获取器函数

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)

overset

overset 函数生成对给定键路径(或 设置器函数)中的一个结构中的 Value 进行操作的 (Root) -> Root 转换函数。

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 = with(URLRequest(url: url), concat(
  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 = ["[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 有许多作用域层次结构来帮助您。

    • 您可以使用 fileprivateprivate 作用域,限制超出现有文件的高特定函数的公开。
    • 您可以将函数定义为类型内部的 static 成员。
    • 您可以用模块的名称来限定函数(例如,Overture.pipe(f, g))。您甚至可以从模块名中自动完成自由函数,因此发现性(discoverability)不必受损!
  • 自由函数在 Swift 中很常见吗?

    虽然看起来可能不是这样,但自由函数在 Swift 中无处不在,这使得 Overture 非常有用!下面是一些例子

    • 初始化器,如 String.init
    • 未绑定方法,如 String.uppercased
    • 带关联值的枚举实例,如 Optional.some
    • 我们传递给 mapfilter 和其他高阶方法的临时闭包。
    • 顶级标准库函数,如 maxminzip

安装

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 WilliamsStephen Celis 主持。

本期节目中的想法最初在 第 11 集中 进行了探讨。

video poster image

许可证

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