1. Introduction

今天我们想谈谈两个世界之间的联系:Swift类型和代数。我们对这两个世界都很熟悉,但事实证明,它们有着非常深刻、美丽的联系。使用这种对应关系,我们可以更好地理解是什么给我们的数据结构增加了复杂性,甚至使用我们从代数中获得的直觉来更好地塑造我们的数据,这样不可能的状态是不可表示的,因此不能编译!

2. The algebra of structs

首先,让我们看看一些足够简单的东西:结构体。事实上,这个结构:

struct Pair<A, B> {
  let first: A
  let second: B
}

这是最通用的,非平凡的结构。 它有两个字段,每个字段只包含一段通用数据。 让我们做一些奇怪的事情,对于A和B的特定值,让我们计算Pair< A, B>有多少值:

Pair<Bool, Bool>(first: true, second: true)
Pair<Bool, Bool>(first: true, second: false)
Pair<Bool, Bool>(first: false, second: true)
Pair<Bool, Bool>(first: false, second: false)

Pair<Bool, Bool>正好包含四个值。不可能构造任何其他有效的、正在编译的Swift程序的值。让我们试试另一个。 我要创建一个只有3个值的lil类型,并用Bool数其对:

enum Three {
  case one
  case two
  case three
}

我们可以用Bool和Three创建多少对?

Pair<Bool, Three>(first: true, second: .one)
Pair<Bool, Three>(first: true, second: .two)
Pair<Bool, Three>(first: true, second: .three)
Pair<Bool, Three>(first: false, second: .one)
Pair<Bool, Three>(first: false, second: .two)
Pair<Bool, Three>(first: false, second: .three)

这个有六个值!有趣!

Swift有一种奇怪的类型叫Void。 事实上,它的奇怪至少有两个原因。首先,你可以用同样的方式来引用类型和值:

_: Void = Void()
_: Void = ()
_: () = ()

这就引出了另一件奇怪的事。 Void只有一个值! 正因为如此,()的实质内容根本不重要。它只是一个值,表示我们在Void中有这个东西,但你不能用()做任何事。 这也是为什么没有返回值的函数会偷偷返回Void,即使没有明确指定:

func foo(_ x: Int) /* -> Void */ {
  // return ()
}

Swift编译器会在你之后return()

让我们尝试将Void插入到我们的pair中,看看会发生什么:

Pair<Bool, Void>(first: true, second: ())
Pair<Bool, Void>(first: false, second: ())

它只包含两个值。

那么一对Void和Void呢?

Pair<Void, Void>(first: (), second: ())

Only 1 value!

有趣! 我们别无选择,只能为第二个字段插入(),因此它实际上并没有改变太多类型。

Swift还有另一种奇怪的类型:Never。它的定义非常简单:

enum Never {}

这意味着什么? 它是一个没有caseenum。这就是所谓的“uninhabited type”:不包含值的类型。没有办法做这样的事情:

_: Never = ???

这里没有任何东西可以让Swift编译这个程序。

当我们将Never插入Pair时会发生什么?

Pair<Bool, Never>(first: true, second: ???)

没有我们可以投入的东西了?!

Never类型还会得到编译器的特殊处理,在编译器中,返回Never的函数被认为是不返回的函数。例如,fatalError函数返回Never。编译器知道在执行此语句之后的所有代码行和分支都不会发生,并可以使用这一点来证明穷尽性。

考虑到所有这些例子,A和B中的值的数量与Pair<A, B>中的值的数量之间的关系是什么?

// Pair<Bool, Bool>  = 4
// Pair<Bool, Three> = 6
// Pair<Bool, Void>  = 2
// Pair<Void, Void>  = 1
// Pair<Bool, Never> = 0

一种模式开始出现了:乘法!

// Pair<Bool, Bool>  = 4 = 2 * 2
// Pair<Bool, Three> = 6 = 2 * 3
// Pair<Bool, Void>  = 2 = 2 * 1
// Pair<Void, Void>  = 1 = 1 * 1
// Pair<Bool, Never> = 0 = 2 * 0

Pair使值的数量相乘,即Pair<A, B> 中的值的数量是A中的值乘以B中的值的数量。

这个现象还有另一种代数解释:logical conjunction, a.k.a. andPair类型封装了接受两种类型的“和”的含义,即Pair< A, B>的值就是类型A的值和另一个类型B的值。

这适用于任何结构体和元组,而不仅仅是Pair。让我们来看一个例子

enum Theme {
  case light
  case dark
}

