🎁 NonEmpty

CI

编译时保证,集合包含一个值。

动机

我们经常使用不应永远为空的集合,但类型系统无法保证这一点,因此我们不得不处理空的情况,通常使用 ifguard 语句。NonEmpty 是一种轻量级类型,可以将任何集合类型转换为非空版本。一些例子

// 1.) A non-empty array of integers
let xs = NonEmpty<[Int]>(1, 2, 3, 4)
xs.first + 1 // `first` is non-optional since it's guaranteed to be present

// 2.) A non-empty set of integers
let ys = NonEmpty<Set<Int>>(1, 1, 2, 2, 3, 4)
ys.forEach { print($0) } // => 1, 2, 3, 4

// 3.) A non-empty dictionary of values
let zs = NonEmpty<[Int: String]>((1, "one"), [2: "two", 3: "three"])

// 4.) A non-empty string
let helloWorld = NonEmpty<String>("H", "ello World")
print("\(helloWorld)!") // "Hello World!"

应用

非空集合类型有很多应用,但由于 Swift 标准库没有提供这种类型,因此可能很难看到。这里只是一些这样的应用

加强第一方 API

许多 API 接受并返回可能为空的数组,但实际上它们可以保证数组是非空的。考虑一个 groupBy 函数

extension Sequence {
  func groupBy<A>(_ f: (Element) -> A) -> [A: [Element]] {
    // Unimplemented
  }
}

Array(1...10)
  .groupBy { $0 % 3 }
// [0: [3, 6, 9], 1: [1, 4, 7, 10], 2: [2, 5, 8]]

然而,返回类型 [A: [Element]] 中的数组 [Element] 可以保证永远不为空,因为生成 A 的唯一方法是从 Element 中获取。因此,此函数的签名可以加强为

extension Sequence {
  func groupBy<A>(_ f: (Element) -> A) -> [A: NonEmpty<[Element]>] {
    // Unimplemented
  }
}

更好地与第三方 API 交互

有时,我们与之交互的第三方 API 需要非空的值集合,因此在我们的代码中,我们应该使用非空类型,以便确保永远不会向 API 发送空值。 GraphQL 就是一个很好的例子。这是一个非常简单的查询构建器和打印器

enum UserField: String { case id, name, email }

func query(_ fields: Set<UserField>) -> String {
  return (["{"] + fields.map { "  \($0.rawValue)" } + ["}"])
    .joined()
}

print(query([.name, .email]))
// {
//   name
//   email
// }

print(query([]))
// {
// }

最后一个查询是程序员错误,将导致 GraphQL 服务器返回错误,因为发送空查询是无效的。我们可以通过强制我们的查询构建器使用非空集合来防止这种情况发生

func query(_ fields: NonEmptySet<UserField>) -> String {
  return (["{"] + fields.map { "  \($0.rawValue)" } + ["}"])
    .joined()
}

print(query(.init(.name, .email)))
// {
//   name
//   email
// }

print(query(.init()))
// 🛑 Does not compile

更具表现力的数据结构

Swift 社区(和其他语言)中流行的一种类型是 Result 类型。它允许你表达一个可以是成功或失败的值。还有一种相关的类型也很方便,称为 Validated 类型

enum Validated<Value, Error> {
  case valid(Value)
  case invalid([Error])
}

类型为 Validated 的值要么是有效的,因此带有一个 Value,要么是无效的,并带有一个错误数组,描述该值的全部错误之处。例如

let validatedPassword: Validated<String, String> =
  .invalid(["Password is too short.", "Password must contain at least one number."])

这很有用,因为它允许你描述一个值的所有错误之处,而不仅仅是一件事。但是,如果我们使用验证错误列表的空数组,那就没有多大意义了

let validatedPassword: Validated<String, String> = .invalid([]) // ???

相反,我们应该加强 Validated 类型以使用非空数组

enum Validated<Value, Error> {
  case valid(Value)
  case invalid(NonEmptyArray<Error>)
}

现在这是一个编译器错误

let validatedPassword: Validated<String, String> = .invalid(.init([])) // 🛑

安装

如果你想在使用了 SwiftPM 的项目中使用 NonEmpty,只需向你的 Package.swift 添加一个依赖项即可

dependencies: [
  .package(url: "https://github.com/pointfreeco/swift-nonempty.git", from: "0.3.0")
]

有兴趣了解更多吗?

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

NonEmpty 首次在 第 20 集 中进行了探讨

video poster image

许可证

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