Swift 和函数式编程的遗产

tryswift-rob-napier-swift-legacy-functional-programming
原文地址 by Rob Napier
译文地址 by Alex Tse

Rob Napier 开发过单子(Monad),Functor of Doom。 他接触过 map,flattened 和 lensed。他反复迭代尝试,提出了 Maybe 类型,今后他也将继续致力于对函数式编程的探索。
他从 Haskell 到 Church,可以确定:Swift 不是一个函数编程语言,如果硬把它们当成一回事反而会扰乱 Swift 和破坏 Cocoa。
然而,Swift 却已经从函数编程吸取了一些很不错的经验,虽然值类型可能不是当前所流行的,但它们显然是未来趋势。
Rob 研究着已经有数十年的函数语言是如何对 Swift 产生了影响,以及如何更好的使用这些特性,同时还要保持 Swift 的个性和 Cocoa 的友好,当然少不了要拥抱面向协议编程。


概述

Swift 面世一周后,我写了“Swift并不是一种函数语言”这篇博客。两年过去了,Swift 依旧不是函数语言。这篇文章讨论的是经历了数十载的函数编程,以更 Swifty 方式带到 Swift 里去。

什么是函数编程?

它是单子(monads),函子(functors),Haskell 这类优美的代码。

函数编程不是一种语言或语法,它是一种用来思考问题的方式。函数编程在 Monad 出来的前几十年就有了。 我们会用函数编程来思考如何解决问题,并将它们结构化的组织起来。

用 Swift 写一段非常简单的代码看看:

var persons: [Person] = []
for name in names {
    let person = Person(name: name)
    if person.isValid {
        persons.append(person)
    }
}

这是个实现了两件事的简单循环:用 name 实例 person,然后把有效的 person 插入到数组当中。如此简单的代码,还是做了不少工作。因为要知道这其中发生了什么,必须从头看到尾过一遍。而且你还需要思考,“这个数组是做什么的?”

这段代码其实还可以更简单。我们可以关注点分离(separate the concerns),把事情分开。

比如我们现在有两个循环,每个循环做少量的事。我们从每个简单的循环代码里面找到模式:创建一个数组,遍历一些值,在每个值上面做一些操作,最后把它们插入到另一个数组里。

当你有一些事需要重复执行多次,你可能需要类似一个 extract 函数 – Swift 就有这个函数,叫 “map”。Map 可以将一个列表的东西转换成另一个列表。要注意的是:Map 会包含所有有效或无效的 person 对象。这是个基于 names 的 persons 列表。因此我们不需要从头开始过一遍代码,我们要表达的已经在这里面。

let possiblePersons = names.map(Person.init)
let persons = possiblePersons.filter { $0.isValid }

另一个循环模式也很常见:叫“filter”。Filter 接受一个返回 bool 的闭包函数。Filter 适用这个例子,可以得到有效的 person。我们可以将 map 和 filter 放在一起,把 map 得到的 Person 列表放到 filter 函数当中。

我们从上面的7行代码减少到2行。此外,我们可以重组它们:把这些所有函数通过链式(chaining)放在一起处理。

它十分易读,我们一次就可以读完。

let persons = names
    .map(Person.init)
    .filter { $0.isValid }

这种方式值得好好学习,我们应该适应这种写法(在我看来这很Swift)。在这个例子里无需再写 for 循环。

函数式工具

在1977年,约翰·巴科斯(John Backus)(帮助发明了 FORTRAN 和 ALGOL)赢得了图灵奖,并发表了一场名叫“编程能否从 Von Neumann 风格中解放出来”的演讲,我喜欢这个标题。“Von Neumann 风格”指的就是 FORTRAN 和 ALGOL。

这次演讲是一场道歉,原因是他发明了 FORTRAN 和 ALGOL。他解释说,指令式编程是一步一步的改变一些状态,直到进入你想得到的最终状态。他说“函数”这并不代表今天所说的功能,他鼓励许多函数研究者去学习研究。

我对该演讲很感兴趣,因为有一些东西可以带给 Swift:我们如何将复杂的东西分解成简单的东西。使这些简单的东西变得通用,然后再使用一些规则(如代数)将它们粘合起来。

代数的规则是把东西堆放在一起,然后按条件分离,最后一起转换它们。我们可以想一个用来控制程序的规则,事实上我们已经做到了:将一个循环分解成两个更简单的循环,找到它们惯用的形式,然后用链式(chaining)链在一起。当 Haskell 开发者第一次接触 Swift 的时候,往往会觉得不爽,因为他们一直实践的是 Haskell 的理论。

就如所有函数式语言一样,Haskell 的基础构成单位就是函数。它们都有非常好的方法来组织函数。下面我创建了一个新函数,叫 sum,把 foldr 函数和 + 函数合在一起,并赋予其初始值为 0。你阅读时可能觉得不顺畅,但是一旦运用起来,它的魅力会把你折服。

let sum = foldr (+) 0
  sum [1..10]

你可以用在 Swift 上,但你会发现代码变得丑陋,又不能很好的运行,别用错了单位组合,Swift 可不是函数式。

Swift 的构成单位是类型。你可以用 Classes,structs,enums 和 protocols 来构建(compose)。我们通常会通过一个函数将两种类型结合在一起,使用更简单的方式构建它们。

Swift 另一种常见的构成是将类型添加上下文(context)。最常见的是可选(optionals)类型。可选(optional)是添加上下文的类型,这个上下文可能有值,可能没有。那么这个类型就包含了一个小小的信息:它是否存在?这就是上下文的意思。添加上下文比其他跟踪信息的方式更强大。

extension MyStruct<T>: Sequence {
    func makeIterator() -> AnyIterator<T> {
return ... }
}

