🎁 NonEmpty
确保集合包含值的编译时保证。
动机
我们经常处理那些理论上永远不应该为空的集合,但类型系统并没有提供这样的保证,所以我们不得不处理空的情况,通常是通过 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
许多 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([])) // 🛑
安装
Carthage
如果你使用Carthage,你可以在你的Cartfile
中添加以下依赖项
github "pointfreeco/swift-nonempty" ~> 0.2
CocoaPods
如果你的项目使用CocoaPods,只需在你的Podfile
中添加以下内容
pod 'NonEmpty', '~> 0.2'
SwiftPM
如果您想在一款使用SwiftPM的项目中使用NonEmpty,只需在您的Package.swift
文件中添加一个dependencies
子句即可。
dependencies: [
.package(url: "https://github.com/pointfreeco/swift-nonempty.git", from: "0.2.0")
]
Xcode 子项目
将NonEmpty作为子模块克隆,或下载它,并将NonEmpty.xcodeproj
文件拖拽到您的项目中。
想了解更多?
这些概念及更多内容在Point-Free中得到了详尽的探讨,这是一个由Brandon Williams和Stephen Celis主持的探讨函数式编程和Swift的视频系列。
NonEmpty首次在第20集中被探索。
许可证
所有模块均在MIT许可证下发布。有关详细信息,请参阅LICENSE。