1. Introduction
因此,创建case path非常简单,但是这个代码片段有很多地方不完全正确。首先,用这些静态变量污染Result命名空间是不对的,最好有一个不同的地方来存储它们。此外,在创建这些case path时还涉及一些样板。我们在这里看到if case let stuff实现提取时,基本上我们创建的每个case path都是这个样子的,一段时间后,它会很痛苦,需要一遍又一遍地写。
我们很快就会解决这两个问题,但首先我们想要探索case path的一些属性,并展示与关键路径的相似之处。Swift标准库为关键路径提供了一些操作,我们当然希望case path也有一些版本的这些操作。
2. Appending paths
首先,关键路径带有一个非常强大的特性,允许您将它们附加在一起,以便更深入地了解一个结构。为了看到这一点,让我们通过让它也持有一个Location来加强我们的User结构体:
struct User {
var id: Int
var isAdmin: Bool
var location: Location
var name: String
}
struct Location {
var city: String
var country: String
}
然后我们可以选择从用户到他们所在位置的关键路径:
\User.location
以及从一个location到它所在城市的关键路径:
\Location.city
我们可以把它们加在一起:
(\User.location).appending(path: \Location.city)
这样我们就得到了一个全新的键路径,它从一个User值一直延伸到一个字符串,这代表了用户所在的城市:
Swift甚至在句法上加了一些糖,让这句话看起来更好:
\User.location.city
这个操作类似于函数的组合。它说,如果我们有一条从A到B的键路径和一条从B到C的键路径,那么我们可以创建一条从A到C的全新键路径。
那么,问题是,case路径也支持这样的组合操作符吗? 他们确实如此! 如果我们有从A到B的路径和从B到C的路径,我们可以创建一个从A到C的全新路径。为了实现这一点,我们将扩展CasePath结构体,添加一个类似于关键路径方法的新方法:
extension CasePath/*<Root, Value>*/ {
func appending<AppendedValue>(
path: CasePath<Value, AppendedValue>
) -> CasePath<Root, AppendedValue> {
}
}
那么,我们如何实现这个呢? 我们知道我们需要返回一个新的case路径,所以我们也可以得到一个存根:
extension CasePath/*<Root, Value>*/ {
func appending<AppendedValue>(
path: CasePath<Value, AppendedValue>
) -> CasePath<Root, AppendedValue> {
CasePath<Root, AppendedValue>(
extract: <#(Root) -> AppendedValue?#>,
embed: <#(AppendedValue) -> Root#>
)
}
}
对于提取(extract)方法,我们有一个根,我们想从中提取一个AppendedValue。我们可以接触self.extract它允许我们从根结点中提取一个值,所以我们可以从这里开始:
extension CasePath/*<Root, Value>*/ {
func appending<AppendedValue>(
path: CasePath<Value, AppendedValue>
) -> CasePath<Root, AppendedValue> {
CasePath<Root, AppendedValue>(
extract: { root in
let value = self.extract(root)
},
embed: <#(AppendedValue) -> Root#>
)
}
}
它返回一个可选的值,我们可以使用传入的case路径进一步从中提取附加的值。现在,我们的值是可选的,还有path.Extract需要一个真实的值,所以我们需要做一些展开来完成这个操作:
extension CasePath/*<Root, Value>*/ {
func appending<AppendedValue>(
path: CasePath<Value, AppendedValue>
) -> CasePath<Root, AppendedValue> {
CasePath<Root, AppendedValue>(
extract: { root in
if let value = self.extract(root) {
return path.extract(value)
}
return nil
},
embed: <#(AppendedValue) -> Root#>
)
}
}
但这可以通过使用定义在可选选项上的flatMap操作来简化:
extension CasePath/*<Root, Value>*/ {
func appending<AppendedValue>(
path: CasePath<Value, AppendedValue>
) -> CasePath<Root, AppendedValue> {
CasePath<Root, AppendedValue>(
extract: { root in
self.extract(root).flatMap(path.extract)
},
embed: <#(AppendedValue) -> Root#>
)
}
}
这就是说,首先我们试着从根结点中提取一个值,然后我们试着从值中提取一个附加的值。
embed函数甚至更简单。这里我们从一个附加值开始,我们可以用给定的case路径嵌入它。现在我们有了一个值,我们可以使用self.embed将其嵌入到根目录中:
extension CasePath/*<Root, Value>*/ {
func appending<AppendedValue>(
path: CasePath<Value, AppendedValue>
) -> CasePath<Root, AppendedValue> {
CasePath<Root, AppendedValue>(
extract: { root in
self.extract(root).flatMap(path.extract)
},
embed: { appendedValue in
self.embed(path.embed(appendedValue))
})
}
}
就像那样,我们有一种方法将case paths附加在一起,前提是它们的类型匹配。
为了展示一个示例用例,假设我们有一个enum,它表示用户的身份验证状态。它们可以使用访问令牌进行身份验证,也可以不进行身份验证:
enum Authentication {
case authenticated(AccessToken)
case unauthenticated
}
struct AccessToken {
var token: String
}
让我们也为enum的authenticated案例快速创建一个case path:
let authenticatedCasePath = CasePath<Authentication, AccessToken>(
extract: {
if case let .authenticated(accessToken) = $0 { return accessToken }
return nil
},
embed: Authentication.authenticated
)
我们在这里再次看到一些烦人的样板,但我们很快就会解决这个问题。
我们可以取一个结果用例路径和一个经过验证的用例路径,并将它们附加在一起,以获得一个遍历结果成功用例的用例路径,然后通过验证用例最终获得对令牌的访问:
Result<Authentication, Error>.successCasePath
.appending(path: authenticatedCasePath)
// CasePath<Result<Authentication, Error>, AccessToken>
现在我们有了一个从Result到访问令牌的案例路径。
3. Introducing the .. operator
然而,对于如此简单的事情,使用**appending(path:)**是相当冗长的。对于键路径,我们可以使用简单的点语法将键路径附加到一起,但这是在特殊的编译器支持下完成的,所以我们不能对键路径这样做。
不过,我们可以引入一个模仿点语法样式的中缀操作符。我们已经有一段时间没有在Point-Free上使用操作符了,但是我们可以先声明我们想要使用的操作符符号:
infix operator ..
然后我们可以用这些符号作为函数名来实现一个函数:
func .. <A, B, C>(
lhs: CasePath<A, B>,
rhs: CasePath<B, C>
) -> CasePath<A, C> {
return lhs.appending(path: rhs)
}
现在,通过这个操作符,我们可以表达遍历结果成功的情况,然后通过下面的方式遍历Authentication枚举的经过验证的情况:
Result<Authentication, Error>.successCasePath .. authenticatedCasePath
这就简单多了,噪音也少了,很快我们会让它变得更简单。
虽然我们的一些观众很乐意向运营商介绍他们的代码库,但在Swift中这仍然是一件相对不常见的事情。
4. Identity paths
Swift中还有一个关键路径的小功能,它被称为“identity”关键路径。
Swift中的每一种数据类型都有一个特殊的键路径,您可以通过以下方式访问:
\User.self as WritableKeyPath<User, User>
\Location.self as WritableKeyPath<Location, Location>
\String.self as WritableKeyPath<String, String>
\Int.self as WritableKeyPath<Int, Int>
这些都是完全有效的关键路径,尽管可能有点愚蠢。这些键路径的“get”只是返回整个值,而“set”只是用一个新值替换整个值。有时,当API强制您提供一个关键路径时,这个关键路径可能很有用,但您不想实际关注结构的一部分,而是想使用整个结构。
那么,这对于案例路径是什么样的呢? case路径也有一个标识,我们可以很容易地将其实现为CasePath类型上的一个静态变量,只要Root和Value泛型相同:
extension CasePath where Root == Value {
static var `self`: CasePath {
CasePath(
embed: { $0 },
extract: { .some($0) }
)
}
}
所以现在我们有一个简单的方法来获得任何类型的恒等情形路径:
CasePath<Authentication, Authentication>.`self`
5. Re-introducing the ^ operator
还有一个关于关键路径的操作,我们想要讨论,它不是与Swift标准库一起提供的东西,而是我们在之前的Point-Free章节中讨论的东西。
prefix operator ^
prefix func ^ <Root, Value>(_ kp: KeyPath<Root, Value>) -> (Root) -> Value {
return { root in root[keyPath: kp] }
}
这允许我们用很少的礼节来编写富有表现力的代码。例如,如果我们有一个用户列表:
let users = [
User(
id: 1,
isAdmin: true,
location: Location(city: "Brooklyn", country: "USA"),
name: "Blob"
),
User(
id: 2,
isAdmin: false,
location: Location(city: "Los Angeles", country: "USA"),
name: "Blob Jr."
),
User(
id: 3,
isAdmin: true,
location: Location(city: "Copenhagen", country: "DK"),
name: "Blob Sr."
),
]
我们可以很容易地以各种方式转换这个数组:
users.map(^\.name)
// ["Blob", "Blob Jr.", "Blob Sr."]
users.map(^\.location.city)
// ["Brooklyn", "Los Angeles", "Copenhagen"]
users.filter(^\.isAdmin).count
// 2
很快我们就不需要这个前缀操作符了。多亏了我们的Stephen Celis和Greg Titus的合作,Swift提出了一个建议,允许关键路径自动转换为功能。它在一段时间前就被接受了,一旦Swift 5.2发布,我们将能够简单地做以下事情:
users.map(\.name)
users.map(\.location.city)
users.filter(\.isAdmin)
不需要特殊操作符。
尽管不再需要这个操作符,但它所做的事情是访问的重要内容。
这个操作符应该对案例路径做什么? 由于在关键路径上,操作符充当了将getter函数从关键路径中取出的一种方式,因此可以将它用于案例路径以访问提取功能。让我们来定义它:
prefix func ^ <Root, Value>(
path: CasePath<Root, Value>
) -> (Root) -> Value? {
return path.extract
}
它非常简单,但它允许我们立即将任何case路径转换为一个提取函数:
^authenticatedCasePath
// (Authentication) -> AccessToken?
我们可以像使用键路径一样使用这些值,除了如果我们有一个枚举值数组,我们可以尝试从枚举中提取一个值:
let authentications: [Authentication] = [
.authenticated(AccessToken(token: "deadbeef")),
.unauthenticated,
.authenticated(AccessToken(token: "cafed00d"))
]
authentications
.compactMap(^authenticatedCasePath)
// [{token "deadbeef"}, {token "cafed00d"}]
这是一种非常简洁的方法。如果我们试图在没有case路径的情况下这样做,我们将不得不自己解构认证值:
authentications
.compactMap {
if case let .authenticated(accessToken) = $0 {
return accessToken
}
return nil
}
但这行不通,因为多行闭包需要显式类型:
authentications
.compactMap { authentication -> AccessToken? in
if case let .authenticated(accessToken) = authentication {
return accessToken
}
return nil
}
这是我们能得到的最短的代码,它仍然是有噪声的。它要求我们指定闭包的返回类型,因为它是多行的,并且if case let比做一个完整的切换要短,但是我总是很难记住准确的语法。
6. Next time: case paths for free
现在,把这两个片段并排显示并说其中一个比另一个干净可能不完全公平:
authentications
.compactMap(^authenticatedCasePath)
authentications
.compactMap { authentication -> AccessToken? in
if case let .authenticated(accessToken) = authentication {
return accessToken
}
return nil
}
第一个代码片段有完全相同的样板,它只是被隐藏在case路径中,所以我们在调用站点看不到它。这样可能会好一点,但样板还在。
然而,我们甚至可以对这个样板做一些事情。如前所述,创建case路径的样板与extract函数有关,该函数试图从枚举中提取相关的值。embed函数在Swift中是免费的,因为每一个enum的例子都作为一个函数,可以嵌入相关的值到enum中,但提取需要一些工作。
摆脱这种样板的一种方法是转向代码生成。这是非常强大的,我们可能会转向代码生成的案例路径,但代码生成是相当繁重的。我们需要找到在源代码发生变化时运行该工具的最佳方法,以确保它是最新的,这可能会使构建过程复杂化。
实际上,对于案例路径,我们可以做一些不同的事情。我们可以神奇地从enum的情况下的embed函数中派生一个case路径的extract函数。我们说这是“神奇的”,因为它使用了Swift的运行时反射能力。
如果您不熟悉编程中的反射思想,那么您只需要知道它允许您在运行时检查值和对象的内部结构。例如,您可以使用反射来获得结构体所存储属性的所有字符串名称的列表。
任何时候你在Swift中使用反射,你都是故意超出Swift编译器的权限。这意味着您处于危险的水域中,因为编译器不支持您。然而,如果您轻描淡写地编写大量测试,您可以提出一些合理的方法,可以清除所有重复的样板。让我们从探索反射API开始,看看我们有什么可用的……下次吧!