我们可以跟踪查看一个值,判断它是不是整数,是否有值,是否为-1,如果是-1那这意味着是无效值。那么如果每个地方都写上这样的判断,你会发现代码开始变得丑陋起来,而且容易出错,甚至编译器也帮不到你。

如果我们把 int 加上可选类型,那么编译器就可以帮助你,这个 int 就有了这样的上下文:“有值,没值,我可以确保你不会忘记它”。如果你曾经使用过-1,而这又很容易忘记检查,那么你的程序就会走到你意想不到的地方。

// No value: magic value
let noValue = -1
let n = 0
if n != noValue { ... }

// No value: context
let n: Int? = 0
if let n = n { ... }

例子

让我们来看一个更复杂的例子。

func login(username: String, password: String,
           completion: (String?, Error?) -> Void)
login(username: "rob", password: "s3cret") {
    (token, error) in
    if let token = token {
        // success
    } else if let error = error {
// failure }
}

这是很常见的 API。我们有一个登录函数,需要用户名密码,同时还会回传一个 token 和一个可能会出现错误的信息。我认为我们可以把上下文(content)处理的更好。

第一个问题completion 回调。这里说的是一个 String,但这个字符串是什么?是一个 token。我可以加上一个 label,但这并没有用。在 Swift 中,label 不是类型。即时这样做了,它依旧是字符串,字符串可以表示很多东西。

Token 有一些规则:它可能是固定长度,或者不能为空。这些都可以用字符串表示,但都不能表示 token。我们想要 token 带有更多的上下文。它是有规则的,所以我们要使用这些规则。既然如此,我们可以给它更多的结构。这就是为什么称它为 struct

struct Token {
    let string: String
}

我把字符串加入到结构当中。这不会产生其他开销,也不会有任何额外内存的使用,但现在我可以把一些规则运用在这里。

在这就只能构建一些指定的字符串,使用扩展还可以创建一些其他字符串。更棒的是这可以添加所有类型,string,dictionary,array,int 等,统统没问题。

当有了这些类型,你就可以把它们添加到一个上下文当中,并在使用它们的地方控制它们。意味着你可以控制它们是什么意思。我不再需要 label 或添加标注(comment),因为这第一眼可以看出是 Token 类型,第一个参数就是令牌(token)

第二个问题是我们传入了一个用户名密码。在大部分的程序中,你总会把它们一起传入进去;但密码本身就很少被使用到。我想要创建一个规则,允许我用“and”把用户名和密码组合起来,所以我需要一个“and”类型。事实上我们已经有一个,那就是刚才用到的 struct。

“AND”类型(积)

struct Credential {
  var username: String
  var password: String
}

Struct 是“and”类型。Credential 是一个用户名密码。“and”类型常用的名字叫“积类型”(product type)。

我鼓励你大声的把看法说出来。例如:“credential 结构是一个用户名和密码。”是否有意义?如果没有意义,这或许是一个错误的类型,又或许是你构建错了。

func login(credential: Credential,
           completion: (Token?, Error?) -> Void)


let credential = Credential(username: "rob",
                            password: "s3cret")
login(credential: credential) { (token, error) in
    if let token = token {
        // success
    } else if let error = error {
        // failure
    }
}

现在我们可以把用户名密码换成 Credential:这也使得我们的验证函数变得更短,更清晰,还开辟了更多不错的可能:对 Credential 进行扩展,或者用其他类型来代替它们。或许我们想要一个 one0time 密码,一个访问令牌(access tokens),或者 Facebook、Google 等第三方登录。现在我不需要修改其他地方的代码,因为这里只需传一个凭证即可。

但这依旧有问题。我们已经传递了这个 (Token?, Error?) 元组(tuple) – 元组是“and”类型。它们都是匿名构造体。我们的意思是“这可能是一个 token?可能是一个 error”吗?所以有四种可能:都是,都不是,要么是 token,要么是 error。但任何场景下,只有其中两种可能性。如果我同时得到一个 token 和 error,怎么办?这是错误的情况吗?需要一个致命的错误码?需要忽略它吗?此时你需要思考一下才能针对性的测试。

“OR”类型(和)

问题是你不需要把所有情况都表示出来 – 你只需表达清楚这是 token 或是 error。那么是否有一个“or”类型提供我们使用吗?

enum Result<Value> {
    case success(Value)
    case failure(Error)
}

这是一个枚举(enum) – 枚举是“or”类型,而结构(struct)是“and”类型。“and”类型是“积类型”(product type),而“or”是“和类型”(sum type)。

func login(credential: Credential,
           completion: (Result<Token>) -> Void)
login(credential: credential) { result in
    switch result {
    case .success(let token): // success
    case .failure(let error): // failure
    }
}

我构建一个 result 类型。result 类型不是内建在 Swift 里,这真的让我很恼火。幸好它很容易构建。我们将赋予 value 更多上下文,从一个普通 value 变成有“登录成功”含义的 value。

我们这里的 error 也有了更多上下文,它更像处理失败情况的 error。如果返回像之前的结果,最终 token 会包含在里面,所有不可能发生的情况都需要编写一次测试。现在我们并不需要担心,因为已经不会发生这种情况了。我可不想为不存在的 bug 编写一个个测试。

我喜欢这个API。我通用一个 credential,它就会返回一个最终的 token。

总结

这是函数式编程真正的遗产,这些也应该带给 Swift:复杂的东西,可以分解成更小,更简单的东西。

我们可以为这些简单的东西找到一些通用的解决办法,并在程序有条理的使用一贯的规则将简单的东西放在一起。这能避免编译器不会出错,而且我认为在70年代的约翰·巴科斯(John Backus)也会认同这个观点。打破它,并重建(Break it down, build it up)。