enum State {
  case highlighted
  case normal
  case selected
}

struct Component {
  let enabled: Bool
  let state: State
  let theme: Theme
}

代数的Component是多少?

// Bool * Theme * State = 2 * 3 * 2 = 12

Component有12个值!

根据这种直觉,让我们删除所有类型的名称,只关注字段中存储的数据。为了做到这一点,我们将创建一个不是有效的Swift代码的符号,但将允许我们更紧凑地看到我们在这里所揭示的代数。 因此,我们以前写Pair<A, B>,现在我们只写A * B,这看起来很奇怪,但它只是帮助指导我们的直觉:

// Pair<A, B>        = A * B
// Pair<Bool, Bool>  = Bool * Bool
// Pair<Bool, Three> = Bool * Three
// Pair<Bool, Void>  = Bool * Void
// Pair<Bool, Never> = Bool * Never

我们称A * B为A和B类型的乘积。现在我们考虑得更抽象一点了,我们甚至可以放松对< A, B>的直觉,它是A和B中元素个数的字面量的乘法。虽然对于具有有限多个值的类型确实是这样,但这并不能真正帮助我们解决以下问题:

// Pair<Bool, String> = Bool * String

我们不再讨论值的数量,因为String有一个无限的数,但我们仍然可以把它看作乘法。

我们甚至可以考虑以下几点:

// String * [Int]
// [String] * [[Int]]

我们将无限种类型相乘!

让我们更进一步,从Void, Never和Bool中删除名称,只通过包含的值的数量来表示这些类型。

// Never = 0
// Void = 1
// Bool = 2

现在我们甚至不考虑具体的类型,只考虑抽象的代数实体。

3. The algebra of enums

现在我们知道了Swift中的结构体对应于类型的乘法。但是有一个对应的“dual”:加法! 这在Swift的类型系统中是什么样子的?

好吧,事实证明Swift支持这样的构造,而这恰恰是一个enum! 让我们考虑一下最通用的、非平凡的枚举:

enum Either<A, B> {
  case left(A)
  case right(B)
}

让我们使用之前的一些值,看看如何从这个类型构造一些简单的值:

Either<Bool, Bool>.left(true)
Either<Bool, Bool>.left(false)
Either<Bool, Bool>.right(true)
Either<Bool, Bool>.right(false)

我们又得到了四个值。Three?

Either<Bool, Three>.left(true)
Either<Bool, Three>.left(false)
Either<Bool, Three>.right(.one)
Either<Bool, Three>.right(.two)
Either<Bool, Three>.right(.three)

这次我们得到5个值。嗯!Void呢?

Either<Bool, Void>.left(true)
Either<Bool, Void>.left(false)
Either<Bool, Void>.right(Void())

Never呢?

Either<Bool, Never>.left(true)
Either<Bool, Never>.left(false)
Either<Bool, Never>.right(???)

最后一个例子特别有趣。我们看到,通过取一对Never,即Pair<A, Never>,我们使这一对无人居住。然而,对于Either,它只是意味着一个情况是无人居住的,但另一个是自由地在Bool值。

现在我们可以看到一些代数。那么A和B中的值的数量和Either<A, B>中的值的数量之间的关系是什么?

Either<Bool, Bool>  = 4 = 2 + 2
Either<Bool, Three> = 5 = 2 + 3
Either<Bool, Void>  = 3 = 2 + 1
Either<Bool, Never> = 2 = 2 + 0

从这些例子中,我们可以看到Either<A, B>中的值的数量正好是A中的值的数量加上B的值的数量。所以Either直接对应于取类型的和。 这就是为什么枚举被称为“和类型”。 我们还可以从逻辑的角度解释Either,就像我们对Pair做的那样:Either类型封装了接受两种类型的“或”的含义,即Either< A, B>正好是A类型的值或B类型的值。

和之前一样,让我们用一个新的符号来抽象取类型的和,这个符号在Swift中无效,但它会帮助我们发展直觉。

Either<Bool, Bool>  = Bool + Bool  = 2 + 2 = 4
Either<Bool, Three> = Bool + Three = 2 + 3 = 5
Either<Bool, Void>  = Bool + Void  = 2 + 1 = 3
Either<Bool, Never> = Bool + Never = 2 + 0 = 2

4. Word of warning: Void

值得注意的是,有些语言(如Haskell、PureScript、Idris)使用Void来表示uninhabited类型(即Swift所称的Never),因此如果你研究这些语言,可能会导致一些困惑。 事实上,从某种意义上说,这是一个很棒的名字,因为“void”看起来像是一个没有任何东西的空间!

