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
}

让我们也为enumauthenticated案例快速创建一个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开始,看看我们有什么可用的……下次吧!