编译时保证,集合包含一个值。
我们经常使用不应永远为空的集合,但类型系统无法保证这一点,因此我们不得不处理空的情况,通常使用 if
和 guard
语句。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 接受并返回可能为空的数组,但实际上它们可以保证数组是非空的。考虑一个 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 发送空值。 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 Williams 和 Stephen Celis 主持的探索函数式编程和 Swift 的视频系列。
NonEmpty 首次在 第 20 集 中进行了探讨
所有模块均在 MIT 许可证下发布。有关详细信息,请参阅 LICENSE。