对于只有一个唯一值的类型来说,更好的名称可能是类似Unit的东西。我们可以这样定义它:

struct Unit {}
let unit = Unit()

这很好,因为我们现在为类型Unit和值Unit有了一个独特的名称。另一个关于Unit的实际结构类型的好处是我们可以扩展它:

extension Unit: Equatable {
  static func == (lhs: Unit, rhs: Unit) -> Bool {
    return true
  }
}

现在我们可以把Unit传递给只需要等价值的函数,这很酷。但在Swift的 Void中,这是不可能的。如果你试图扩展它,你会得到这个错误:

Non-nominal type 'Void' cannot be extended

原因是Void被定义为一个空元组:

typealias Void = ()

Swift中的元组是non-nominal 类型,你不能通过名字来引用它们,只能通过结构来引用。这对Swift来说是一件非常不幸的事情,希望有一天能够得到纠正。

5. Empty structs vs. empty enums

但是现在我们想说一些非常奇怪的事情。让我们一起来看看Unit和Never的定义:

struct Unit {}
enum Never {}

显然这里有一些对称:一个没有case的enum和一个没有字段的struct。 为什么没有case的枚举中没有值,而没有字段的结构体却有值? 我们完全有理由认为Unit也没有值。

但是我们能凭直觉理解为什么会这样吗?

利用Swift类型和代数之间的对应关系,我们可以提出一个可能更容易回答的相关问题。我们可以问自己,“空枚举和空结构中有什么值?”这就相当于问:“空数组中整数的和和积是多少?”

假设我们有一个整数数组。如何定义以下函数:

func sum(_ xs: [Int]) -> Int {
  fatalError()
}

func product(_ xs: [Int]) -> Int {
  fatalError()
}

let xs = [1, 2, 3]
sum(xs)
product(xs)

当然,我们想要循环遍历数组,求和并将所有值相乘:

func sum(_ xs: [Int]) -> Int {
  var result: Int
  for x in xs {
    result += x
  }
  return result
}

func product(_ xs: [Int]) -> Int {
  var result: Int
  for x in xs {
    result *= x
  }
  return result
}

因为我们还没有给result一个初始值,所以它目前还不能编译。但是我们应该选择什么呢? 为了回答这个问题,我们需要理解和和乘积应该满足什么性质,这将迫使我们决定需要result从什么开始。 我们想要满足的最简单的属性与sumproduct在数组连接方面的行为:

sum([1, 2]) + sum([3]) == sum([1, 2] + [3])
product([1, 2]) * product([3]) == product([1, 2] + [3])

现在,如果我们使用空数组呢?

sum([1, 2]) + sum([]) == sum([1, 2] + [])
product([1, 2]) * product([]) == product([1, 2] + [])

这迫使sum([])为0,product([])为1。没有别的选择。因此empty sum是0, empty product是1。

sum([1, 2]) + 0 == sum([1, 2] + [])
product([1, 2]) * 1 == product([1, 2] + [])

sum([]) == 0
product([]) == 1

现在,将这个概念转换回类型世界,我们自然会得到这样的语句:“empty sum type”没有值(即uninhabited),而“empty product type”只有一个值! 所以我们用代数来解开一个非常棘手的存在困境!

6. Algebraic properties

现在我们已经建立了一些概念来理解Swift类型和代数之间的对应关系,让我们试着展示这些肌肉,看看我们是否能在更高层次上获得一些类型结构的直觉。

让我们从简单入手。回想一下Void对应于1,在代数世界中,我们知道乘以1没有任何作用。它的类型是什么?

// Void = 1
// A * Void = A = Void * A

这意味着在结构体的字段中使用Void值实际上会保持类型不变。

另一方面,Never对应于0,我们知道与它相乘是0。在类型世界中,这看起来像:

// Never = 0
// A * Never = Never = Never * A

因此,将Never放到结构体的字段中,最终会将该结构体转换为Never类型本身。 它完全消灭了它。

但是,加0的结果是保持值不变,在类型中,这对应于:

// A + Never = A = Never + A

我们换一种方法。考虑这个类型表达式:

// 1 + A = Void + A

In terms of Either this is:

// Either<Void, A> {
//   case left(())
//   case right(A)
// }

这是一种类型,所有A的值都在右边,然后这个特殊的值*left(Void())*是附加的。 什么样的本土的Swift类型有同样的形状? Optionals!

