Tryswift rob napier cover

Swift 和函数式编程的遗产

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


概述 (00:00)

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),把事情分开。

Receive news and updates from Realm straight to your inbox

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

当你有一些事需要重复执行多次,你可能需要类似一个 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 循环。

函数式工具 (5:54)

在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 { ... }

例子 (11:44)

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

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”类型(积)(14:50)

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”类型(和)(17:19)

问题是你不需要把所有情况都表示出来 - 你只需表达清楚这是 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。

总结(18:51)

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

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

About the content

This talk was delivered live in September 2016 at try! Swift NYC. The video was recorded, produced, and transcribed by Realm, and is published here with the permission of the conference organizers.

Rob Napier

Rob is co-author of iOS Programming Pushing the Limits. Before coming to Cocoa, he made his living sneaking into Chinese facilities in broad daylight. Later, he became a Mac developer for Dell. It’s not clear which was the stranger choice. He has a passion for the fiddly bits below the surface, like networking, performance, security, and text layout. He asks “but is it good Swift?” a lot.

4 design patterns for a RESTless mobile integration »

close