🛂 已验证

CI

一种可以累积多个错误的结果类型。

目录

动机

问题

Swift 错误处理在第一次失败时会短路。 因此,对于处理表单数据之类的事情来说,它不是最佳选择,因为多个输入可能会导致多个错误。

struct User {
  let id: Int
  let email: String
  let name: String
}

func validate(id: Int) throws -> Int {
  guard id > 0 else {
    throw Invalid.error("id must be greater than zero")
  }
  return id
}

func validate(email: String) throws -> String {
  guard email.contains("@") else {
    throw Invalid.error("email must be valid")
  }
  return email
}

func validate(name: String) throws -> String {
  guard !name.isEmpty else {
    throw Invalid.error("name can't be blank")
  }
  return name
}

func validateUser(id: Int, email: String, name: String) throws -> User {
  return User(
    id: try validate(id: id),
    email: try validate(id: email),
    name: try validate(id: name)
  )
}

这里,我们将几个抛出函数组合成一个可能返回 User 的单个抛出函数。

let user = try validateUser(id: 1, email: "blob@pointfree.co", name: "Blob")
// User(id: 1, email: "blob@pointfree.co", name: "Blob")

如果 idemailname 无效,则会抛出错误。

let user = try validateUser(id: 1, email: "blob@pointfree.co", name: "")
// throws Invalid.error("name can't be blank")

不幸的是,如果其中几个或所有这些输入都无效,则第一个错误会胜出。

let user = try validateUser(id: -1, email: "blobpointfree.co", name: "")
// throws Invalid.error("id must be greater than zero")

使用 Validated 处理多个错误

Validated 是一种类似于 Result 的类型,它可以累积多个错误。 我们可以定义使用 Validated 的函数,而不是使用 throw 函数。

func validate(id: Int) -> Validated<Int, String> {
  return id > 0
    ? .valid(id)
    : .error("id must be greater than zero")
}

func validate(email: String) -> Validated<String, String> {
  return email.contains("@")
    ? .valid(email)
    : .error("email must be valid")
}

func validate(name: String) -> Validated<String, String> {
  return !name.isEmpty
    ? .valid(name)
    : .error("name can't be blank")
}

为了累积错误,我们使用一个可能已经熟悉的函数:zip

let validInputs = zip(
  validate(id: 1),
  validate(email: "blob@pointfree.co"),
  validate(name: "Blob")
)
// Validated<(Int, String, String), String>

Validated 上的 zip 函数的工作方式与它在序列上的工作方式非常相似,但它不是将一对序列压缩成一个对序列,而是将一组单个 Validated 值压缩成一个组的单个 Validated 值。

从这里,我们可以使用另一个我们可能已经熟悉的函数 map,它接受一个转换函数并生成一个新的 Validated 值,其有效情况已转换。

let validUser = validInputs.map(User.init)
// valid(User(id: 1, email: "blob@pointfree.co", name: "Blob"))

我们有效的输入组已转换为有效的用户。

为了符合人体工程学和组合,提供了一个柯里化的 zip(with:) 函数,它接受一个转换函数和 Validated 输入。

zip(with: User.init)(
  validate(id: 1),
  validate(email: "blob@pointfree.co"),
  validate(name: "Blob")
)
// valid(User(id: 1, email: "blob@pointfree.co", name: "Blob"))

无效的输入会在 invalid 的情况下产生错误。

zip(with: User.init)(
  validate(id: 1),
  validate(email: "blob@pointfree.co"),
  validate(name: "")
)
// invalid(["name can't be blank"])

更重要的是,多个无效的输入会产生一个包含多个错误的 invalid 情况。

zip(with: User.init)(
  validate(id: -1),
  validate(email: "blobpointfree.co"),
  validate(name: "")
)
// invalid([
//   "id must be greater than zero",
//   "email must be valid",
//   "name can't be blank"
// ])

无效的错误保存在 非空数组中,以提供编译时保证,您永远不会遇到空的 invalid 情况。

安装

您可以通过将其添加为包依赖项来将 Validated 添加到 Xcode 项目。

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

如果您想在 SwiftPM 项目中使用 Validated,只需将其添加到 Package.swift 中的 dependencies 子句中即可

dependencies: [
  .package(url: "https://github.com/pointfreeco/swift-validated", from: "0.2.1")
]

想了解更多?

这些概念(以及更多)在 Point-Free 中进行了深入探讨,这是一个探索函数式编程和 Swift 的视频系列,由 Brandon WilliamsStephen Celis 主持。

Validated 在 Zip 的多种面孔:第 2 部分 中进行了探讨

video poster image

许可证

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