enum Optional<A> {
  case none
  case some(A)
}

none对应于left(没有关联值的case本质上与具有Void值的case相同),而some对应于right。现在我们看到:

// 1 + A = Void + A = A?

现在,假设你遇到了这个表达:

// Either<Pair<A, B>, Pair<A, C>>

让我们看看它在符号里是什么样子的:

// A * B + A * C

使用基础代数,我们知道如何将其分解成一个更简单的表达式:

A * (B + C)

现在这对应于一个带enum的pair:

Pair<A, Either<B, C>>

这里我们可以看到代数直觉让我们得到了一个更简单的数据结构。

另一方面,如果我们简单地调换PairEither的角色,我们有:

Pair<Either<A, B>, Either<A, C>>

And in the math world:

// (A + B) * (A + C)

这个方程不再因式分解了所以不能再简化了。

当然,我们可以把它展开,使它等于

// A * A + A * C + B * A + B * C

这有点像一个有四种情况的enum,每一种情况都是一对。这可能不是你想要的,但也许你想要,你有代数来告诉你怎么做!

我们讨论的每一个数据结构,如果我们只考虑数据而不考虑与数据相关的行为,那都只是产品的总和:从一个case的基本枚举开始,每个case都有一系列的product,这些product又可能包含更多的sum和product。

7. What’s the point?

我们已经写了一堆伪代码,甚至不是有效的Swift,它所能做的只是引导我们的直觉。我们从中得到什么好处了吗?

让我们看看URLSession的一个方法。

// URLSession.shared
//   .dataTask(with: url, completionHandler: (data: Data?, response: URLResponse?, error: Error?) -> Void)

completionHandler返回三个可选值。这是一个有3个字段的乘积类型。Swift元组只是乘积。我们用代数表示一下:

// (Data + 1) * (URLResponse + 1) * (Error + 1)

这看起来有点奇怪。如果我们完全扩展它会发生什么?

// (Data + 1) * (URLResponse + 1) * (Error + 1)
//   = Data * URLResponse * Error
//     + Data * URLResponse
//     + URLResponse * Error
//     + Data * Error
//     + Data
//     + URLResponse
//     + Error
//     + 1

这里有很多可表示的状态是没有意义的。它们甚至会在每一行中跳出来。我们可以得到URLResponse * Error,而URLResponse不应该和Error同时出现。我们还可以得到Data * Error,这也没有意义。我们也可以得到1,也就是Void,或者在这里所有值都是nil。我们还可以得到一切:Data * URLResponse * Error,这是不应该发生的。

当您使用这个接口时,您可能会注意到,如果您忽略了预期的情况,那么不可避免地会出现一个需要fatalError的分支,并且只希望它永远不会被调用。

让我们用新的直觉来表示我们想要的东西:

// Data * URLResponse + Error

我们这种类型的是什么样子?

Either<Pair<Data, URLResponse>, Error>

事实上,Swift社区已经接受了一种允许我们处理这类状态的类型,即Result类型。

// Result<(Data, URLResponse), Error>

在本例中,我们可以使用一个简单的元组来表示DataURLResponse的乘积,而不是使用Pair。

通过在完成回调中使用适当的类型,我们大大减少了编译时允许的无效状态的数量,从而简化了回调中所需的逻辑。

让我们进一步考虑Result类型。如果我们使用一个返回Result的API,但是带有一个永远不会失败的特定操作呢? 我们可以指定错误类型为Never!

// Result<A, Never>

现在我们可以确定错误情况是不适合的。

如果我们正在处理一个支持取消的异步API呢? 如何将取消案例添加到Result中?

// Result<A, Error>?

我们只是把它变成可选的!

看到我们如何既能解决复杂性,又能自然地将自己引向更适合我们需求的类型,就能更清楚地看到代数直觉如何改善我们的日常代码。 我们还看到,虽然结构体在元组中有轻量级版本,但也许Either是属于我们日常武器库的轻量级enum。我们也不要害怕!既害怕元组又不害怕元组,就好像你害怕加法而不害怕乘法一样。 或者这就像说你害怕“or”,而不是“and”。我们不会只使用乘法(*)和“和”(&&)来编写程序。 我们允许自己使用加法(+)和“或”(||)。所以让我们熟悉求和类型和Either!

我们的代数之旅才刚刚开始。我们还没有看到类型系统如何表示其他概念,比如求幂! 一种类型的能量比另一种类型的能量看起来像什么? 但那得等到下次了。请继续关注!