Motivation


我们一直在与函数打交道,但函数的组成却隐藏在我们的眼皮底下!

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

函数是最小的代码构建块。函数组合使我们能够将这些模块组合在一起,并使用小的、可重用的、可理解的单元构建整个应用程序。

Examples

pipe

Overture中最基本的组成部分。它将现有的功能整合在一起。也就是说,给定一个函数(A) -> B和一个函数(B) -> C,管道将返回一个全新的(A) -> C函数。

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

computeAndStringify(42)
// "1849"

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

with and update

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

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, and 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还将方法公开为静态的、未绑定的函数。这些函数已经是curry过的了。我们要做的就是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中。在这里,我们构建了一个函数,它对整数进行加1、平方,将其转换为字符串,然后获取字符串的字符数:

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)

over and set

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, and 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 = update(
  URLRequest(url: url),
  mut(\.httpMethod, "POST"),
  setHeader("Authorization", "Token " + token),
  setHeader("Content-Type", "application/json; charset=utf-8")
)

zip and zip(with:)

这是Swift船的功能! 不幸的是,它仅限于对sequencesOverturezip定义为一次最多处理10个序列,这使得组合几组相关数据变得轻而易举。

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

立即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:)助手,它预先接受一个transform函数并经过curry处理,因此它可以使用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-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
}