objc.io|objc中国函数swift


Chris Eidhof, Florian Kugler, Wouter Swiersta 著 陈聿菡, 杜欣, 王巍 译 英文版本 2.0 (2015 年 12 月),中文版本 1.1 (2016 年 6 月) © 2015 Kugler, Eggert und Eidhof GbR 版权所有 ObjC 中国 在中国地区独家翻译和销售授权 获取更多书籍或文章,请访问 http://objccn.io 电子邮件: mail@objccn.io 1 引言 译序 10 2 函数式思想 案例:Battleship 13 ⼀等函数 19 类型驱动开发 23 注解 23 3 案例研究:封装 Core Image 滤镜类型 25 构建滤镜 25 组合滤镜 28 理论背景:柯⾥化 29 讨论 31 4 Map、Filter 和 Reduce 泛型介绍 33 Filter 37 Reduce 38 实际运⽤ 42 泛型和 Any 类型 43 注释 45 5 可选值 案例研究:字典 47 玩转可选值 50 为什么使⽤可选值? 56 6 案例研究:QuickCheck 构建 QuickCheck 61 缩⼩范围 65 随机数组 67 使⽤ QuickCheck 70 展望 71 7 不可变性的价值 变量和引⽤ 73 值类型与引⽤类型 74 讨论 77 8 枚举 关于枚举 81 关联值 83 添加泛型 85 Swift 中的错误处理 86 再聊聊可选值 87 数据类型中的代数学 88 为什么使⽤枚举? 90 9 纯函数式数据结构 ⼆叉搜索树 92 基于字典树的⾃动补全 98 讨论 105 10 案例研究:图表 绘制正⽅形和圆 107 核⼼数据结构 110 计算与绘制 113 创建视图与 PDF 117 额外的组合算⼦ 118 讨论 120 11 生成器和序列 ⽣成器 123 序列 128 案例研究:遍历⼆叉树 131 案例研究:优化 QuickCheck 的范围收缩 132 不⽌是 Map 与 Filter 135 12 案例研究:解析器组合算子 核⼼部分 140 选择 143 顺序解析 144 便利组合算⼦ 150 ⼀个简单的计算器 154 13 案例研究:构建一个表格应用 ⽰例代码 161 解析器 161 求值器 171 GUI 175 14 函子、适用函子与单子 函⼦ 180 适⽤函⼦ 181 单⼦ 184 讨论 187 15 尾声 拓展阅读 189 结语 190 参考文献 引言 1 为什么写这本书?关于 Swift,已经有⼤量来⾃ Apple 的现成⽂档,⽽且还有更多的书正在编 写中。为什么世界上依然需要关于这种编程语⾔的另⼀本书呢? 这本书尝试让你学会以函数式的⽅式进⾏思考。我们认为 Swift 有着合适的语⾔特性来适配函 数式的编程⽅法。然⽽是什么使得程序具有函数式特性?⼜为何要⼀开始就学习关于函数式的 内容呢? 很难给出函数式的准确定义 — 其实同样地,我们也很难给出⾯向对象编程,亦或是其它编程范 式的准确定义。因此,我们会尽量把重点放在我们认为设计良好的 Swift 函数式程序应该具有 的⼀些特质上: → 模块化: 相较于把程序认为是⼀系列赋值和⽅法调⽤,函数式开发者更倾向于强调每个 程序都能够被反复分解为越来越⼩的模块单元,⽽所有这些块可以通过函数装配起来, 以定义⼀个完整的程序。当然,只有当我们能够避免在两个独⽴组件之间共享状态时, 才能将⼀个⼤型程序分解为更⼩的单元。这引出我们的下⼀个关注特质。 → 对可变状态的谨慎处理: 函数式编程有时候 (被半开玩笑地) 称为 “⾯向值编程”。⾯向 对象编程专注于类和对象的设计,每个类和对象都有它们⾃⼰的封装状态。然⽽,函数 式编程强调基于值编程的重要性,这能使我们免受可变状态或其他⼀些副作⽤的困扰。 通过避免可变状态,函数式程序⽐其对应的命令式或者⾯向对象的程序更容易组合。 → 类型: 最后,⼀个设计良好的函数式程序在使⽤类型时应该相当谨慎。精⼼选择你的数 据和函数的类型,将会有助于构建你的代码,这⽐其他东西都重要。Swift 有⼀个强⼤ 的类型系统,使⽤得当的话,它能够让你的代码更加安全和健壮。 我们认为这些特质是 Swift 程序员可能从函数式编程社区学习到的精华点。在这本书中,我们 将通过许多实例和学习案例说明以上⼏点。 根据我们的经验,学习⽤函数式的⽅式思考并不容易。它挑战了我们既有的熟练解决问题的⽅ 式。对于习惯写 for 循环的程序员来说,递归可能让我们倍感迷惑;赋值语句和全局状态的缺 失让我们⼨步难⾏;更不⽤提闭包,泛型,⾼阶函数和单⼦ (Monad),这些东西简直让⼈痛不 欲⽣。 在这本书中,我们假定你以前有过 Objective-C (或其他⼀些⾯向对象的语⾔) 的编程经验。书 中不会涵盖 Swift 的基础知识,或者教你建⽴你的第⼀个 Xcode ⼯程,不过我们会尝试在适当 的时候引⽤现有的 Apple ⽂档。你应当能⾃如地阅读 Swift 程序,并且熟悉常⻅的编程概念, 如类,⽅法和变量等。如果你只是刚刚开始学习编程,这本书可能并不适合你。 在这本书中,我们希望让函数式编程易于理解,并消除⼈们对它的⼀些偏⻅。使⽤这些理念去 改善你的代码并不需要你拥有数学的博⼠学位!函数式编程并不是 Swift 编程的唯⼀⽅式。但 是我们相信学习函数式编程会为你的⼯具箱添加⼀件重要的新⼯具,不论你使⽤那种语⾔,这 件⼯具都会让你成为⼀个更好的开发者。 示例代码 你可以在我们的 GitHub 仓库中找到这本书⾥所有的⽰例代码。这个仓库包括⼀些章节的 playgrounds,以及其它章节的 Swift ⽂件和 OS X ⼯程。 书籍更新 随着 Swift 的发展,我们会继续更新和改进这本书。如果你遇到任何错误,或者是想给我们其 它类型的反馈,请在我们的 GitHub 仓库中创建⼀个 issue。 致谢 我们想要感谢众多帮助我们塑造了这本书的⼈。在此我们想要特别提及其中⼏位: Natalye Childress 是我们的出版编辑。她给了我们很多宝贵的反馈意⻅,不仅保证了语⾔的正 确性和⼀致性,⽽且确保了本书清晰易懂。 Sarah Lincoln 设计了本书的封⾯和布局。 Wouter 想要感谢乌得勒⽀⼤学允许他能够在这本书上投⼊时间进⾏编写。 我们想要感谢测试版读者在本书的写作过程中给我们的反馈 (按字⺟顺序排列): Adrian Kosmaczewski, Alexander Altman, Andrew Halls, Bang Jun-young, Daniel Eggert, Daniel Steinberg, David Hart, David Owens II, Eugene Dorfman, f-dz-v, Henry Stamerjohann, J Bucaran, Jamie Forrest, Jaromir Siska, Jason Larsen, Jesse Armand, John Gallagher, Kaan Dedeoglu, Kare Morstol, Kiel Gillard, Kristopher Johnson, Matteo Piombo, Nicholas Outram, Ole Begemann, Rob Napier, Ronald Mannak, Sam Isaacson, Ssu Jen Lu, Stephen Horne, TJ, Terry Lewis, Tim Brooks, Vadim Shpakovski. Chris, Florian, and Wouter 译序 随着程序语⾔的发展,我们作为软件开发⼈员,所熟知和使⽤的⼯具也在不断进化。以 Java 和 C++ 为代表的⾯向对象编程的编程⽅式在上世纪企业级的软件开发中⼤放异彩,然⽽随着软件 ⾏业不断发展,开发者们发现了⾯向对象范式的诸多不⾜。⾯向对象强调的是将与某数据类型 相关的⼀系列操作都封装到该数据类型中去,因此,在数据类型中难免存在⼤量状态,以及相 关的⾏为。虽然这很符合⼈类的逻辑直觉,但是当类型关系变得错综复杂的时候,类型中状态 的改变和类型之间彼此的继承和依赖将使程序的复杂度⼏何上升。 避免使⽤程序状态和可变对象,是降低程序复杂度的有效⽅式之⼀,⽽这也正是函数式编程的 精髓。函数式编程强调执⾏的结果,⽽⾮执⾏的过程。我们先构建⼀系列简单却具有⼀定功能 的⼩函数,然后再将这些函数进⾏组装以实现完整的逻辑和复杂的运算,这是函数式编程的基 本思想。 正如上⾯引⾔所述,Swift 是⼀⻔有着合适的语⾔特性来适配函数式编程⽅法的优秀语⾔。这个 世界上最纯粹的函数式编程语⾔⾮ Haskell 莫属,但是由于我国程序开发的起步和热⻔相对西 ⽅世界要晚⼀些,使⽤ Haskell 的开发者可谓寥寥⽆⼏,因此 Haskell 在国内的影响⼒也⼗分 有限。对于国内的不少开发者,特别是 iOS / OS X 的开发者来说,Swift 可能是我们第⼀次真正 有机会去接触和使⽤的⼀⻔函数式特性语⾔。相⽐于很多已有的函数式编程语⾔,Swift 在语法 上更加优雅灵活,语⾔本⾝也遵循了函数式的设计模式。作为函数式编程的⼊⻔语⾔,可以说 Swift 是⾮常理想的选择。⽽本书正是⼀本引领你进⼊ Swift 函数式编程世界的优秀读物,让更 多的中国开发者有机会接触并了解 Swift 语⾔函数式的⼀⾯,正是我们翻译本书的⽬的所在。 本书⼤致上可以分为两个部分。⾸先,在第⼆章⾄第⼗章中,我们会介绍 Swift 函数式编程特 性的⼀些基本概念,包括⾼阶函数的使⽤⽅法,不可变量的必要性,可选值的存在价值,枚举 在函数式编程中的意义,以及纯函数式数据结构的优势等内容。这些都是函数式编程中的基本 概念,也构成了 Swift 函数式特性甚⾄是这⻔语⾔的基础。当然,在这些概念讲解中我们也穿 插了不少研究案例,以帮助读者真正理解这些基本概念,并对在何时使⽤它们以及使⽤它们为 程序设计带来的改善形成直观印象。第⼆部分从第⼗⼀章开始,相⽐于前⾯的章节,这部分属 于本书的进阶内容。我们将从构建最基本的⽣成器和序列开始,利⽤解析器组合算⼦构建⼀个 解析器库,并最终实现⼀个相对复杂的公式解析器和函数式的表格应⽤。这部分内容环环相扣, 因为内容抽象度较⾼,所以理解起来也可能⽐较困难。如果你在阅读最后表格应⽤章节时遇到 ⿇烦的话,我们强烈建议你下载对应的完整源码进⾏研究,并且折回头去再次阅读第⼆部分的 相关章节。随着你对各个函数式算⼦的深⼊理解,函数式编程的概念和思想将⾃然⽽然进⼊你 的⾎液,这将丰富你的知识体系,并会对之后的开发⼯作⼤有裨益。 本书原版的三位作者都是富有经验的函数式编程⽅法的使⽤者或教师,他们将⾃⼰对于函数式 编程的理解和 Swift 中的相关特性进⾏了对应和总结,并将这些联系揭⽰了出来。⽽中⽂版的 三位译者花费了⼤量时间和精⼒,试图将这些规律以更易于理解的组织⽅式和语⾔,带给国内 的开发者们。不过不论是原作者还是译者,其实和各位读者⼀样,都只不过是普通开发者中的 ⼀员,所以本书出现谬漏可能在所难免。如果您在阅读时发现了问题,可以给我们发邮件,或 是在本书 issue ⻚⾯提出,我们将及时研究并加以改进。 事不宜迟,现在就让我们开始在函数式的世界中遨游⼀番吧。 陈⾀菡,杜欣,王巍 函数式思想 2 函数在 Swift 中是⼀等值 (rst-class-values),换句话说,函数可以作为参数被传递到其它函 数,也可以作为其它函数的返回值。如果你习惯了使⽤像是整型,布尔型或是结构体这样的简 单类型来编程,那么这个理念可能看来⾮常奇怪。在本章中,我们会尽可能解释为什么⼀等函 数是很有⽤的语⾔特性,并实际地提供本书的第⼀个函数式编程案例。 案例:Battleship 我们将会⽤⼀个⼩案例来引出⼀等函数:这个例⼦是你在编写战舰类游戏时可能需要实现的⼀ 个核⼼函数。我们把将要看到的问题归结为,判断⼀个给定的点是否在射程范围内,并且距离 友⽅船舶和我们⾃⾝都不太近。 ⾸先,你可能会写⼀个很简单的函数来检验⼀个点是否在范围内。为了简明易懂,我们假定我 们的船位于原点。这样⼀来,我们就可以将想要描述的区域形象化,如图 2.1: !ringRange 图 2.1: 位于原点的船舶射程范围内的点 ⾸先,我们定义两种类型,Distance 和 Position: typealias Distance = Double struct Position { var x: Double var y: Double } 然后我们在 Position 中添加⼀个函数 inRange(_:),⽤于检验⼀个点是否在图 2.1 中的灰⾊区 域⾥。使⽤⼀些基础的⼏何知识,我们可以像下⾯这样定义这个函数: extension Position { func inRange(range: Distance) -> Bool { return sqrt(x * x + y * y) <= range } } 如果假设我们总是位于原点,那现在这样就可以正常⼯作了。但是船舶还可能在原点以外的其 它位置出现,我们可以更新⼀下形象化图,如图 2.2 所⽰: !ringRange position.x position.y 图 2.2: 允许船有它自己的位置 考虑到这⼀点,我们引⼊⼀个结构体 Ship,它有⼀个属性为 position: struct Ship { var position: Position var ringRange: Distance var unsafeRange: Distance } ⽬前,姑且先忽略附加属性 unsafeRange。⼀会⼉我们会回到这个问题。 我们向结构体 Ship 中添加⼀个 canEngageShip(_:) 函数对其进⾏扩展,这个函数允许我们检 验是否有另⼀艘船在范围内,不论我们是位于原点还是其它任何位置: extension Ship { func canEngageShip(target: Ship) -> Bool { let dx = target.position.x - position.x let dy = target.position.y - position.y let targetDistance = sqrt(dx * dx + dy * dy) return targetDistance <= ringRange } } 也许现在你已经意识到,我们同时还想避免⽬标船舶离你过近。我们可以⽤图 2.3 来说明新情 况,我们想要瞄准的仅仅只有那些对我们当前位置⽽⾔在 unsafeRange (不安全范围) 外的敌 ⼈: unsafeRange !ringRange 图 2.3: 避免与过近的敌方船舶交战 这样⼀来,我们需要再⼀次修改代码,使 unsafeRange 属性能够发挥作⽤: extension Ship { func canSafelyEngageShip(target: Ship) -> Bool { let dx = target.position.x - position.x let dy = target.position.y - position.y let targetDistance = sqrt(dx * dx + dy * dy) return targetDistance <= ringRange && targetDistance > unsafeRange } } 最后,我们还需要避免⽬标船舶过于靠近我⽅的任意⼀艘船。我们再⼀次将其形象化,⻅图 2.4: unsafeRange !ringRange Friendly 图 2.4: 避免敌方过于接近友方船舶 相应地,我们可以向 canSafelyEngageShip(_:) 函数添加另⼀个参数代表友好船舶位置: extension Ship { func canSafelyEngageShip1(target: Ship, friendly: Ship) -> Bool { let dx = target.position.x - position.x let dy = target.position.y - position.y let targetDistance = sqrt(dx * dx + dy * dy) let friendlyDx = friendly.position.x - target.position.x let friendlyDy = friendly.position.y - target.position.y let friendlyDistance = sqrt(friendlyDx * friendlyDx + friendlyDy * friendlyDy) return targetDistance <= ringRange && targetDistance > unsafeRange && (friendlyDistance > unsafeRange) } } 随着代码的发展,它变得越来越难维护。这个函数包含了⼀⼤段复杂的计算的代码。我们可以 在 Position 中添加两个负责⼏何运算的辅助函数让这段代码变得清晰易懂⼀些: extension Position { func minus(p: Position) -> Position { return Position(x: x - p.x, y: y - p.y) } var length: Double { return sqrt(x * x + y * y) } } 添加了辅助函数之后,函数变成了下⾯这样: extension Ship { func canSafelyEngageShip2(target: Ship, friendly: Ship) -> Bool { let targetDistance = target.position.minus(position).length let friendlyDistance = friendly.position.minus(target.position).length return targetDistance <= ringRange && targetDistance > unsafeRange && (friendlyDistance > unsafeRange) } } 现在的代码已经更易读了,但是我们还想更进⼀步,⽤⼀种声明式的⽅法来明确现有的问题。 一等函数 在当前 canSafelyEngageShip 的函数体中,主要的⾏为是为构成返回值的布尔条件组合进⾏ 编码。在这个简单的例⼦中,虽然想知道这个函数做了什么并不是太难,但我们还是想要⼀个 更模块化的解决⽅案。 我们已经在 Position 中引⼊了辅助函数使⼏何运算的代码更清晰易懂。⽤同样的⽅式,我们现 在要添加⼀个函数,以更加声明式的⽅式来判断⼀个区域内是否包含某个点。 原来的问题归根结底是要定义⼀个函数来判断⼀个点是否在范围内。这样⼀个函数的类型会像 是下⾯这样的: func pointInRange(point: Position) -> Bool { // 方法的具体实现 } 这个函数的类型将会⾮常重要,所以我们打算给它⼀个独⽴的名字: typealias Region = Position -> Bool 从现在开始,Region 类型将指代把 Position 转化为 Bool 的函数。严格来说这不是必须的,但 是它可以让我们更容易理解在接下来即将看到的⼀些类型。 我们使⽤⼀个能判断给定点是否在区域内的函数来代表⼀个区域,⽽不是定义⼀个对象或结构 体来表⽰它。如果你不习惯函数式编程,这可能看起来会很奇怪,但是记住:在 Swift 中函数 是⼀等值!我们有意识地选择了 Region 作为这个类型的名字,⽽⾮ CheckInRegion 或 RegionBlock 这种字⾥⾏间暗⽰着它们代表⼀种函数类型的名字。函数式编程的核⼼理念就是 函数是值,它和结构体、整型或是布尔型没有什么区别 —— 对函数使⽤另外⼀套命名规则会 违背这⼀理念。 我们现在要写⼏个函数来创建、控制和合并各个区域。 我们定义的第⼀个区域是以原点为圆⼼的圆 (circle): func circle(radius: Distance) -> Region { return { point in point.length <= radius } } 当然,并不是所有圆的圆⼼都是原点。我们可以给 circle 函数添加更多的参数来解决这个问题。 要得到⼀个圆⼼是任意定点的圆,我们只需要添加另⼀个代表圆⼼的参数,并确保在计算新区 域时将这个参数考虑进去: func circle2(radius: Distance, center: Position) -> Region { return { point in point.minus(center).length <= radius } } 然⽽,如果我们想对更多的图形组件 (例如,想象我们不仅有圆,还有矩形或其它形状) 做出同 样的改变,可能需要重复这些代码。更加函数式的⽅式是写⼀个区域变换函数。这个函数按⼀ 定的偏移量移动⼀个区域: func shift(region: Region, offset: Position) -> Region { return { point in region(point.minus(offset)) } } 调⽤ shift (region, offset: offset) 函数会将区域向右上⽅移动,偏移量分别是 offset.x 和 offset.y。我们需要的是⼀个传⼊ Position 并返回 Bool 的函数 Region。为此,我们需要另写 ⼀个闭包,它接受我们要检验的点,这个点减去偏移量之后我们得到⼀个新的点。最后,为了 检验新点是否在原来的区域内,我们将它作为参数传递给 region 函数。 这是函数式编程的核⼼概念之⼀:为了避免创建像 circle2 这样越来越复杂的函数,我们编写 了⼀个 shift (_:offset:) 函数来改变另⼀个函数。例如,⼀个圆⼼为 (5, 5) 半径为 10 的圆,可 以⽤下⾯的⽅式表⽰: shift ( circle (10), offset: Position(x: 5, y: 5)) 还有很多其它的⽅法可以变换已经存在的区域。例如,也许我们想要通过反转⼀个区域以定义 另⼀个区域。这个新产⽣的区域由原区域以外的所有点组成: func invert(region: Region) -> Region { return { point in !region(point) } } 我们也可以写⼀个函数将既存区域合并为更⼤更复杂的区域。⽐如,下⾯两个函数分别可以计 算参数中两个区域的交集和并集: func intersection(region1: Region, _ region2: Region) -> Region { return { point in region1(point) && region2(point) } } func union(region1: Region, _ region2: Region) -> Region { return { point in region1(point) || region2(point) } } 当然,我们可以利⽤这些函数来定义更丰富的区域。difference 函数接受两个区域作为参数 —— 原来的区域和要减去的区域 —— 然后为所有在第⼀个区域中且不在第⼆个区域中的点构 建⼀个新的区域: func difference(region: Region, minus: Region) -> Region { return intersection(region, invert(minus)) } 这个例⼦告诉我们,在 Swift 中计算和传递函数的⽅式与整型或布尔型没有任何不同。这让我 们能够写出⼀些基础的图形组件 (⽐如圆),进⽽能以这些组件为基础,来构建⼀系列函数。每 个函数都能修改或是合并区域,并以此创建新的区域。⽐起写复杂的函数来解决某个具体的问 题,现在我们完全可以通过将⼀些⼩型函数装配起来,⼴泛地解决各种各样的问题。 现在让我们把注意⼒转回原来的例⼦。关于区域的⼩型函数库已经准备就绪,我们可以像下⾯ 这样重构 canSafelyEngageShip(_:friendly:) 这个复杂的函数: extension Ship { func canSafelyEngageShip(target: Ship, friendly: Ship) -> Bool { let rangeRegion = difference(circle(ringRange), minus: circle(unsafeRange)) let ringRegion = shift(rangeRegion, offset: position) let friendlyRegion = shift(circle (unsafeRange), offset: friendly.position) let resultRegion = difference(ringRegion, minus: friendlyRegion) return resultRegion(target.position) } } 这段代码定义了两个区域:ringRegion 和 friendlyRegion。通过计算这两个区域的差集 (即在 ringRegion 中且不在 friendlyRegion 中的点的集合),我们可以得到我们感兴趣的区域。将这 个区域函数作⽤在⽬标船舶的位置上,我们就可以计算所需的布尔值了。 ⾯对同⼀个问题,与原来的 canSafelyEngageShip1(_:friendly:) 函数相⽐,使⽤ Region 函数 重构后的版本是更加声明式的解决⽅案。我们坚信后⼀个版本会更容易理解,因为我们的解决 ⽅案是装配式的。你可以探究组成它的每个部分,例如 ringRegion 和 friendlyRegion,看⼀ 看它们是如何被装配并解决原来的问题的。另⼀⽅⾯,原来庞⼤的函数混合了各个组成区域的 描述语句和描述它们所需要的算式。通过定义我们之前提出的辅助函数将这些关注点进⾏分离, 显著提⾼了复杂区域的组合性和易读性。 能做到这样,⼀等函数是⾄关重要的。虽然 Objective-C 也⽀持⼀等函数,或者说是 block,也 可以做到类似的事情,但遗憾的是,在 Objective-C 中使⽤ block ⼗分繁琐。⼀部分原因是因 为语法问题:block 的声明和 block 的类型与 Swift 的对应部分相⽐并不是那么简单。在后⾯ 的章节中,我们也将看到泛型如何让⼀等函数更加强⼤,远⽐ Objective-C 中⽤ blocks 实现更 加容易。 我们定义 Region 类型的⽅法有它⾃⾝的缺点。我们选择了将 Region 类型定义为简单类型,并 作为 Position -> Bool 函数的别名。其实,我们也可以选择将其定义为⼀个包含单⼀函数的结 构体: struct Region { let lookup: Position -> Bool } 接下来我们可以⽤ extensions 的⽅式为结构体定义⼀些类似的函数,来代替对原来的 Region 类型进⾏操作的那些函数。这可以让我们能够通过对区域进⾏反复的函数调⽤来变换这个区域, 直⾄得到需要的复杂区域,⽽不⽤像以前那样将区域作为参数传递给其他函数: rangeRegion.shift(ownPosition).difference(friendlyRegion) 这种⽅法有⼀个优点,它需要的括号更少。再者,这种⽅式下 Xcode 的⾃动补全在装配复杂的 区域时会⼗分有⽤。不过,为了便于展⽰,我们选择了使⽤简单的类型别名以突出在 Swift 中 使⽤⾼阶函数的⽅法。 此外,值得指出的是,现在我们不能够看到⼀个区域是如何被构建的:它是由更⼩的区域组成 的吗?还是单纯只是⼀个以原点为圆⼼的圆?我们唯⼀能做的是检验⼀个给定的点是否在区域 内。如果想要形象化⼀个区域,我们只能对⾜够多的点进⾏采样来⽣成 (⿊⽩) 位图。 在后⾯的章节中,我们将使⽤另外⼀种设计,来帮助你解答这些问题。 类型驱动开发 在引⾔中,我们提到了函数式编程可以⽤规范的⽅式将函数作为参数装配为规模更⼤的程序。 在本章中,我们看到了⼀个以这种函数式⽅式设计的具体例⼦。我们定义了⼀系列函数来描述 区域。每⼀个函数单打独⽃的话都并不强⼤。然⽽装配到⼀起时,却可以描述你绝不会想要从 零开始编写的复杂区域。 解决的办法简单⽽优雅。这与单纯地将 canSafelyEngageShip1(_:friendly:) 函数写成⼀些分开 的⽅法那种重构⽅式是完全不同的。我们确定了如何来定义区域,这是⾄关重要的设计决策。 当我们选择了 Region 类型之后,其它所有的定义就都⾃然⽽然,⽔到渠成了。这个例⼦给我们 的启⽰是,我们应该谨慎地选择类型。这⽐其他任何事都重要,因为类型将左右开发流程。 注解 本章提供的代码受到了⼀个 Haskell 解决⽅案的启发,该⽅案解决了由美国⾼等研究计划局 (ARPA) 的 Hudak and Jones (1994) 提出的⼀个问题。 Objective-C 通过引⼊ blocks 实现了对⼀等函数的⽀持:你可以将函数和闭包作为参数并轻松 地使⽤内联的⽅式定义它们。然⽽,在 Objective-C 中使⽤它们并不像在 Swift 中⼀样⽅便, 尽管两者在语意上完全相同。 从历史上看,⼀等函数的理念可以追溯到 Church 的 lambda 演算 (Church 1941; Barendregt 1984)。此后,包括 Haskell,OCaml,Standard ML,Scala 和 F# 在内的⼤量 (函数式) 编程 语⾔都不同程度地借鉴了这个概念。 案例研究:封装 Core Image 3 前⼀章介绍了⾼阶函数的概念,并展⽰了将函数作为参数传递给其它函数的⽅法。不过,使⽤ 的例⼦可能与你⽇常写的 “真实” 代码相去甚远。在本章中,我们将会围绕⼀个已经存在且⾯向 对象的 API,展⽰如何使⽤⾼阶函数将其以⼩巧且函数式的⽅式进⾏封装。 Core Image 是⼀个强⼤的图像处理框架,但是它的 API 有时可能略显笨拙。Core Image 的 API 是弱类型的 —— 我们通过键值编码 (KVC) 来配置图像滤镜 (lter)。在使⽤参数的类型或名字 时,我们都使⽤字符串来进⾏表⽰,这⼗分容易出错,极有可能导致运⾏时错误。⽽我们开发 的新 API 将会利⽤类型来避免这些原因导致的运⾏时错误,最终我们将得到⼀组类型安全⽽且 ⾼度模块化的 API。 即使你不熟悉 Core Image,或者不能完全理解本章代码⽚段的细节,也⼤可不必担⼼。我们的 ⽬标并不是围绕 Core Image 构建⼀个完整的封装,⽽是要说明如何把像⾼阶函数这样的函数 式编程概念运⽤到实际的⽣产代码中。 滤镜类型 CIFilter 是 Core Image 中的核⼼类之⼀,⽤于创建图像滤镜。当实例化⼀个 CIFilter 对象时, 你 (⼏乎) 总是通过 kCIInputImageKey 键提供输⼊图像,再通过 kCIOutputImageKey 键取回 处理后的图像。取回的结果可以作为下⼀个滤镜的输⼊值。 在本章即将开发的 API 中,我们会尝试封装应⽤这些键值对的具体细节,从⽽呈现给⽤⼾⼀个 安全的强类型 API。我们将 Filter 类型定义为⼀个函数,该函数接受⼀个图像作为参数并返回 ⼀个新的图像: typealias Filter = CIImage -> CIImage 我们将在这个类型的基础上进⾏后续的构建。 构建滤镜 现在我们已经定义了 Filter 类型,接着就可以开始定义函数来构建特定的的滤镜了。这些函数 在接受特定滤镜所需要的参数之后,构造并返回⼀个 Filter 类型的值。它们的基本形态⼤概都 是下⾯这样: func myFilter(/* parameters */) -> Filter 模糊 让我们来定义第⼀个简单的滤镜 —— ⾼斯模糊滤镜。定义它只需要模糊半径这⼀个参数: func blur(radius: Double) -> Filter { return { image in let parameters = [ kCIInputRadiusKey: radius, kCIInputImageKey: image ] guard let lter = CIFilter (name: "CIGaussianBlur", withInputParameters: parameters) else { fatalError() } guard let outputImage = lter.outputImage else { fatalError() } return outputImage } } ⼀切就是这么简单。blur 函数返回⼀个新函数,新函数接受⼀个 CIImage 类型的参数 image, 并返回⼀个新图像 (return lter .outputImage)。因此,blur 函数的返回值满⾜我们之前定义 的 CIImage -> CIImage,也就是 Filter 类型。 这个例⼦仅仅只是对 Core Image 中⼀个已经存在的滤镜进⾏的简单封装。我们可以反复使⽤ 相同的模式来创建⾃⼰的滤镜函数。 颜色叠层 现在让我们来定义⼀个能够在图像上覆盖纯⾊叠层的滤镜。Core Image 默认不包含这样⼀个滤 镜,但是我们完全可以⽤已经存在的滤镜来组成它。 我们将使⽤的两个基础组件:颜⾊⽣成滤镜 (CIConstantColorGenerator) 和图像覆盖合成滤镜 (CISourceOverCompositing)。⾸先让我们来定义⼀个⽣成固定颜⾊的滤镜: func colorGenerator(color: NSColor) -> Filter { return {_ in guard let c = CIColor(color: color) else { fatalError () } let parameters = [kCIInputColorKey: c] guard let lter = CIFilter (name: "CIConstantColorGenerator", withInputParameters: parameters) else { fatalError() } guard let outputImage = lter.outputImage else { fatalError() } return outputImage } } 这段代码看起来和我们⽤来定义模糊滤镜的代码⾮常相似,但是有⼀个显著的区别:颜⾊⽣成 滤镜不检查输⼊图像。因此,我们不需要给返回函数中的图像参数命名。取⽽代之,我们使⽤ ⼀个匿名参数 _ 来强调滤镜的输⼊图像参数是被忽略的。 接下来,我们将要定义合成滤镜: func compositeSourceOver(overlay: CIImage) -> Filter { return { image in let parameters = [ kCIInputBackgroundImageKey: image, kCIInputImageKey: overlay ] guard let lter = CIFilter (name: "CISourceOverCompositing", withInputParameters: parameters) else { fatalError() } guard let outputImage = lter.outputImage else { fatalError() } let cropRect = image.extent return outputImage.imageByCroppingToRect(cropRect) } } 在这⾥我们将输出图像剪裁为与输⼊图像⼀致的尺⼨。严格来说,这不是必须的,⽽完全取决 于我们希望滤镜如何⼯作。不过,这个选择在我们即将涉及的例⼦中效果很好。 最后,我们通过结合两个滤镜来创建颜⾊叠层滤镜: func colorOverlay(color: NSColor) -> Filter { return { image in let overlay = colorGenerator(color)(image) return compositeSourceOver(overlay)(image) } } 我们再次返回了⼀个接受图像作为参数的函数。colorOverlay 函数⾸先调⽤了 colorGenerator 滤镜。colorGenerator 滤镜需要⼀个 color 作为参数,然后返回⼀个新的滤镜,因此代码⽚段 colorGenerator(color) 是 Filter 类型。⽽ Filter 类型本⾝就是⼀个从 CIImage 到 CIImage 的 函数;因此我们可以向 colorGenerator(color) 函数传递⼀个附加的 CIImage 类型的参数,最终 我们能够得到⼀个 CIImage 类型的新叠层。这就是我们在定义 overlay 的过程中所发⽣的全部 事情事,可以⼤致概括为 —— ⾸先使⽤ colorGenerator 函数创建⼀个滤镜,接着向这个滤镜 传递⼀个 image 参数来创建新图像。与之类似,返回值 compositeSourceOver(overlay)(image) 由⼀个通过 compositeSourceOver(overlay) 函数构建 的滤镜和随即被作为参数的 image 组成。 组合滤镜 到现在为⽌,我们已经定义了模糊滤镜和颜⾊叠层滤镜,可以把它们组合在⼀起使⽤:⾸先将 图像模糊,然后再覆盖上⼀层红⾊叠层。让我们来载⼊⼀张图⽚试试看: let url = NSURL(string: "http://www.objc.io/images/covers/16.jpg")! let image = CIImage(contentsOfURL: url)! 现在我们可以链式地将两个滤镜应⽤到载⼊的图像上: let blurRadius = 5.0 let overlayColor = NSColor.redColor().colorWithAlphaComponent(0.2) let blurredImage = blur(blurRadius)(image) let overlaidImage = colorOverlay(overlayColor)(blurredImage) 我们再⼀次通过创建滤镜来处理图像,例如先创建了 blur(blurRadius) 滤镜,然后将其运⽤到 图像上。 复合函数 当然,我们可以将上⾯代码⾥两个调⽤滤镜的表达式简单合为⼀体: let result = colorOverlay(overlayColor)(blur(blurRadius)(image)) 然⽽,由于括号错综复杂,这些代码很快失去了可读性。更好的解决⽅式是⾃定义⼀个运算符 来组合滤镜。为了定义该运算符,⾸先我们要定义⼀个⽤于组合滤镜的函数: func composeFilters(lter1: Filter , _ lter2 : Filter ) -> Filter { return { image in lter2 ( lter1 (image)) } } composeFilters 函数接受两个 Filter 类型的参数,并返回⼀个新定义的滤镜。这个复合滤镜接 受⼀个 CIImage 类型的图像参数,然后将该参数传递给 lter1,取得返回值之后再传递给 lter2。我们可以使⽤复合函数来定义复合滤镜,就像下⾯这样: let myFilter1 = composeFilters(blur(blurRadius), colorOverlay(overlayColor)) let result1 = myFilter1(image) 为了让代码更具可读性,我们可以再进⼀步,为组合滤镜引⼊运算符。诚然,随意⾃定义运算 符并不⼀定对提升代码可读性有帮助。不过,在图像处理库中,滤镜的组合是⼀个反复被讨论 的问题,所以引⼊运算符极有意义: inx operator >>> { associativity left } func >>> ( lter1 : Filter , lter2 : Filter ) -> Filter { return { image in lter2 ( lter1 (image)) } } 与之前使⽤ composeFilters 的⽅法相同,现在我们可以使⽤ >>> 运算符达到⽬的: let myFilter2 = blur(blurRadius) >>> colorOverlay(overlayColor) let result2 = myFilter2(image) 由于已经定义的运算符 >>> 是左结合的 (left-associative),就像 Unix 的管道⼀样,滤镜将以从 左到右的顺序被应⽤到图像上。 我们定义的组合滤镜运算符是⼀个复合函数的例⼦。在数学中,f 和 g 两个函数构成的复合函 数有时候被写作 f · g,表⽰定义的新函数将输⼊的 x 映射到 f(g(x))。除了顺序,这恰恰也是 我们的 >>> 运算符所做的:将⼀个图像参数传递给运算符操作的两个滤镜。 理论背景:柯里化 在本章中,我们已经看到了⽤于定义接受两个参数的函数的两种⽅法。对于⼤多数程序员来说, 应该会觉得第⼀种⻛格更熟悉: func add1(x: Int, _ y: Int) -> Int { return x + y } add1 函数接受两个整型参数并返回它们的和。然⽽在 Swift 中,我们对该函数的定义还可以有 另⼀个版本: func add2(x: Int) -> (Int -> Int) { return { y in return x + y } } 这⾥的 add2 函数接受第⼀个参数 x 之后,返回⼀个闭包,然后等待第⼆个参数 y。这两个 add 函数的调⽤⽅法⾃然也是不同的: add1(1, 2) add2(1)(2) 3 在第⼀种⽅法中,我们将两个参数同时传递给 add1;⽽第⼆种⽅法则⾸先向函数传递第⼀个参 数 1,然后将返回的函数应⽤到第⼆个参数 2。两个版本是完全等价的:我们可以根据 add2 来 定义 add1,反之亦然。 在 Swift 中,我们甚⾄可以省略 add2 函数的⼀个 return 关键字和返回类型中的某些括号,然 后写为下⾯这样: func add2(x: Int) -> Int -> Int { return { y in x + y } } 函数中的箭头 -> 向右结合。这也就是说,你可以将 A -> B -> C 理解为 A -> (B -> C)。然⽽在 本书中,我们通常会为函数类型引⼊⼀个类型别名 (像我们对 Region 和 Filter 类型做的处理⼀ 样),或者是显式地写括号来提升代码的可读性。 add1 和 add2 的例⼦向我们展⽰了如何将⼀个接受多参数的函数变换为⼀系列只接受单个参数 的函数,这个过程被称为柯⾥化 (Currying),它得名于逻辑学家 Haskell Curry;我们将 add2 称为 add1 的柯⾥化版本。 那么,为什么说柯⾥化很有趣呢?正如迄今为⽌我们在本书中所看到的,在⼀些情况下,你可 能想要将函数作为参数传递给其它函数。如果我们有像 add1 ⼀样未柯⾥化的函数,那我们就 必须⽤到它的全部两个参数来调⽤这个函数。然⽽,对于⼀个像 add2 ⼀样被柯⾥化了的函数 来说,我们有两个选择:可以使⽤⼀个或两个参数来调⽤。在本章中为了创建滤镜⽽定义的函 数全部都已经被柯⾥化了 —— 它们都接受⼀个附加的图像参数。按照柯⾥化⻛格来定义滤镜, 我们可以很容易地使⽤ >>> 运算符将它们进⾏组合。假如我们⽤这些函数未柯⾥化的版本来构 建滤镜的话,虽然依然可以写出相同的滤镜,但是这些滤镜的类型将根据它们所接受的参数不 同⽽略有不同。这样⼀来,想要为这些不同类型的滤镜定义⼀个统⼀的组合运算符就要⽐现在 困难得多了。 讨论 这个例⼦再⼀次阐释了将复杂的代码拆解为⼩块的⽅式,⽽这些⼩块可以使⽤函数的⽅式进⾏ 重新装配,并形成完整的功能。本章的⽬标并不是为 Core Image 定义⼀个完整的 API,⽽是想 说明在更实际的案例中如何使⽤⾼阶函数和复合函数。 为什么要做这么多努⼒呢?事实上 Core Image API 已经很成熟并能够提供⼏乎所有你可能需要 的功能。但是尽管如此,我们相信本章所设计的 API 也有⼀些优点: → 安全 — 使⽤我们构筑的 API ⼏乎不可能发⽣由未定义键或强制类型转换失败导致的运 ⾏时错误。 → 模块化 — 使⽤ >>> 运算符很容易将滤镜进⾏组合。这样你可以将复杂的滤镜拆解为更 ⼩,更简单,且可复⽤的组件。此外,组合滤镜与组成它的组件是完全相同的类型,所 以你可以交替使⽤它们。 → 清晰易懂 — 即使你从未使⽤过 Core Image,也应该能够通过我们定义的函数来装配简 单的滤镜。要得到结果,你并不需要知道像 kCIOutputImageKey ⼀样特殊的字典键, 也不需要关⼼对 kCIInputImageKey 或 kCIInputRadiusKey 这样的键进⾏初始化。单看 类型,你⼏乎就能够知道如何使⽤ API,甚⾄不需要更多⽂档。 我们的 API 提供了⼀系列能够⽤来定义和组合滤镜的函数。使⽤和复⽤任何⾃定义的滤镜都是 安全的。每个滤镜都能被独⽴测试和理解。⽐起原来的 Core Image API,我们的设计会更让⼈ 偏爱。如果要说理由的话,相信上⾯⼏点⾜够让⼈信服。 Map、Filter 和 Reduce 4 接受其它函数作为参数的函数有时被称为⾼阶函数。本章中,我们将在⼀些来⾃ Swift 标准库 中作⽤于数组的⾼阶函数中漫游。伴随这个过程,我们将介绍 Swift 的泛型,以及展⽰如何将 复杂计算运⽤于数组。 泛型介绍 假如我们需要写⼀个函数,它接受⼀个给定的整型数组,通过计算得到并返回⼀个新数组,新 数组各项为原数组中对应的整型数据加⼀。这⼀切,仅仅只需要使⽤⼀个 for 循环就能⾮常容 易地实现: func incrementArray(xs: [Int]) -> [Int] { var result: [Int] = [] for x in xs { result.append(x + 1) } return result } 现在假设我们还需要⼀个函数,⽤于⽣成⼀个每项都为参数数组对应项两倍的新数组。这同样 能很容易地使⽤⼀个 for 循环来实现: func doubleArray1(xs: [Int]) -> [Int] { var result: [Int] = [] for x in xs { result.append(x * 2) } return result } 这两个函数有⼤量相同的代码,我们能不能将没有区别的地⽅抽象出来,并单独写⼀个体现这 种模式且更通⽤的函数呢?像这样的函数需要追加⼀个新参数来接受⼀个函数,这个参数能根 据各个数组项计算得到新的整型数值: func computeIntArray(xs: [Int], transform: Int -> Int) -> [Int] { var result: [Int] = [] for x in xs { result.append(transform(x)) } return result } 现在,取决于我们想如何根据原数组得到⼀个新数组,我们可以向函数传递不同的参数。 doubleArray 函数和 incrementArray 函数都精简为了⼀⾏调⽤ computeIntArray 的语句: func doubleArray2(xs: [Int]) -> [Int] { return computeIntArray(xs) { x in x * 2 } } 代码仍然不像想象中的那么灵活。假设我们想要得到⼀个布尔型的新数组,⽤于表⽰原数组中 对应的数字是否是偶数。我们可能会尝试编写⼀些像下⾯这样的代码: func isEvenArray(xs: [Int ]) -> [Bool] { computeIntArray(xs) { x in x % 2 == 0 } } 不幸的是,这段代码导致了⼀个类型错误。问题在于,我们的 computeIntArray 函数接受⼀个 Int -> Int 类型的参数,也就是说,该参数是⼀个返回整型值的函数。⽽在 isEvenArray 函数的 定义中,我们传递了⼀个 Int -> Bool 类型的参数,于是导致了类型错误。 我们应该如何解决这个问题呢?⼀种可⾏⽅案是定义新版本的 computeIntArray 函数,接受⼀ 个 Int -> Bool 类型的函数作为参数。类似下⾯这样: func computeBoolArray(xs: [Int], transform: Int -> Bool) -> [Bool] { var result: [Bool] = [] for x in xs { result.append(transform(x)) } return result } 但是,这个⽅案的扩展性并不好。如果接下来需要计算 String 类型呢?我们是否还需要定义另 ⼀个⾼阶函数来接受 Int -> String 类型的参数? 幸运的是,该问题有⼀个解决⽅案:我们可以使⽤泛型。computeBoolArray 和 computeIntArray 的定义是相同的;唯⼀的区别在于类型签名 (type signature)。假如我们定 义⼀个相似的函数 computeStringArray 来⽀持 String 类型,其函数体将会与先前两个函数完 全⼀致。事实上,相同部分的代码可以⽤于任何类型。我们真正想做的是写⼀个能够适⽤于每 种可能类型的泛型函数: func genericComputeArray1(xs: [Int], transform: Int -> T) -> [T] { var result: [T] = [] for x in xs { result.append(transform(x)) } return result } 关于这段代码,最有意思的是它的类型签名。理解这个类型签名有助于你将 genericComputeArray 理解为⼀个函数族。类型参数 T 的每个选择都会确定⼀个新函数。 该函数接受⼀个整型数组和⼀个 Int -> T 类型的函数作为参数,并返回⼀个 [T] 类型的数组。 我们仍能进⼀步将这个函数⼀般化。没有理由让它仅能对类型为 [Int] 的输⼊数组进⾏处理。 将数组类型进⾏抽象,能得到下⾯这样的类型签名: func map(xs: [Element], transform: Element -> T) -> [T] { var result: [T] = [] for x in xs { result.append(transform(x)) } return result } 这⾥我们写了⼀个 map 函数,它在两个维度都是通⽤的:对于任何 Element 的数组和 transform: Element -> T 函数,它都会⽣成⼀个 T 的新数组。这个 map 函数甚⾄⽐我们之前 看到的 genericComputeArray 函数更通⽤。事实上,我们可以通过 map 来定义 genericComputeArray: func genericComputeArray2(xs: [Int], transform: Int -> T) -> [T] { return map(xs, transform: transform) } 同样的,上述函数的定义并没有什么太过特别之处:函数接受 xs 和 transform 两个参数之后, 将它们传递给 map 函数,然后返回结果。关于这个定义,最有意思⾮类型莫属。 genericComputeArray(_:transform:) 是 map 函数的⼀个实例,只是它有⼀个更具体的类型。 实际上,⽐起定义⼀个顶层 map 函数,按照 Swift 的惯例将 map 定义为 Array 的扩展会更合 适: extension Array { func map(transform: Element -> T) -> [T] { var result: [T] = [] for x in self { result.append(transform(x)) } return result } } 我们在函数的 transform 参数中所使⽤的 Element 类型源⾃于 Swift 的 Array 中对 Element 所进⾏的泛型定义。 作为 map(xs, transform) 的替代,我们现在可以通过 xs.map(transform) 来调⽤ Array 的 map 函数: func genericComputeArray(xs: [Int], transform: Int -> T) -> [T] { return xs.map(transform) } 想必你会很乐意听到其实并不需要⾃⼰像这样来定义 map 函数,因为它已经是 Swift 标准库的 ⼀部分了 (实际上,它基于 SequenceType 协议被定义,我们会在之后关于⽣成器和序列的章 节中提到)。本章的重点并不是说你应该⾃⼰定义 map;我们只是想要告诉你 map 的定义中并 没有什么复杂难懂的魔法 —— 你能够轻松地⾃⼰定义它! 顶层函数和扩展 你可能已经注意到,在本节的函数中我们使⽤了两种不同的⽅式来声明函数:顶层函数和类型 扩展。在⼀开始创建 map 函数的过程中,为了简单起⻅,我们选择了顶层函数的版本作为例⼦ 进⾏展⽰。不过,最终我们将 map 的泛型版本定义为 Array 的扩展,这与它在 Swift 标准库中 的实现⽅式⼗分相似。 在 Swift 标准库最初的版本中,顶层函数仍然是⽆处不在的,但伴随 Swift 2.0 的诞⽣,这种模 式被彻底地从标准库中移除了。随着协议扩展 (protocol extensions),当前第三⽅开发者有了 ⼀个强有⼒的⼯具来定义他们⾃⼰的扩展 —— 现在我们不仅仅可以在 Array 这样的具体类型 上进⾏定义,还可以在 SequenceType ⼀样的协议上来定义扩展。 我们建议遵循此规则,并把处理确定类型的函数定义为该类型的扩展。这样做的优点是⾃动补 全更完善,有歧义的命名更少,以及 (通常) 代码结构更清晰。 Filter map 函数并不是 Swift 标准数组库中唯⼀⼀个使⽤泛型的函数。我们将在后⾯的部分中介绍其 它⼏个。 假设我们有⼀个由字符串组成的数组,代表⽂件夹的内容: let exampleFiles = ["README.md","HelloWorld.swift","FlappyBird.swift"] 现在如果我们想要⼀个包含所有 .swift ⽂件的数组,可以很容易通过简单的循环得到: func getSwiftFiles(les: [String]) -> [String] { var result: [String] = [] for le in les { if le .hasSufx(".swift"){ result.append(le) } } return result } 现在可以使⽤这个函数来取得 exampleFiles 数组中的 Swift ⽂件: getSwiftFiles(exampleFiles) ["HelloWorld.swift","FlappyBird.swift"] 当然,我们可以将 getSwiftFiles 函数⼀般化。⽐如,相⽐于使⽤硬编码 (hardcoding) 的⽅式 筛选扩展名为 .swift 的⽂件,传递⼀个附加的 String 参数进⾏⽐对会是更好的⽅法。我们接下 来可以使⽤同样的函数去⽐对 .swift 或 .md ⽂件。但是假如我们想查找没有扩展名的所有⽂ 件,或者是名字以字符串 "Hello" 开头的⽂件,那该怎么办呢? 为了进⾏⼀个这样的查找,我们可以定义⼀个名为 lter 的通⽤型函数。就像之前看到的 map 那样, lter 函数接受⼀个函数作为参数。 lter 函数的类型是 Element -> Bool —— 对于数 组中的所有元素,此函数都会判定它是否应该被包含在结果中: extension Array { func lter (includeElement: Element -> Bool) -> [Element] { var result: [Element] = [] for x in self where includeElement(x) { result.append(x) } return result } } 根据 lter 能很容易地定义 getSwiftFiles: func getSwiftFiles2(les: [String]) -> [String] { return les . lter { le in le .hasSufx(".swift")} } 就像 map ⼀样,Swift 标准库中的数组类型已经有定义好的 lter 函数了。所以除⾮是作为练 习,否则并没有必要重写它。 现在你可能会问:有没有更通⽤的函数,既可以⽤来定义 map,⼜可以⽤来定义 lter ?关于 这个问题,我们将在本章的最后解答。 Reduce 在定义⼀个泛型函数来体现⼀个更常⻅的模式之前,我们会先考虑⼀些相对简单的函数。 定义⼀个计算数组中所有整型值之和的函数⾮常简单: func sum(xs: [Int]) -> Int { var result: Int = 0 for x in xs { result += x } return result } 可以像下⾯⼀样使⽤ sum 函数: sum([1, 2, 3, 4]) 10 我们也可以使⽤类似 sum 中的 for 循环来定义⼀个 product 函数,⽤于计算所有数组项相乘之 积: func product(xs: [Int ]) -> Int { var result: Int = 1 for x in xs { result = x * result } return result } 同样地,我们可能想要连接数组中的所有字符串: func concatenate(xs: [String]) -> String { var result: String = "" for x in xs { result += x } return result } 或者说,我们可以选择连接数组中的所有字符串,并插⼊⼀个单独的⾸⾏,以及在每⼀项后⾯ 追加⼀个换⾏符: func prettyPrintArray(xs: [String]) -> String { var result: String = "Entries in the array xs:\n" for x in xs { result = "" + result + x + "\n" } return result } 这些函数有什么共同点呢?它们都将变量 result 初始化为某个值。随后对输⼊数组 xs 的每⼀项 进⾏遍历,最后以某种⽅式更新结果。为了定义⼀个可以体现所需类型的泛型函数,我们需要 对两份信息进⾏抽象:赋给 result 变量的初始值,和⽤于在每⼀次循环中更新 result 的函数。 考虑到这⼀点,我们得出了能够匹配此模式的 reduce 函数定义,如下所⽰: extension Array { func reduce(initial: T, combine: (T, Element) -> T) -> T { var result = initial for x in self { result = combine(result, x) } return result } } 这个函数的泛型体现在两个⽅⾯:对于任意 [Element] 类型的输⼊数组来说,它会计算⼀个类 型为 T 的返回值。这么做的前提是,⾸先需要⼀个 T 类型的初始值 (赋给 result 变量),以及⼀ 个⽤于更新 for 循环中变量值的函数 combine: (T, Element) -> T。在⼀些像 OCaml 和 Haskell ⼀样的函数式语⾔中,reduce 函数被称为 fold 或 fold_left 我们可以⽤ reduce 来定义迄今为⽌本章出现的所有函数。下⾯是⼏个例⼦: func sumUsingReduce(xs: [Int]) -> Int { return xs.reduce(0) { result, x in result + x } } 除了写⼀个闭包,我们也可以将运算符作为最后⼀个参数。这使得代码更短,如下⾯两个函数 所⽰: func productUsingReduce(xs: [Int]) -> Int { return xs.reduce(1, combine: *) } func concatUsingReduce(xs: [String]) -> String { return xs.reduce("", combine: +) } 要再⼀次说明,我们⾃定义 reduce 仅仅只是为了练习。Swift 的标准库已经为数组提供了 reduce 函数。 我们可以使⽤ reduce 来定义新的泛型函数。例如,假设有⼀个数组,它的每⼀项都是数组,⽽ 我们想将它展开为⼀个单⼀数组。可以使⽤ for 循环编写⼀个函数: func atten(xss: [[ T ]]) -> [T] { var result: [T] = [] for xs in xss { result += xs } return result } 然⽽,若使⽤ reduce 则可以像下⾯这样编写这个函数: func attenUsingReduce(xss: [[T]]) -> [T] { return xss.reduce([]) { result, xs in result + xs } } 实际上,我们甚⾄可以使⽤ reduce 重新定义 map 和 lter : extension Array { func mapUsingReduce(transform: Element -> T) -> [T] { return reduce([]) { result, x in return result + [transform(x)] } } func lterUsingReduce(includeElement: Element -> Bool) -> [Element] { return reduce([]) { result, x in return includeElement(x) ? result + [x] : result } } } 我们能够使⽤ reduce 来表⽰所有这些函数,这个事实说明了 reduce 能够通过通⽤的⽅法来体 现⼀个相当常⻅的编程模式:遍历数组并计算结果。 请务必注意:尽管通过 reduce 来定义⼀切是个很有趣的练习,但是在实践中这往往不是⼀个 什么好主意。原因在于,不出意外的话你的代码最终会在运⾏期间⼤量复制⽣成的数组,换句 话说,它不得不反复分配内存,释放内存,以及复制⼤量内存中的内容。像我们之前做的⼀样, ⽤⼀个可变结果数组定义 map 的效率显然会更⾼。理论上,编译器可以优化代码,使其速度与 可变结果数组的版本⼀样快,但是 Swift 2.0 并没有那么做。如果想了解更多详情,请参阅我们 的另⼀本书 —— 《Swift 进阶》。 实际运用 渐渐进⼊了本章的尾声,我们将给出⼀个使⽤了 map、 lter 和 reduce 的⼩实例。 假设我们有下⾯这样的结构体,其定义由城市的名字和⼈⼝ (单位为千居⺠) 组成: struct City { let name: String let population: Int } 我们可以定义⼀些⽰例城市: let paris = City(name: "Paris", population: 2241) let madrid = City(name: "Madrid", population: 3165) let amsterdam = City(name: "Amsterdam", population: 827) let berlin = City(name: "Berlin", population: 3562) let cities = [paris, madrid, amsterdam, berlin] 假设我们现在想筛选出居⺠数量⾄少⼀百万的城市,并打印⼀份这些城市的名字及总⼈⼝数的 列表。我们可以定义⼀个辅助函数来换算居⺠数量: extension City { func cityByScalingPopulation() -> City { return City(name: name, population: population * 1000) } } 现在我们可以使⽤所有本章中⻅到的函数来编写下⾯的语句: cities . lter { $0.population > 1000 } .map { $0.cityByScalingPopulation() } .reduce("City: Population") { result, c in return result + "\n" + "\(c.name): \(c.population)" } City: Population Paris: 2241000 Madrid: 3165000 Berlin: 3562000 我们⾸先将居⺠数量少于⼀百万的城市过滤掉。然后将剩下的结果通过 cityByScalingPopulation 函数进⾏ map 操作。最后,使⽤ reduce 函数来构建⼀个包含城市 名字和⼈⼝数量列表的 String。这⾥我们使⽤了 Swift 标准库中 Array 类型的 map、 lter 和 reduce 定义。于是,我们可以顺利地链式使⽤过滤和映射的结果。表达式 cities . lter (..) 的 结果是⼀个数组,对其调⽤ map;然后这个返回值调⽤ reduce 即可得到最终结果。 泛型和 Any 类型 除了泛型,Swift 还⽀持 Any 类型,它能代表任何类型的值。从表⾯上看,这好像和泛型极其 相似。Any 类型和泛型两者都能⽤于定义接受两个不同类型参数的函数。然⽽,理解两者之间 的区别⾄关重要:泛型可以⽤于定义灵活的函数,类型检查仍然由编译器负责;⽽ Any 类型则 可以避开 Swift 的类型系统 (所以应该尽可能避免使⽤)。 让我们考虑⼀个最简单的例⼦,构想⼀个函数,除了返回它的参数,其它什么也不做。如果使 ⽤泛型,我们可能写为下⾯这样: func noOp(x: T) -> T { return x } ⽽使⽤ Any 类型,则可能写为这样: func noOpAny(x: Any) -> Any { return x } noOp 和 noOpAny 两者都将接受任意参数。关键的区别在于我们所知道的返回值。在 noOp 的 定义中,我们可以清楚地看到返回值和输⼊值完全⼀样。⽽ noOpAny 的例⼦则不太⼀样,返回 值是任意类型 — 甚⾄可以是和原来的输⼊值不同的类型。我们可以给出⼀个 noOpAny 的错误 定义,如下所⽰: func noOpAnyWrong(x: Any) -> Any { return 0 } 使⽤ Any 类型可以避开 Swift 的类型系统。然⽽,尝试将使⽤泛型定义的 noOp 函数返回值设 为 0 将会导致类型错误。此外,任何调⽤ noOpAny 的函数都不知道返回值会被转换为何种类 型。⽽结果就是可能导致各种各样的运⾏时错误。 最后不得不说,泛型函数的类型真的是⼗分丰富。不妨考虑⼀下我们在上⼀章节封装 Core Image 中定义的函数组合运算符 >>> 的泛型版本: inx operator >>> { associativity left } func >>> (f: A -> B, g: B -> C) -> A -> C { return { x in g(f(x)) } } 这个函数的类型极具通⽤性,以⾄于它完全决定了函数⾃⾝被定义的形式。这⾥我们会尝试对 此进⾏⼀些不太正式的说明和讨论。 我们需要得到的是⼀个 A -> C 类型的函数。由于我们并不知道其它任何有关 C 的信息,所以暂 时没有能够返回的值。如果知道 C 是像 Int 或者 Bool 这样的具体类型的话,我们就可以返回⼀ 个该类型的值,例如分别返回 5 或 True。然⽽函数必须能处理任意类型的 C,所以我们不能轻 率地返回具体值。在 >>> 运算符的参数中,只有 g: B -> C 函数提及了类型 C。因此,将 B 类型 的值传递给函数 g 是我们能够触及类型 C 的唯⼀途径。 同样,要得到⼀个 B 类型值的唯⼀⽅法是将类型为 A 的值传递给 f。类型为 A 的值唯⼀出现的 地⽅是在我们运算符要求返回的函数的输⼊参数⾥。因此,函数组合的定义只有这唯⼀⼀种可 能,才能满⾜所要求的泛型类型。 我们可以⽤相同的⽅式定义⼀个泛型函数,该函数能够将任意的接受两个元素的元组作为输⼊ 的函数进⾏柯⾥化 (curry) 处理,从⽽⽣成相应的柯⾥化版本: func curry(f: (A, B) -> C) -> A -> B -> C { return { x in { y in f(x, y) } } } 我们不再需要像我们在上⼀章中做过的那样,对同样的函数定义柯⾥化与未柯⾥化两个不同的 版本。换句话说,像 curry ⼀样的泛型函数可以被⽤于函数变换 —— 由未柯⾥化版本变换为柯 ⾥化版本。同样地,这个函数的类型⾮常通⽤,它 (⼏乎) 给出了⼀个完整的函数设计:确实只 有⼀种合理的实现⽅式。 使⽤泛型允许你⽆需牺牲类型安全就能够在编译器的帮助下写出灵活的函数;如果使⽤ Any 类 型,那你就真的就孤⽴⽆援了。 注释 泛型的历史可以追溯到 Strachey (2000), Girard 的系统 F(1972) 和 Reynolds (1974)。务必注 意,这些作者将泛型称为 (参数) 多态 (polymorphism),该术语仍然被⽤在很多函数式语⾔中。 ⽽许多⾯向对象语⾔⽤多态这个术语指代⼦类型引起的隐式类型转换,因此泛型这个词的引⼊ 是为了消除两个概念之间的歧义。 在前⾯我们进⾏了⼀些⾮正式的讨论,说明了为什么这样的泛型类型只有⼀种可能的对应函数: (f: A -> B, g: B -> C) -> A -> C 这其实可以⽤数学⽅法来进⾏更精确的解释。Reynolds (1983) ⾸先完成了这项⼯作,⽽后来 Wadler (1989) 将其称为⾃然推断 (Theorems for free!) —— 它强调你可以通过泛型函数的类 型来推断出函数的内容。 可选值 5 Swift 的可选类型可以⽤来表⽰可能缺失或是计算失败的值。本章将介绍如何有效地利⽤可选 类型,以及它们在函数式编程范式中的使⽤⽅式。 案例研究:字典 除了数组,Swift 对字典也有特别的⽀持。字典是键值对的集合,它提供了⼀个有效的⽅法来查 询与某个键关联的值。创建字典的语法与创建数组类似: let cities = ["Paris": 2241, "Madrid": 3165, "Amsterdam": 827, "Berlin": 3562] 上述字典存储了⼏个欧洲城市的⼈⼝数量。在这个例⼦中,键 "Paris" 与值 2241 相关联;也就 是说,Paris 的居⺠数量约为 2,241,000。 和数组⼀样,Dictionary ⽀持泛型。字典类型接受两种类型参数,分别对应所存储的键和值。在 我们的例⼦中,城市字典的类型是 Dictionary。还有⼀种简写形式,[String: Int]。 我们可以使⽤与数组索引相同的形式来查询与键相关联的值: let madridPopulation: Int = cities["Madrid"] 然⽽,这个例⼦⽆法通过类型检查。问题是 "Madrid" 键可能并不存在于 cities 字典⾥ —— 当 不存在时应该返回什么值呢?我们⽆法保证字典的查询操作总是为每个键返回⼀个 Int 值。 Swift 的可选类型可以表达这种失败的可能性。编写这个例⼦的正确⽅式应该如下所⽰: let madridPopulation: Int? = cities["Madrid"] 例⼦中 madridPopulation 的类型是可选类型 Int?,⽽⾮ Int。⼀个 Int? 类型的值是 Int 或者特 殊的 “缺失” 值 nil。 我们可以检验查询是否成功: if madridPopulation != nil { print("The population of Madrid is \(madridPopulation!* 1000)") } else { print("Unknown city: Madrid") } 如果 madridPopulation 不是 nil,第⼀个分⽀就会被执⾏。我们写 madridPopulation! 是为了 获取可选值中实际的 Int 值。后缀运算符 ! 强制将⼀个可选类型转换为⼀个不可选类型。为了 计算 Madrid 的总⼈⼝数,我们强制将可选的 madridPopulation 转换为 Int 并与 1000 相乘。 Swift 有⼀个特殊的可选绑定 (optional binding) 机制,可以让你避免写 ! 后缀。我们可以将 madridPopulation 的定义和上⾯的检验语句相结合,使它成为⼀个新语句: if let madridPopulation = cities["Madrid"]{ print("The population of Madrid is \(madridPopulation * 1000)") } else { print("Unknown city: Madrid") } 如果查询 cities ["Madrid"] 是成功的,我们便可以在分⽀中使⽤ Int 类型的变量 madridPopulation。值得注意的是,我们不再需要显式地使⽤强制解包 (forced unwrapping) 运算符。 如果可以选择,我们建议使⽤可选绑定⽽⾮强制解包。如果你有⼀个 nil 值,强制解包可能导 致崩溃;可选绑定⿎励你显式地处理异常情况,从⽽避免运⾏时错误。可选类型未经检验进⾏ 的强制解包,或者 Swift 的隐式解包可选值,都是很糟的代码异味,它们预⽰着可能发⽣运⾏ 时错误。 Swift 还给 ! 运算符提供了⼀个更安全的替代,?? 运算符。使⽤这个运算符时,需要额外提供 ⼀个默认值,当运算符被运⽤于 nil 时,这个默认值将被作为返回值。简单来说,它可以定义 为下⾯这样: inx operator ?? func ??(optional: T?, defaultValue: T) -> T { if let x = optional { return x } else { return defaultValue } } ?? 运算符会检验它的可选参数是否为 nil。如果是,返回 defaultValue 参数;否则,返回可选 值中实际的值。 上⾯的定义有⼀个问题:如果 default 的值是通过某个函数或者表达式得到的,那么⽆论可选 值是否为 nil,defaultValue 都会被求值。通常我们并不希望这种情况发⽣:⼀个 if-then-else 语句应该根据各分⽀关联的值是否为真,只执⾏其中⼀个分⽀。同样的道理,?? 运算符应该只 在可选值参数是 nil 时才对 defaultValue 参数进⾏求值。举个例⼦,假设我们像下⾯这样调⽤ ??: optional ?? defaultValue 在这个例⼦中,如果 optional 变量是⾮ nil 的话,我们真的不愿意对 defaultValue 进⾏求值 —— 因为这可能是⼀个开销⾮常⼤的计算,只有绝对必要时我们才会想运⾏这段代码。可以按 如下⽅式解决这个问题: func ??(optional: T?, defaultValue: () -> T) -> T { if let x = optional { return x } else { return defaultValue() } } 作为 T 类型的替代,我们提供⼀个 () -> T 类型的默认值。现在 defaultValue 闭包中的代码仅 当我们对它进⾏调⽤时才会执⾏。在这样的定义下,代码会如预期⼀样,只在 else 分⽀中被执 ⾏。美中不⾜的是,当调⽤ ?? 运算符时需要为默认值创建⼀个显式闭包。例如,我们需要编写 以下代码: myOptional ?? { myDefaultValue } Swift 标准库中的定义通过使⽤ Swift 的 autoclosure 类型标签来避开创建显式闭包的需求。 它会在所需要的闭包中隐式地将参数封装到 ?? 运算符。这样⼀来,我们能够提供与最初相同的 接⼝,但是⽤⼾⽆需再显式地创建闭包封装 defaultValue 参数。Swift 标准库中使⽤的实际定 义如下: inx operator ?? { associativity right precedence 110 } func ??(optional: T?, @autoclosure defaultValue: () -> T) -> T { if let x = optional { return x } else { return defaultValue() } } ?? 运算符提供了⼀个相较于强制可选解包更安全的替代,并且不像可选绑定⼀样繁琐。 玩转可选值 Swift 的可选值可以使失败的情况直截了当。虽然这在有些场合,特别是当多个可选结果组合 在⼀起的时候,写起来会有些⿇烦。但事实上,有很多技术能让可选值更易⽤。 可选值链 ⾸先,Swift 有⼀个特殊的机制,可选链,它⽤于在被嵌套的类或结构体中对⽅法或属性进⾏选 择。考虑下⾯处理客⼾订单的模型的代码⽚段: struct Order { let orderNumber: Int let person: Person? } struct Person { let name: String let address: Address? } struct Address { let streetName: String let city: String let state: String? } 给定⼀个 Order,如何才能知道客⼾地址中的 state 为何值呢?我们可以使⽤显式解包运算符: order.person!.address!.state! 然⽽,如果任意中间数据缺失,这么做可能会导致运⾏时异常。使⽤可选绑定相对更安全: if let myPerson = order.person { if let myAddress = myPerson.address { if let myState = myAddress.state { // ... 但这未免有些烦琐。若使⽤可选链,这个例⼦将会变成: if let myState = order.person?.address?.state { print("This order will be shipped to \(myState)") } else { print("Unknown person, address, or state.") } 我们使⽤问号运算符来尝试对可选类型进⾏解包,⽽不是强制将它们解包。当任意⼀个组成项 失败时,整条语句链将返回 nil。 分支上的可选值 上⾯我们已经讨论了 if let 可选绑定机制,但是 Swift 还有其他两种分⽀语句,switch 和 guard,它们也⾮常适合与可选值搭配使⽤。 为了在⼀个 switch 语句中匹配可选值,我们简单地为 case 分⽀中的每个模式添加⼀个 ? 后 缀。如果我们对⼀个特定值没有兴趣,也可以直接匹配 Optional 的 None 值或 Some 值: switch madridPopulation { case 0?: print("Nobody in Madrid") case (1..<1000)?: print("Less than a million in Madrid") case .Some(let x): print("\(x) people in Madrid") case .None: print("We don't know about Madrid") } guard 语句的设计旨在当⼀些条件不满⾜时,可以尽早退出当前作⽤域。没有值存在时就提早 退出,是⼀个很常⻅的使⽤情境。将它和可选绑定组合在⼀起可以很好地处理 None 的情况。 很显然,guard 语句后⾯的任何代码都需要值存在才会被执⾏。举个例⼦,我们可以重写打印 给定城市居⺠数量的代码,如下所⽰: func populationDescriptionForCity(city: String) -> String? { guard let population = cities[city] else { return nil } return "The population of Madrid is \(population * 1000)" } populationDescriptionForCity("Madrid") /** 结果为: Optional("The population of Madrid is 3165000") */ 在 guard 语句后⾯,我们有⾮可选的 population 值可以使⽤。以这种⽅式使⽤ guard 语句, 会让控制流⽐嵌套 if let 语句时更简单。 可选映射 ? 运算符允许我们选择性地访问可选值的⽅法或字段。然⽽,在很多其它例⼦中,若可选值存 在,你可能会想操作它,否则返回 nil。参考下⾯的例⼦: func incrementOptional(optional: Int?) -> Int? { guard let x = optional else { return nil } return x + 1 } 例⼦ incrementOptional 的⾏为与 ? 运算符相似:如果可选值是 nil,则结果也是 nil;不然就 执⾏⼀些计算。 我们可以将 incrementOptional 函数和 ? 运算符⼀般化,然后为可选值定义⼀个 map 函数。这 样⼀来,我们不仅能像 incrementOptional 那样,对⼀个 Int? 类型的值做增量运算,还可以将 想要执⾏的任何运算作为参数传递给 map 函数: extension Optional { func map(transform: Wrapped -> U) -> U? { guard let x = self else { return nil } return transform(x) } } map 函数接受⼀个类型为 Wrapped -> U 的 transform 函数作为参数。如果可选值不是 nil, map 将会将其作为参数来调⽤ transform,并返回结果;否则 map 函数将返回 nil。这个 map 函数是 Swift 标准库的⼀部分。 我们使⽤ map 来重写 incrementOptional,如下所⽰: func incrementOptional2(optional: Int?) -> Int? { return optional.map { $0 + 1 } } 当然,我们也可以使⽤ map 来访问和操作可选值结构体或者类中的字段或⽅法,就像我们使⽤ ? 运算符时所做的那样。 为什么将这个函数命名为 map?它和运⽤于数组的 map 运算有什么共同点吗?我们有充分的 理由将这两个函数都称为 map,但是现在我们暂时不会展开,之后在关于函⼦、适⽤函⼦与单 ⼦的章节中会再次讨论这个问题。 再谈可选绑定 map 函数展⽰了⼀种操作可选值的⽅法,但是还有很多其它⽅法存在。参考下⾯的例⼦: let x: Int? = 3 let y: Int? = nil let z: Int? = x + y 这段程序并不被 Swift 编译器接受,你能定位到错误吗? 这⾥的问题是加法运算只⽀持 Int 值,⽽不⽀持我们这⾥的 Int? 值。要解决这个问题,我们可 以像下⾯这样引⼊ if 嵌套语句: func addOptionals(optionalX: Int?, optionalY: Int?) -> Int? { if let x = optionalX { if let y = optionalY { return x + y } } return nil } 不过,除了层层嵌套,我们还可以同时绑定多个可选: func addOptionals(optionalX: Int?, optionalY: Int?) -> Int? { if let x = optionalX, y = optionalY { return x + y } return nil } 若还想更简短,可以使⽤⼀个 guard 语句,当值缺失时提前退出: func addOptionals(optionalX: Int?, optionalY: Int?) -> Int? { guard let x = optionalX, y = optionalY else { return nil } return x + y } 这个例⼦可能看起来很⽣硬,不过操作可选值确实是常常发⽣的事。假设我们定义了下⾯这个 字典,国家与其⾸都相关联: let capitals = [ "France":"Paris", "Spain":"Madrid", "The Netherlands":"Amsterdam", "Belgium":"Brussels" ] 为了编写⼀个能返回给定国家⾸都⼈⼝数量的函数,我们将 capitals 字典与之前定义的的 cities 字典结合。对于每⼀次字典查询,我们必须确保它返回⼀个结果: func populationOfCapital(country: String) -> Int? { guard let capital = capitals[country], population = cities[capital] else { return nil } return population * 1000 } 可选链和 if let (或 guard let) 都是语⾔中让可选值能够更易于使⽤的特殊构造。不过,Swift 还提供了另⼀条途径来解决上述问题:那就是借⼒于标准库中的 atMap 函数。多种类型中都 定义了atMap 函数,在可选值类型的情况下,它的定义是这样的: extension Optional { func atMap(f: Wrapped -> U?) -> U? { guard let x = self else { return nil } return f(x) } } atMap 函数检查⼀个可选值是否为 nil。若不是,我们将其传递给参数函数 f;若是 nil,那么 结果也将是 nil。 现在可以使⽤此函数,来重写我们的例⼦: func addOptionals2(optionalX: Int?, optionalY: Int?) -> Int? { return optionalX.atMap { x in optionalY.atMap { y in return x + y } } } func populationOfCapital2(country: String) -> Int? { return capitals[country].atMap { capital in cities [capital].atMap { population in return population * 1000 } } } 当前我们通过嵌套的⽅式调⽤ atMap,取⽽代之,也可以通过链式调⽤来重写 populationOfCapital2,这样使代码结构更浅显易懂: func populationOfCapital3(country: String) -> Int? { return capitals[country].atMap { capital in return cities [capital] }.atMap { population in return population * 1000 } } 我们并不想⿎吹 atMap 是组合可选值的 “正确” ⽅法。恰恰相反,我们希望说明的是 Swift 编 译器内置的可选绑定并不神奇,它只是⼀种你能够使⽤⾼阶函数⾃⼰实现的控制结构。 为什么使用可选值? 引⼊⼀个显式可选类型的意义是什么呢?对于习惯了 Objective-C 的程序员来说,最初使⽤可 选类型也许会觉得奇怪。Swift 的类型系统相当严格:⼀旦我们有可选类型,就必须处理它可能 是 nil 的问题。于是不得不编写像 map ⼀样的新函数来操作可选值。在 Objective-C 中,这⼀ 切更灵活。举个例⼦来说,将上⾯的例⼦翻译为下⾯的 Objective-C 代码,将不会有任何编译 错误: - (int)populationOfCapital:(NSString *)country { return [self. cities [self.capitals[country]] intValue] * 1000; } 我们可以将 nil 作为⼀个国家的名字传递给函数,然后得到⼀个结果 0。⼀切都很顺利。在许多 语⾔中并没有可选,空指针是危险的来源。Objective-C 稍好⼀些,你可以安全地向 nil 发送消 息,然后根据不同的返回值的类型,得到像是 nil、数字 0 这样的零值。为什么在 Swift 中要改 变这种特性呢? 选择显式的可选类型更符合 Swift 增强静态安全的特性。强⼤的类型系统能在代码执⾏前捕获 到错误,⽽且显式可选类型有助于避免由缺失值导致的意外崩溃。 Objective-C 采⽤的默认近似零的做法有其弊端,可能你会想要区分失败 (键不存在于字典) 和 成功返回 nil (键存在于字典,但关联值是 nil) 两种情况。若要在 Objective-C 中做到这⼀点, 你只能使⽤ NSNull。 虽然在 Objective-C 中将消息发送给 nil 是安全的,但是使⽤它们往往并不安全。 ⽐⽅说我们想创建⼀个属性字符串 (attributed string)。如果我们传递 nil 作为 country 的值, 那么 capital 也会是 nil,但是当我们试图将 NSAttributedString 初始化为 nil 时,将会引起崩 溃: - (NSAttributedString *)attributedCapital:(NSString *)country { NSString *capital = self.capitals[country]; NSDictionary *attr = @{ /* ... */ }; return [[ NSAttributedString alloc] initWithString:capital attributes:attr ]; } 虽然像上⾯那样的崩溃并不经常发⽣,不过⼏乎每个开发者都写过像这样导致崩溃的代码。⼤ 多数时候,在调试阶段这些这些崩溃的导⽕索就会被发现,不过偶尔难免不知不觉就发布了代 码,⼀些情况下,变量还可能出乎意料的是 nil。因此,许多程序员使⽤断⾔来显式地标⽰这这 种情况。例如,我们可以添加⼀个 NSParameterAssert 以确保当 country 是 nil 时就⽴即崩溃: - (NSAttributedString *)attributedCapital:(NSString *)country { NSParameterAssert(country); NSString *capital = self.capitals[country]; NSDictionary *attr = @{ /* ... */ }; return [[ NSAttributedString alloc] initWithString:capital attributes:attr ]; } 如果我们传递了⼀个 country 值,但是 self.capitals 中并没有键与之匹配该怎么办呢?这种情 况发⽣的概率很⼤,特别是当 country 来源于⽤⼾输⼊时。在这种情况下,capital 将会是 nil, 我们的代码仍然会崩溃。当然,修复的⽅法很简单。不过,我们关注的重点是,在 Swift 中使⽤ nil 编写健壮的代码⽐在 Objective-C 中容易。 最后,从本质上看,使⽤断⾔是⾮模块化的。假设我们要实现⼀个 checkCountry ⽅法⽤于确 认是否⽀持⾮空 NSString *。可以很容易地在上述代码中加⼊该⽅法: - (NSAttributedString *)attributedCapital:(NSString*)country { NSParameterAssert(country); if (checkCountry(country)) { // ... } } 现在的问题是:checkCountry 函数是否也应该断⾔它的参数不是 nil?⼀⽅⾯,它不应该 —— 因为我们⽅才刚在 attributedCapital ⽅法中执⾏了检验的代码。另⼀⽅⾯,如果 checkCountry 函数仅适⽤于⾮ nil 值,我们还是应该复制上述断⾔。我们被迫在暴露不安全接 ⼝和复制断⾔之间进⾏选择。还有⼀种做法是,可以给签名添加⼀个 nonnull 标注,当该⽅法 被⼀个可能为 nil 的值调⽤时,它会发出警告,但这种做法在⼤多数 Objective-C 的代码库中 并不常⻅。 在 Swift 中,事情要来得容易得多:函数签名可以显式地使⽤可选值来提⽰⼀个值可能为 nil。 与他⼈协同编码时,这是⼗分宝贵的信息。下⾯的签名提供了⼤量信息: func attributedCapital(country: String) -> NSAttributedString? 我们不仅被警告有失败的可能性,还知道了必须传递⼀个 String 作为参数 —— 且不能是 nil 值。像上⾯⼀样的崩溃将不会再发⽣。此外,这也是编译器要检验的信息。⽂档很容易过时, 但是你可以永远依赖函数签名。 在 Objective-C 中处理标量值时,可选问题显得更加棘⼿。不妨看看下⾯的⽰例,尝试在⼀个 字符串中查找⼀个特定关键词的位置: NSString *someString = ...; if ([ someString rangeOfString:@"swift"].location != NSNotFound) { NSLog(@"Someone mentioned swift!"); } 看起来毫⽆问题:如果 rangeOfString: 没有找到字符串,location 就会被设置为 NSNotFound。NSNotFound 被定义为 NSIntegerMax。这段代码⼏乎是正确的,第⼀眼很难 看到它的问题:若 someString 是 nil,rangeOfString: 将返回⼀个属性全为零的结构体, location 将返回 0。接着所做的判断结果若为真,if 语句中的代码将被执⾏。 如果有可选值的话,这⼀切将免于发⽣。如果我们想将这份代码转为 Swift,会需要做出⼀些结 构性的改变。上⾯的代码会被编译器拒绝,类型系统也不会允许你在⼀个 nil 值上运⾏ rangeOfString:。因此,⾸先需要将它解包: if let someString = ... { if someString.rangeOfString("swift").location != NSNotFound { print("Found") } } 类型系统将有助于你捕捉难以察觉的细微错误。其中⼀些错误很容易在开发过程中被发现,但 是其余的可能会⼀直留存到⽣产代码中去。坚持使⽤可选值能够从根本上杜绝这类错误。 案例研究: QuickCheck 6 近些年来,在 Objective-C 中,测试变得越来越普遍。现在有许多流⾏的库通过持续集成⼯具 来进⾏⾃动化测试。XCTest 是⽤来写单元测试的标准框架。此外,很多第三⽅框架 (例如 Specta、Kiwi 和 FBSnapshotTestCase) 也已经可供使⽤,同时现阶段还有若⼲ Swift 的新框 架正在开发中。 所有的这些框架都遵循⼀个相似的模式:测试通常由⼀些代码⽚段和预期结果组成。执⾏代码 之后,将它的结果与测试中定义的预期结果相⽐较。不同的库测试的层次会有所不同 —— 有 的测试独⽴的⽅法,有的测试类,还有⼀些执⾏集成测试 (运⾏整个应⽤)。在本章中,我们将 通过迭代的⽅法,⼀步⼀步完善并最终构建⼀个针对 Swift 函数进⾏ “特性测试 (property-based testing)” 的⼩型库。 在写单元测试的时候,输⼊数据是程序员定义的静态数据。⽐⽅说,在对⼀个加法进⾏单元测 试时,我们可能会写⼀个验证 1 + 1 等于 2 的测试。如果加法的实现⽅式破坏了这个特性,测 试就会失败。不过,为了更⼀般化,我们可以选择测试加法的交换律 —— 换句话说,就是验证 a + b 等于 b + a。为了进⾏这项测试,我们可以写⼀个测试⽤例来验证 42 + 7 等于 7 + 42。 QuickCheck (Claessen and Hughes 2000) 是⼀个⽤于随机测试的 Haskell 库。相较于独⽴的 单元测试中每个部分都依赖特定输⼊来测试函数是否正确,QuickCheck 允许你描述函数的抽 象特性并⽣成测试来验证这些特性。当⼀个特性通过了测试,就没有必要再证明它的正确性。 更确切地说,QuickCheck 旨在找到证明特性错误的临界条件。在本章中,我们将⽤ Swift (部 分地) 移植 QuickCheck 库。 这⾥举例说明会⽐较好。假设我们想要验证加法是⼀个满⾜交换律的运算。于是,⾸先为两个 整型数值 x 和 y 写了⼀个函数,来检验 x + y 与 y + x 是否相等: func plusIsCommutative(x: Int, y: Int) -> Bool { return x + y == y + x } ⽤ QuickCheck 检验这条语句就像调⽤ check 函数⼀样简单: check("Plus should be commutative", plusIsCommutative) /** 打印: "Plus should be commutative" passed 10 tests. */ check 函数⼀遍⼜⼀遍地调⽤ plusIsCommutative 函数且每次传递两个随机整型值作为参数, 以此来完成上述检验。如果语句不为真,它将会打印出导致测试失败的输⼊值。这⾥的关键是, 我们可以⽤返回 Bool 的函数 (如 plusIsCommutative) 来描述代码的抽象特性 (如交换律)。现 在,check 函数使⽤这个特性来⽣成单元测试,这⽐起你⾃⼰⼿写的单元测试,代码覆盖率会 更⾼。 当然,并不是所有的测试都能通过。例如,我们可以定义⼀个语句来描述减法满⾜交换律: func minusIsCommutative(x: Int, y: Int) -> Bool { return x - y == y - x } 现在,如果我们使⽤ QuickCheck 对这个函数进⾏测试,将会得到⼀个失败的测试⽤例: check("Minus should be commutative", minusIsCommutative) /** 打印: "Minus should be commutative" doesn't hold:(3, 2) */ 使⽤ Swift 的尾随闭包 (trailing closures) 语法,我们也可以直接编写测试,⽽⽆需单独定义 (像 plusIsCommutative 或 minusIsCommutative 这样的) 特性: check("Additive identity") { (x: Int) in x + 0 == x } /** 打印: "Additive identity" passed 10 tests. */ 当然,我们还能测试很多其它类似的标准算术特性。接下来,我们即将介绍更多有趣的测试和 特性。不过在此之前,⾸先要给出⼀些关于如何实现 QuickCheck 的细节。 构建 QuickCheck 为了构建 Swift 版本的 QuickCheck,我们需要做⼏件事情: → ⾸先,我们需要⼀个⽅法来⽣成不同类型的随机数。 → 有了随机数⽣成器之后,我们需要实现 check 函数,然后将随机数传递给它的特性参数。 → 如果⼀个测试失败了,我们会希望测试的输⼊值尽可能⼩。⽐⽅说,如果我们在对⼀个 有 100 个元素的数组进⾏测试时失败了,我们会尝试让数组元素更少⼀些,然后看⼀看 测试是否依然失败。 → 最后,我们还需要做⼀些额外的⼯作以确保检验函数适⽤于带有泛型的类型。 生成随机数 ⾸先,让我们定义⼀个可以表达如何⽣成随机数的协议。这个协议只包含⼀个函数 —— 返回 Self 类型值的 arbitrary,返回的 Self 也就是实现了 Arbitrary 协议的这个类或结构体的实例: protocol Arbitrary { static func arbitrary() -> Self } 然后,让我们来写⼀个 Int 的例⼦。使⽤标准库中的 arc4random 函数并将其返回值转换为 Int。注意,这⾥只能⽣成正整数。事实上,⼀个完整实现的库也应能够⽣成负整数,不过本章 中我们会尽可能让事情简单⼀些: extension Int: Arbitrary { static func arbitrary() -> Int { return Int(arc4random()) } } 现在我们可以像下⾯这样⽣成随机整数: Int.arbitrary () /** 结果为: 994416812 */ 如果要⽣成随机字符串,还需要再多做⼀点点⼯作。⾸先是⽣成随机字符: extension Character: Arbitrary { static func arbitrary() -> Character { return Character(UnicodeScalar(Int.random(from: 65, to: 90))) } } 然后,我们使⽤下⾯定义的 random 函数,随机⽣成⼀个介于 0 到 40 之间的数 x 作为字符串 ⻓度。接着再⽣成 x 个随机字符,并将它们组合为⼀个字符串。注意,⽬前我们只随机⽣成⼤ 写字⺟。在实际的⽣产库中,应该⽣成包含任意字符且更⻓的字符串: func tabulate(times: Int, transform: Int -> A) -> [A] { return (0..< times).map(transform) } extension Int { static func random(from from: Int, to: Int) -> Int { return from + (Int(arc4random()) % (to - from)) } } extension String: Arbitrary { static func arbitrary() -> String { let randomLength = Int.random(from: 0, to: 40) let randomCharacters = tabulate(randomLength) { _ in Character.arbitrary() } return String(randomCharacters) } } 我们通过调⽤ tabulate 函数,对 0 到 times-1 的数字使⽤ map 函数,⽣成⼀个由 f(0), f(1), …, f(times-1) 组成的数组。String 的扩展 arbitrary 使⽤了 tabulate 函数来填充⼀个随机字符 数组。 如同我们⽣成随机 Int 类型值时所做的,我们可以⽤同样的⽅法调⽤ arbitrary 函数,唯⼀不同 的是我们在 String 类上调⽤它: String.arbitrary () /** 结果为: WTGYFDTDCMQCLSKPJULMLHTVVMEUVWMMG */ 实现 check 函数 现在,我们已经准备就绪,即将开始实现检验函数的第⼀个版本。check1 函数包含⼀个简单循 环,每次迭代时为待检验特性⽣成随机的输⼊值,然后进⾏检验。⼀旦发现反例,就将其打印 出来,并⽴即返回。否则 check1 函数将会汇报成功通过的测试数量。(注意,这⾥我们将函数 称为 check1 是由于我们稍后将会编写其最终版。) func check1(message: String, _ property: A -> Bool) -> () { for _ in 0.. CGSize { return CGSize(width: CGFloat.arbitrary(), height: CGFloat.arbitrary()) } } check1("Area should be at least 0") { (size: CGSize) in size.area >= 0 } /** 打印: "Area should be at least 0" doesn't hold:(293.39088483094, -230.171975034795) */ 上⾯我们我们看到的例⼦充分地说明了何时 QuickCheck 会⾮常有⽤:它为我们找到了临界情 况。如果尺⼨有且只有⼀个负值,我们的 area 函数将返回⼀个负值。当被作为 CGRect 的⼀部 分来使⽤时,CGSize 可以有负值。在编写⼀般的单元测试时,这种情况很容易被发现,因为尺 ⼨通常只有正值。 缩小范围 如果我们在字符串上运⾏ check1 函数,可能会收到⼀条相当⻓的失败消息: check1("Every string starts with Hello") { (s: String) in s.hasPrex("Hello") } /** 打印: "Every string starts with Hello" doesn't hold: PMRVWLLVLBRJXMTUFUBCDBVBBE */ 理想情况下,我们希望失败的输⼊尽可能简单。通常,反例所处的范围越⼩,越容易定位到失 败是由哪⼀段代码引起的。在上例中,反例还是相当易于理解的,但是不可能总是这种情况。 想象⼀个复杂的状况,数组或字典因为不明原因失败了 —— 如果有最⼩反例,判断为什么测 试会失败将变得容易很多。原则上,⽤⼾可以尝试对失败的输⼊值进⾏缩减,并重新运⾏测试。 然⽽,为了不将这个⿇烦抛给⽤⼾,我们会将这个过程⾃动化。 ⾸先,我们将额外定义⼀个名为 Smaller 的协议,它只做⼀件事 —— 尝试缩⼩反例所处的范 围: protocol Smaller { func smaller() -> Self? } 某些情况下,如何进⼀步缩⼩测试数据的范围这件事本⾝并不是很明确。例如,没有办法缩⼩ 空数组,这种情况我们将返回 nil。 在我们的例⼦中,对于整数,我们尝试将其除以⼆,直到等于零: extension Int: Smaller { func smaller() -> Int? { return self == 0 ? nil : self / 2 } } 现在,让我们来测试⼀下上例: 100.smaller() /** 结果为: Optional(50) */ ⽽对于字符串,则是移除第⼀个字符 (除⾮该字符串为空): extension String: Smaller { func smaller() -> String? { return isEmpty ? nil : String(characters.dropFirst()) } } 为了在 check 函数中使⽤ Smaller 协议,我们需要⼀个⽅法,能够缩⼩ check 函数⽣成的任意 测试数据的范围。于是,我们将重新定义 Arbitrary 协议以扩展 Smaller 协议: protocol Arbitrary: Smaller { static func arbitrary() -> Self } 反复缩小范围 我们现在可以重新定义 check 函数,来缩⼩任意导致失败的测试数据范围。为此,我们使⽤ iterateWhile 函数,它接受⼀个条件和⼀个初始值,并且只要条件成⽴就反复调⽤⾃⾝: func iterateWhile(condition: A -> Bool, initial : A, next: A -> A?) -> A { if let x = next( initial ) where condition(x) { return iterateWhile(condition, initial : x, next: next) } return initial } 通过使⽤ iterateWhile,我们能够反复缩⼩测试中发现的反例所属的范围: func check2(message: String, _ property: A -> Bool) -> () { for _ in 0.. [Int] { if array.isEmpty { return [] } let pivot = array.removeAtIndex(0) let lesser = array. lter { $0 < pivot } let greater = array. lter { $0 >= pivot } return qsort(lesser) + [pivot] + qsort(greater) } 我们也可以试着编写⼀个特性来检验当前版本的快速排序相对于内置 sort 函数是否有区别: check2("qsort should behave like sort") { (x: [Int ]) in return qsort(x) == x.sort(<) } 然⽽,编译器警告我们 [Int] 不遵循 Arbitrary 协议。在能够实现 Arbitrary 之前,我们还需要 先实现 Smaller。⾸先,我们提供⼀个简单的函数来移除数组的第⼀项: extension Array: Smaller { func smaller() -> [Element]? { guard !isEmpty else { return nil } return Array(dropFirst()) } } 我们也可以编写⼀个函数,它能够为任何遵循 Arbitrary 协议的类型⽣成⼀个随机⻓度的数组: extension Array where Element: Arbitrary { static func arbitrary() -> [Element] { let randomLength = Int(arc4random() % 50) return tabulate(randomLength) { _ in Element.arbitrary() } } } 现在,我们想做的是让 Array 本⾝遵循 Arbitrary 协议。不过,只有数组的每⼀项都遵循 Arbitrary 协议,数组本⾝才会遵循 Arbitrary 协议。例如,为了⽣成⼀个由随机数组成的数组, 我们⾸先需要确保能够⽣成随机数。理想情况下,我们我们会像下⾯这样来表⽰数组的每⼀项 都应该遵循 Arbitrary 协议: extension Array: Arbitrary where Element: Arbitrary { static func arbitrary() -> [Element] { // ... } } 很遗憾,⽬前⽆法将这个限制表⽰为类型约束,并不可能编写⼀个让 Array 遵循 Arbitrary 协议 的扩展。因此,我们选择修改 check2 函数。 check2 函数的问题是它要求类型 A 遵循 Arbitrary 协议。我们将放弃这个需求,取⽽代之, 要求必要的函数 smaller 和 arbitrary 被作为参数传⼊。 我们⾸先定义⼀个包含两个所需函数的辅助结构体: struct ArbitraryInstance { let arbitrary: () -> T let smaller: T -> T? } 现在,我们可以写⼀个接受 ArbitraryInstance 结构体作为参数的辅助函数。checkHelper 的 定义严格参照了前⾯的 check2 函数。两者之间唯⼀的不同是 arbitrary 和 smaller 被定义的位 置。在 check2 中,它们被泛型类型 约束;⽽在 checkHelper 中,它们在 ArbitraryInstance 结构体中被显式地传递: func checkHelper(arbitraryInstance: ArbitraryInstance, _ property: A -> Bool, _ message: String) -> () { for _ in 0..(message: String, property: X -> Bool) -> () { let instance = ArbitraryInstance(arbitrary: X.arbitrary, smaller: { $0.smaller() }) checkHelper(instance, property, message) } 如果我们有⼀个类型,⽆法对它定义所需的 Arbitrary 实例,就像数组的情况⼀样,我们可以重 载 check 函数并⾃⼰构造所需的 ArbitraryInstance 结构体: func check(message: String, _ property: [X] -> Bool) -> () { let instance = ArbitraryInstance(arbitrary: Array.arbitrary, smaller: { (x: [X]) in x.smaller() }) checkHelper(instance, property, message) } 现在,我们终于可以运⾏ check 来验证我们所实现的快速排序了。⼤量的随机数组将会被⽣成 并被传递给我们的测试: check("qsort should behave like sort") { (x: [Int ]) in return qsort(x) == x.sort(<) } /** 打印: "qsort should behave like sort" passed 10 tests. */ 使用 QuickCheck 也许出乎你的意料,不过有确凿的证据表明,测试技术会影响你的代码设计。依赖测试驱动设 计的⼈们使⽤测试并不仅仅是为了验证他们的代码是否正确,他们还根据测试来指导编写测试 驱动的代码,这样⼀来,代码的设计将会变得简单。这⾮常有意义 —— 如果不需要复杂的构 建流程就能够容易地为类编写测试代码的话,说明这个类的耦合度很低。 对于 QuickCheck 来说,同样的规则也是适⽤的。通常来看,事后向现有代码添加 QuickCheck 测试并不容易,尤其是当你有⼀个⾯向对象的架构且它很⼤程度依赖于其它类或使⽤可变状态 的时候。但是,如果你从⼀开始就使⽤ QuickCheck 进⾏测试驱动开发,会发现它将给你的代 码设计带来巨⼤的影响。QuickCheck 迫使你去思考你的函数必须满⾜哪些抽象特性,并允许 你给出⼀个⾼级规范。单元测试可以断⾔ 3 + 0 等于 0 + 3;QuickCheck 特性则更泛⽤地认为 加法是可交换的运算。通过最初对⼀个⾼级 QuickCheck 规范的思考,你的代码会向着模块化 和引⽤透明 (将在下⼀章中提及) 的⽅向发展。QuickCheck 并不适⽤于有状态的函数或 APIs。 因此,从⼀开始就使⽤ QuickCheck 来编写测试代码将有助于保持代码整洁。 展望 这个库已经很有⽤了,但是距离完成还有很⼤差距。也就是说,还有很多明显的地⽅可以改进: → 缩⼩的⽅法很傻很天真。⽐⽅说,在数组的情况下,⽬前我们是移除数组的第⼀项。然 ⽽,我们完全可以选择移除其它项,或是对数组中的元素进⾏缩⼩ (两者均做也可)。当 前的实现⽅式返回⼀个可选且已经缩⼩范围的值,⽽我们可能想要⽣成⼀个由值组成的 列表。在后⾯的章节中,我们将看到如何⽣成⼀个结果的惰性列表 (lazy list),在那⾥我 们可以使⽤相同的⽅法。 → Arbitrary 实例相当简单。为了对应不同的输⼊类型,我们可能想要更多复杂的 Arbitrary 实例。例如,当⽣成随机枚举值时,我们可以基于不同的频率来⽣成某种情 况。我们也可以⽣成像是已排序的或是⾮空的数组这样的约束值。在编写多个 Arbitrary 实例的时候,可以定义⼀些辅助函数来协助我们。 → 将⽣成的测试数据进⾏分类:如果我们⽣成了⼤量⻓度为⼀的数组,可以将它们归类为 “不重要的” 测试数据。被参照的 Haskell 库本⾝是⽀持分类的,直接将这些理念移植过 来即可。 → 我们也许会希望能更好地控制⽣成的随机输⼊值的个数。在 Haskell 版本的 QuickCheck 中,Arbitrary 协议接受⼀个额外的参数,⽤于限制随机输⼊值的个数;因 此 check 函数⼀开始只测试 “⼩” 范围内的值,相当于⼩且快的测试。随着越来越多的 测试通过,check 函数会增⼤输⼊值的范围并试图找到更⼤更复杂的反例。 → 我们可能想⽤确定的种⼦来初始化随机⽣成器,以使它能够重现测试⽤例所⽣成的值。 这将会使失败的测试更容易被复现。 显然,这并不是全部;在让这个库变得完整的路上,还有很多其它⼤⼤⼩⼩的事情可以做。 不可变性的价值 7 Swift 有⼏个⽤于控制值的变化⽅式的机制。在本章中,我们将介绍这些不同的机制是如何⼯ 作的,以及如何区别值类型和引⽤类型,并证明为什么限制可变状态的使⽤是⼀个良好的理念。 变量和引用 Swift 有两种初始化变量的⽅法,分别使⽤ var 和 let 关键字: var x: Int = 1 let y: Int = 2 两者的关键差异在于,我们可以给使⽤ var 声明的变量赋新值,⽽使⽤ let 创建的变量不能被 修改: x = 3 // 没问题 y = 4 // 被编译器拒绝 使⽤ let 声明的变量被称为不可变变量;另⼀⽅⾯,使⽤ var 声明的变量则被叫做可变变量。 为什么?你可能想质问我 —— 为什么要声明⼀个不可变的变量?这么做难道不会限制变量的 能⼒吗?严格来说,⼀个可变变量⽤途更⼴泛。因为这个显⽽易⻅的理由,⽐起 let 我们更偏 爱 var。不过,在本章中,我们会尝试证明事实恰恰相反。 不妨想象⼀下,如果要阅读⼀个他⼈编写的 Swift 类。其中有⼀些⽅法全都引⽤了某个名字毫 ⽆意义的实例变量,例如 x。如果可以选择,你会使⽤ var 还是 let 来声明 x 呢?显然,将 x 声 明为不可变变量会更好:这样你可以通读代码⽽⽆需担⼼当前 x 的值是什么,你可以在 x 的定 义语句中⾃由地替换它的值,⽽不⽤担⼼给 x 赋⼀个新值时可能对类中其余部分的不可变性造 成破坏。 不可变变量不能被赋以新值。因此,很容易知道不可变变量的⾏为。Edsger Dijkstra 在他⼀篇 著名的论⽂《Go To 语句之害》(Go To Statement Considered Harmful) 中写道: 我的…观点是,以我们的智⼒,更适合掌控静态关系,⽽把随时间不断发展的过程形 象化的能⼒相对不那么发达。 Dijkstra 接着论证了在阅读结构化代码 (使⽤条件语句,循环和函数调⽤,⽽⾮ goto 语句) 时, 程序员需要具备的⼼智模型 (mental model) ⽐阅读充斥着 goto 的繁杂代码时所需的要简单。 我们应该恪守这个信条,并再进⼀步,尽可能避开对可变变量的使⽤:var 是有害的。 值类型与引用类型 不可变性并不只存在于变量声明中。Swift 类型分为值类型和引⽤类型。两者最典型的例⼦分 别是结构体和类。为了阐明它们之间的区别,我们将定义下述结构体: struct PointStruct { var x: Int var y: Int } 现在让我们来看看下⾯的代码⽚段: var structPoint = PointStruct(x: 1, y: 2) var sameStructPoint = structPoint sameStructPoint.x = 3 在执⾏这段代码之后,很明显 sameStructPoint 等于 (x: 3, y: 2)。然⽽ structPoint 仍然保持 原始值。这就是值类型与引⽤类型之间的关键区别:当被赋以⼀个新值或是作为参数传递给函 数时,值类型会被复制。之所以给 sameStructPoint.x 赋值并不更新原来的 structPoint,正是 因为先前的 sameStructPoint = structPoint 赋值过程中,发⽣了值复制。 为了进⼀步说明区别,我们可以声明⼀个点类: class PointClass { var x: Int var y: Int init (x: Int, y: Int) { self.x = x self.y = y } } 然后修改上⾯的代码⽚段,使⽤类代替结构体: var classPoint = PointClass(x: 1, y: 2) var sameClassPoint = classPoint sameClassPoint.x = 3 现在,给 sameClassPoint.x 赋值之后,既修改了 sameClassPoint,也修改了 classPoint,原 因在于类是引⽤类型。理解值类型与引⽤类型之间的区别极其重要 —— 由此你可以预测赋值 ⾏为将会如何修改数据,同时哪些代码可能受到这个改变的影响。 在调⽤函数的时候,值类型与引⽤类型之间的区别同样是显⽽易⻅的。不妨让我们看⼀看下⾯ 这个总是返回原点的函数: func setStructToOrigin(var point: PointStruct) -> PointStruct { point.x = 0 point.y = 0 return point } 我们使⽤这个函数来⽣成⼀个点: var structOrigin: PointStruct = setStructToOrigin(structPoint) ⽐如结构体这样的值类型,在作为函数的参数被传递时将会被复制后使⽤。因此,在这个例⼦ 中,调⽤ setStructToOrigin 之后,原来的 structPoint 并没有被修改。 现在假设我们编写了下⾯的函数,参数由结构体变为了类: func setClassToOrigin(point: PointClass) -> PointClass { point.x = 0 point.y = 0 return point } 于是下⾯的函数调⽤将会修改 classPoint: var classOrigin = setClassToOrigin(classPoint) 当被赋以⼀个新变量或者传递给函数时,值类型总是会被复制,⽽引⽤类型并不会被复制。引 ⽤类型顾名思义,我们只是使⽤了对已经存在的对象或实例的引⽤,任何对该引⽤的改变都将 会修改原来的对象或实例。 Andy Matuschak 在他给 objc.io 撰写的⽂章中对值类型与引⽤类型之间的区别进⾏讨论时,给 出了⼀些⼗分有⽤且直观的例⼦。 在 Swift 中,结构体并不是唯⼀的值类型。事实上,Swift ⼏乎所有类型都是值类型,包括数 组,字典,数值,布尔值,元组和枚举 (将在下⼀章介绍)。只有类 (class) 是⼀个例外。这正说 明了 Swift 正在从 “⾯向对象编程” 进化到其他的编程范式。 稍后我们会就此对类和结构体加以对⽐;在此之前,我们想先简要地探讨⼀下迄今为⽌所看到 的不同形式可变性之间的相互作⽤。 结构体与类:究竟是否可变? 在上述例⼦中,我们使⽤ var ⽽⾮ let 将所有的 Point 类型及它们的属性声明为了可变变量。 我们需要对由结构体和类等构造出的混合类型,以及它们与 var 和 let 声明的相互作⽤进⾏⼀ 些说明。 假如我们对 PointStruct 以不可变的⽅式进⾏了实例化,如下所⽰: let immutablePoint = PointStruct(x: 0, y: 0) 很显然,给 immutablePoint 赋⼀个新值不会被接受: immutablePoint = PointStruct(x: 1, y: 1) // 被拒绝 同样地,尝试给点的任意⼀个属性赋新值也会被拒绝,尽管在 PointStruct 中定义属性使⽤了 var,这是由于 immutablePoint 是通过 let 被定义的: immutablePoint.x = 3 // 被拒绝 然⽽,如果我们将点声明为可变变量,则在初始化之后仍然可以修改它的属性: var mutablePoint = PointStruct(x: 1, y: 1) mutablePoint.x = 3; 如果使⽤ let 关键字声明结构体中的 x 和 y 属性,那么⼀旦初始化,我们将再也不能修改它们, ⽆论保存这个点实例的变量可选还是不可选的: struct ImmutablePointStruct { let x: Int let y: Int } var immutablePoint2 = ImmutablePointStruct(x: 1, y: 1) immutablePoint2.x = 3 // 被拒绝! 当然,我们仍然可以给 immutablePoint2 赋⼀个新值: immutablePoint2 = ImmutablePointStruct(x: 2, y: 2) Objective-C ⼤多 Objective-C 程序员对于可变性和不可变性的概念应该早已⼗分熟悉。Apple 的 Core Foundation 和 Foundation 框架提供的许多数据结构都存在不可变和可变两个版本,⽐如 NSArray 和 NSMutableArray,NSString 和 NSMutableString,当然并不只有这两对。⼤多数 情况下使⽤不可变类型是默认选项,就像 Swift 中⽐起引⽤类型更倾向于优先选择值类型⼀样。 不过,相较于 Swift,Objective-C 中并没有万⽆⼀失的⽅法来确保变量不可变。我们可以将对 象的属性声明为只读 (或者为了避免可变,仅暴露⼀个接⼝),但这⽆法阻⽌我们 (⽆意地) 在类 型内部修改已经被初始化的值。⽐⽅说,当遇上遗留代码时,那些编译器⼒所不逮的可变性假 定实在是太容易被打破了。在使⽤可变变量时,如果没有经由编译器进⾏检验,保证任何⼀种 规范都将是天⽅夜谭。 在和框架代码打交道的时候,我们通常可以将已经存在的可变类封装到⼀个结构体中。不过, 这⾥务必⼩⼼:如果我们在结构体中保存了⼀个对象,引⽤是不可变的,但是对象本⾝却可以 改变。Swift 数组就是这样的:它们使⽤低层级的可变数据结构,但提供⼀个⾼效且不可变的接 ⼝。这⾥使⽤了⼀个被称为写⼊时复制 (copy-on-write) 的技术。你可以阅读我们的书籍 《Swift 进阶》来了解更多关于封装已有 API 的内容。 讨论 在本章中,我们已经看到 Swift 如何区别可变值和不可变值,以及值类型和引⽤类型。在本章 最后,我们想要解释⼀下为什么这些区别很重要。 在了解⼀款软件的时候,耦合度通常被⽤来描述代码各个独⽴部分之间彼此依赖的程度。耦合 度是衡量软件构建好坏的重要因素之⼀。最坏的情况下,所有类和⽅法都错综复杂相互关联, 共享⼤量可变变量,甚⾄连具体的实现细节都存在依赖关系。这样的代码难以维护和更新:你 ⽆法理解或修改⼀⼩段独⽴的代码⽚段,⽽是需要⼀直站在整体的⻆度来考虑整个系统。 在 Objective-C 和很多其它⾯向对象的语⾔中,对于⽅法⽽⾔,由于共享实例变量⽽产⽣耦合 的情况⼗分常⻅。其结果就是,修改变量的同时可能会改变类中⽅法的⾏为。通常,这是⼀件 好事 —— 如果你改变了存储在对象中的值,它的所有⽅法都将使⽤新值。不过同时,这样的 共享实例变量在类的⽅法之间建⽴了耦合关系。⼀旦有⽅法或是外部函数将这个共享状态弄错, 所有类⽅法都有可能表现出错误⾏为。由于它们彼此耦合,独⽴测试任意⼀个⽅法也变得⼗分 困难。 现在,让我们来回顾⼀下QuickCheck 章节中我们所测试的函数。那些函数的输出值都只取决 于输⼊值。像这样只要输⼊值相同则得到的输出值⼀定相同的函数有时被称为引⽤透明函数。 根据定义,引⽤透明函数在它所存在环境中是松耦合的:除了函数的参数,不存在任何隐式依 赖的状态或变量。因此,引⽤透明函数更容易单独测试和理解。此外,我们可以创建、调⽤和 组装引⽤透明函数,其结果也将是引⽤透明的。引⽤透明性是模块化和可重⽤性的重要保证。 引⽤透明化在各个层⾯都使代码更加模块化。想象⼀下,你通过阅读 API 源码来试图弄清楚它 是如何⼯作。⽂档可能早已过时,不再有⽤,但如果这个 API 没有可变状态 —— 所有变量都 通过 let ⽽⾮ var 进⾏声明 —— 这将会是难以置信的有⽤信息。你再也不必担⼼初始化对象或 处理命令的顺序是否正确。⽽只需关注函数类型和 API 定义的常量,以及考虑它们是如何被组 装起来并产⽣想要的值的。 在 Swift 中,var 和 let 之间的区别不仅使得程序员能够区分可变和不可变数据,还可以让编译 器识别这种区别。相较于 var,我们更倾向于 let,它降低了程序的复杂性 —— 你不必再因为 不知道可变变量当前的值到底是什么感到不安,⽽可以简单地使⽤它们的不可变定义。对不可 变性的偏爱使得编写引⽤透明函数更加容易,最终还降低了耦合度。 同样,Swift 中值类型和引⽤类型的区别,也⿎励你在程序中对那些可能改变的对象和不会改 变的数据进⾏区分。函数可以⾃由地复制、改变或共享传⼊的值类型 —— 任何对它的修改仅 会影响函数内部的副本。另外,尽量使⽤引⽤透明的函数,将会有助于编写耦合更松散的代码, 因为任何源⾃共享状态或对象的依赖性都将被消除。 我们可以彻底不使⽤可变变量吗?像 Haskell ⼀样的纯函数式编程语⾔⿎励程序员彻底避免使 ⽤可变状态。当然,在这个世界上是存在不使⽤任何可变状态且庞⼤的 Haskell 程序的。然⽽ 在 Swift 中,教条式地不惜⼀切代价避开 var 并不⻅得会使你的代码更好。在不少情况下,函 数会在其内部使⽤⼀些可变状态。不妨看看下⾯这个求所有数组元素之和的例⼦: func sum(xs: [Int]) -> Int { var result = 0 for x in xs { result += x } return result } sum 函数使⽤的变量 result 是可变的,它反复被更新。但是暴露给⽤⼾的接⼝却隐瞒了这个事 实。sum 函数依然是引⽤透明的,甚⾄⽐⼀个为了避开可变变量⽽不惜⼀切代价所进⾏的繁琐 定义更容易理解。这个例⼦展⽰了⼀种可变变量的良性使⽤⽅式。 像这样良性的可变变量运⽤很⼴泛。⽐⽅说在QuickCheck 章节定义的 qsort ⽅法: func qsort(var array: [Int ]) -> [Int] { if array.isEmpty { return [] } let pivot = array.removeAtIndex(0) let lesser = array. lter { $0 < pivot } let greater = array. lter { $0 >= pivot } return qsort(lesser) + [pivot] + qsort(greater) } 虽然这个⽅法尽可能避免了可变引⽤的使⽤,但它带来了额外的内存开销,⽆法运⾏在常数量 级 (O(1)) 的内存中。它为组成返回值的新数组 lesser 和 greater 分配了内存。当然,通过使⽤ 可变数组,我们可以定义⼀个运⾏在常数量级内存中,且仍然是引⽤透明的新版本的快速排序 算法。巧妙地使⽤可变变量有时能够提升性能和内存使⽤。 总⽽⾔之,Swift 提供了⼏种专⻔控制程序中使⽤可变状态的语法特征。虽然完全避开可选状 态⼏乎不可能,但是仍有很多程序过度且不必要地使⽤可变性。学会在可能的时候避免使⽤可 变状态和对象,将有助于降低耦合度,从⽽改善你的代码结构。 枚举 8 在设计和实现 Swift 应⽤时,类型扮演着⾮常重要的⻆⾊,这也是本书想说明的重点之⼀。在 这⼀章,我们将介绍 Swift 中的枚举类型。借此,你可以创建更为严密的类型,来表⽰应⽤中 使⽤的数据。 关于枚举 创建字符串时,字符编码是很重要的信息之⼀。在 Objective-C 中,NSString 对象会有以下⼏ 种可能的编码: enum NSStringEncoding { NSASCIIStringEncoding = 1, NSNEXTSTEPStringEncoding = 2, NSJapaneseEUCStringEncoding = 3, NSUTF8StringEncoding = 4, // ... } 每⼀种编码都可以⽤⼀个数字来表⽰,enum 关键字允许开发者为整数常量指派⼀些有意义的 名字,以此来关联特定的字符编码。 在 Objective-C 和其他类 C 语⾔中,枚举的声明⽅式是有⼀些缺陷的。最需要注意的是, NSStringEncoding 作为类型来说并不够严密 —— 有些整数值,⽐如 16,并没有⼀个与之对 应的合法编码。更糟糕的是,正因为所有的枚举类型实际上都是整数,它们之间是可以进⾏运 算的,就好像它们只是数字⼀样。 NSAssert(NSASCIIStringEncoding + NSNEXTSTEPStringEncoding == NSJapaneseEUCStringEncoding, @"Adds up..."); 谁能想到 NSASCIIStringEncoding + NSNEXTSTEPStringEncoding 会等于 NSJapaneseEUCStringEncoding 呢?这样的表达式虽然毫⽆意义,但 Objective-C 的编译器 却对此⼤开⽅便之⻔。 在之前章节列举的例⼦中,我们已经利⽤ Swift 中的类型系统发现过类似的错误。仅仅依靠整 数作为标记的枚举类型,并不满⾜ Swift 函数式编程中的⼀条核⼼原则:⾼效地利⽤类型排除 程序缺陷。 Swift 也有⼀种 enum 的构造⽅式,不过其⽤法与你熟悉的 Objective-C 语法相距甚远。我们可 以⽤下⾯的代码来声明我们⾃⼰的字符编码枚举类型: enum Encoding { case ASCII case NEXTSTEP case JapaneseEUC case UTF8 } 新枚举只定义了我们之前在 NSStringEncoding 枚举中列举的前四种编码,实际上类似的字符 编码还有很多,这⾥就不再⼀⼀列举了。毕竟,上⽂声明的 Swift 枚举只是为了说明问题。 Encoding 类型中包括四个可能值:ASCII,NEXTSTEP,JapaneseEUC 与 UTF8。我们将这些 可能的值视为枚举的成员值,也可以简称为成员。在很多⽂献中,这样的枚举有时会被称为和 类型 (sum types),不过在本书中,将以苹果的术语为准。 与 Objective-C 相⽐⽽⾔,编译器是不⽀持以下代码的: let myEncoding = Encoding.ASCII + Encoding.UTF8 不同于 Objective-C,枚举在 Swift 中创建了新的类型,与整数或者其他已经存在的类型没有任 何关系。 我们可以定义⼀个函数,利⽤ switch 语句来计算对应的编码。⽐如,我们可能希望计算出枚举 的成员在 NSStringEncoding 中对应的值: extension Encoding { var nsStringEncoding: NSStringEncoding { switch self { case .ASCII: return NSASCIIStringEncoding case .NEXTSTEP: return NSNEXTSTEPStringEncoding case .JapaneseEUC: return NSJapaneseEUCStringEncoding case .UTF8: return NSUTF8StringEncoding } } } 这⾥的 nsStringEncoding 属性映射了每⼀个 Encoding 条件下对应的 NSStringEncoding 值。 要注意的是,以上四种不同的编码⽅案各⾃对应了⼀条分⽀。如果缺少了任意⼀条分⽀,Swift 的编译器会警告我们这个计算属性中的 switch 语句是不完整的。 当然,我们也可以定义⼀个函数实现相反的功能,即根据 NSStringEncoding 创建⼀个 Encoding。我们可以据此实现⼀个 Encoding 枚举的构造⽅法: extension Encoding { init ?(enc: NSStringEncoding) { switch enc { case NSASCIIStringEncoding: self = .ASCII case NSNEXTSTEPStringEncoding: self = .NEXTSTEP case NSJapaneseEUCStringEncoding: self = .JapaneseEUC case NSUTF8StringEncoding: self = .UTF8 default: return nil } } } 由于这个精简版的 Encoding 枚举并没有列举所有可能的 NSStringEncoding 值,所以该构造 ⽅法是可失败的。如果前四个条件都不匹配,default 分⽀就会被选中,并返回⼀个 nil。 在编写完以上的代码之后,我们在使⽤ Encouding 枚举时,就不必再使⽤ switch 语句了。⽐ 如,我们想得到某个编码的本地化名称,可以编写以下代码: func localizedEncodingName(encoding: Encoding) -> String { return .localizedNameOfStringEncoding(encoding.nsStringEncoding) } 关联值 到这⾥,我们已经看到了 Swift 的枚举表⽰若⼲选项中的特定选项的⽤法。Encoding 枚举为不 同的字符编码⽅案提供了⼀种安全、类型化的表⽰⽅式。不过,Swift 中的枚举可不⽌这么点⽤ 途。 回过头来看看第五章中的 populationOfCapital 函数。它⽤来查找⼀个国家的⾸都,如果找到, 它会返回该城市的⼈⼝总数。这个函数的返回类型是⼀个整数类型的可选值:如果所有信息都 被找到的话,返回⼈⼝数;否则,返回 nil。 使⽤ Swift 的可选值类型时有⼀个缺点:当有错误发⽣时,我们⽆法返回相关的信息,所以也⽆ 从判定到底是哪⾥出了错。是国家的信息不在我们的字典⾥么?还是⾸都的⼈⼝数没有被定义? 如果可以的话,我们会更希望 populationOfCapital 函数返回⼀个 Int 或者⼀个 ErrorType。利 ⽤ Swift 的枚举,就可以搞定这件事。我们可以重新定义 populationOfCapital 函数,使之返回 ⼀个 PopulationResult 枚举的成员,来代替之前的 Int?。可以像下⽂这样定义 PopulationResult: enum LookupError: ErrorType { case CapitalNotFound case PopulationNotFound } enum PopulationResult { case Success(Int) case Error(LookupError) } 与 Encoding 枚举相⽐,PopulationResult 的成员是带有关联值的。它只有两个可能的成员值: Success 和 Error,每⼀个成员值都携带了额外的信息:Success 关联了⼀个整数值,对应着 国家⾸都的⼈⼝数;⽽ Error 则关联了⼀个 ErrorType。为了⽅便说明,我们可以像下⽂这样声 明⼀个 Success: let exampleSuccess: PopulationResult = .Success(1000) 类似地,使⽤ Error 成员来创建⼀个 PopulationResult 时,则需要关联⼀个 LookupError 值。 现在,我们可以重写 populationOfCapital 函数,使之返回⼀个 PopulationResult: func populationOfCapital(country: String) -> PopulationResult { guard let capital = capitals[country] else { return .Error(.CapitalNotFound) } guard let population = cities[capital] else { return .Error(.PopulationNotFound) } return .Success(population) } 现在,函数会返回⼈⼝数或者⼀个 LookupError 来代替之前的 Int 可选值。⾸先,我们检查了 capitals 字典中是否存在对应的⾸都名,如果不存在,就返回⼀个 .CapitalNotFound 错误。 接着,我们验证了 cities 字典中是否存在对应的⼈⼝数,如果不存在,则返回⼀个 .PopulationNotFound 错误。最后,如果两次查询都找到了对应的值,便返回⼀个 Success。 在调⽤ populationOfCapital 时,可以使⽤⼀个 switch 语句来确定函数是否成功: switch populationOfCapital("France"){ case let .Success(population): print("France's capital has \(population) thousand inhabitants") case let .Error(error ): print("Error:\(error)") } 添加泛型 有⼈说,我想写⼀个与 populationOfCapital 类似的函数,只不过不是查询⼈⼝,⽽是查询⼀ 个国家⾸都的市⻓: let mayors = [ "Paris":"Hidalgo", "Madrid":"Carmena", "Amsterdam":"van der Laan", "Berlin":"Müller" ] 通过可选值,我们可以简单的查询到⼀个国家的⾸都,然后在结果中使⽤ atMap 找到这座城 市的市⻓: func mayorOfCapital(country: String) -> String? { return capitals[country].atMap { mayors[$0] } } 然⽽,使⽤可选值作为返回类型,依旧不会告诉我们为什么查询会失败。 不过,我们已经知道如何去解决这个问题了!我们的第⼀反应,可能是通过复⽤ PopulationResult 枚举来返回错误。不过,与 mayorOfCapital 成功时的返回值相冲突的是, 之前 Success 的关联值并不是⼀个字符串,⽽是⼀个整数。虽然我们可以将返回的整数再转换 成对应的字符串,但这并不是⼀个好的设计:我们应该使⽤更为严密的类型,来避免为类似的 类型转换编写额外的代码。 或者,我们可以定义⼀个新枚举 MayorResult,来对应两种可能的情况: enum MayorResult { case Success(String) case Error(ErrorType) } 毫⽆疑问,我们可以利⽤这个枚举来编写另⼀个版本的 mayorOfCapital 函数 —— 不过为每⼀ 个新函数都引⼊⼀个枚举实在是太乏味了。更何况,MayorResult 与 PopulationResult ⼤同⼩ 异得令⼈发指。两个枚举唯⼀的区别就是 Success 和 Error 的关联值类型。所以我们定义了⼀ 个新的枚举,将泛型作为 Success 的关联值: enum Result { case Success(T) case Error(ErrorType) } 现在,我们可以在 populationOfCapital 与 mayorOfCapital 中使⽤同样的结果类型了。新的类 型表达式变成了下⾯的样⼦: func populationOfCapital(country: String) -> Result func mayorOfCapital(country: String) -> Result populationOfCapital 函数返回⼀个 Int 或者⼀个 LookupError,mayorOfCapital 则返回⼀个 String 或 ErrorType。 Swift 中的错误处理 实际上,Swift 中内建的错误处理机制与我们在上⽂定义的 Result 类型⼗分相似。它们的不同 主要有两点:Swift 强制你注明哪些代码可能抛出错误,且必须使⽤ try (或 try 的变体) 来调⽤ 这些代码。如果换作 Result 类型的话,我们是⽆法在静态环境下确保错误被处理的。另外, Swift 内建错误处理机制的局限性在于,它必须借助函数的返回类型来触发:如果我们想构建 ⼀个函数,且提供的参数包含失败情况 (⽐如⼀个回调函数),使⽤ throw 的⽅式来提供这个参 数,会让⼀切都变得复杂起来。若是换⽤可选值或 Result,编写起来就没那么繁琐,处理也会 更简单。 如果使⽤ Swift 的错误处理机制重写 populationOfCapital,我们可以简单地在函数声明上加⼊ throws 关键字。相应的,我们得 throw ⼀个错误,⽽不再是返回⼀个 .Error。类似地,我们现 在可以直接返回⼈⼝数⽽不再是⼀个 .Success 了: func populationOfCapital1(country: String) throws -> Int { guard let capital = capitals[country] else { throw LookupError.CapitalNotFound } guard let population = cities[capital] else { throw LookupError.PopulationNotFound } return population } 要调⽤⼀个有 throws 标记的函数,我们可以将调⽤代码嵌⼊⼀个 do 执⾏块中,然后添加⼀个 try 的前缀。这样做的好处在于,我们可以在 do 执⾏块中编写正常的流程,然后在 catch 块中 去处理所有可能的错误: do { let population = try populationOfCapital1("France") print("France's population is \(population)") } catch { print("Lookup error:\(error)") } 再聊聊可选值 实际上,Swift 内建的可选值类型与 Result 类型也很像。下⾯的代码⽚段基本上是直接从 Swift 的标准库中复制出来的: enum Optional { case None case Some(T) // ... } 可选值类型提供了⼀些语法糖,像是后缀标记 ? 以及可选值的展开机制等,使其更容易被使⽤。 其实,你完全可以⾃⼰来定义需要的操作。 ⽐如,我们可以在我们⾃⼰的 Result 类型中定义⼀些⽤于操作可选值的函数。通过在 Result 中重新定义 ?? 运算符,我们可以对 Result 进⾏运算: func ??(result: Result, handleError: ErrorType -> T) -> T { switch result { case let .Success(value): return value case let .Error(error ): return handleError(error) } } 数据类型中的代数学 就像我们之前提到的那样,枚举常常被称为 “和类型”。这可能是⼀个让⼈困惑的名字,枚举看 起来与数字毫⽆关系。不过深挖下去的话,你可能会发现,枚举和多元组的数学结构,在计算 时其实⾮常相似。 在分析这个结构之前,我们需要考虑⼀个问题:两个类型在什么时候是相同的。这个问题可能 会让你觉得诧异,答案就好像 String 与 String 相同,与 Int 是不同的⼀样显⽽易⻅,不是么? 然⽽,当你把泛型、枚举、结构体还有函数都放在⼀起考虑时,问题就变得复杂起来了。实际 上,这个看似简单的问题⾄今仍被当做⼀个数学中的基础课题在研究。为了讲清楚这个⼩节, 我们需要先理解两个类型在什么时候是同构 (isomorphic) 的。 ⽐较直观的解释是,如果两个类型 A 和 B 在相互转换时不会丢失任何信息,那么它们就是同构 的。为此,我们需要构造两个函数,f: A -> B,和 g: B -> A,使两者可以相互转换。也就是 说,对任意 x: A,调⽤ g(f(x)) ⽅法得到的结果⼀定与 x 相等;类似地,对任意 y: B,调⽤ f(g(y)) 的结果也等于 y。我们可以将刚才提到的关于同构的直观说明提取成⼀个定义:我们可 以随意地利⽤ f 和 g 来转换 A 和 B,⽽不会丢失信息 (也就是说我们可以利⽤ g 来撤销 f,反之 亦然)。如果仅仅针对编程,这个定义可能不够严密 —— ⼀个 64 位的值既可以被⽤于表⽰整 数,也可以是⼀个内存地址,这是两个完全不同的概念。不过,在我们研究类型的数学结构时, 这个定义就派上⽤场了。 接着,我们来看看下⾯的枚举: enum Add { case InLeft(T) case InRight(U) } 这段代码给出了两个类型,T 和 U。枚举 Add 由⼀个 T 类型或者⼀个 U 类型的值组成。 就像命名所表达的那样,Add 枚举是 T 与 U 的成员相加之和:如果 T 有三个成员,⽽ U 有七个, 那 Add 就会有⼗个可能的成员。以上描述渗透出来的观点,也为枚举被称为 “和类型” 的 原因,提供了更深层次的解释。 在算术中,0 是加法的运算单元,⽐如 x + 0 和 x ⼀样,可以表⽰任意⼀个数字 x 。我们可以找 到⼀个枚举的表⽰⽅法类似于 0 么?有趣的是,Swift 允许我们定义以下的枚举: enum Zero { } 这个枚举是空的 —— 它没有任何成员。正如我们所希望的那样,这个枚举与算术中的 0 有着极 其相似的功能:对任何⼀个类型 T,Add 和 T 是同构的。这很容易被证明。我们可以使 ⽤ InLeft 定义⼀个函数将 T 转换为 Add,⽽反向的转换则可以通过模式匹配来完成。 关于加法的部分就到此为⽌ —— 我们再来看看乘法。如果有⼀个包含三个成员的枚举 T,和另 ⼀个包含两个成员的枚举 U。我们如何去定义⼀个混合类型 Times,使之包含六个成员 呢?如果要满⾜这个需求,Times 类型应该被允许同时选择⼀个 T 的成员和⼀个 U 的成 员。换句话说,它应该可以代表⼀对类型分别为 T 和 U 的值: struct Times { let fst: T let snd: U } 就像 Zero 可以作为⼀个加法的单元⼀样,空类型 (),也可以作为⼀个 Times (乘法) 的单元: typealias One = () 将这些结构看作同构类型时,很多熟悉的算术规则在这⾥同样适⽤,且很容易被验证: → Times 与 T 是同构的 → Times 与 Zero 是同构的 → Times 与 Times 是同构的 使⽤枚举和多元组定义的类型有时候也被称作代数数据类型 (algebraic data types),因为它们 就像⾃然数⼀样,具有代数学结构。 关于数字与类型的⼀致性这个话题,其实还可以挖的更深。⽐如函数在某种程度上就相当于幂 运算。甚⾄,还可以为类型定义⼀个微分的概念! 以上的讨论可能没什么实⽤的价值。只不过,它告诉了我们,包括枚举在内的许多 Swift 特性, 并不是什么新的发明,⽽是从经年累⽉的数学研究和编程语⾔设计中所萃取的精华。 为什么使用枚举? 在实际开发中,可选值可能还是会⽐上⽂定义的 Result 类型更好⽤,原因有很多:内建的语法 糖使⽤起来更⽅便;相对于使⽤⾃⼰定义的枚举,依赖⼀些已经存在的类型,会使你定义的接 ⼝更容易被其他 Swift 开发者接受;⽽且有时候并不值得为 ErrorType 专⻔费事去定义⼀个枚 举。 其实,我们希望说明的问题,并不是 “Result 类型是 Swift 中处理错误的最好的⽅案”。我们只 是试图阐述,如何使⽤枚举去定义你⾃⼰的类型,来解决你的具体需求。通过让类型更加严密, 我们可以在程序测试或运⾏之前,就利⽤ Swift 的类型检验优势,来避免许多错误。 纯函数式数据结 构 9 在前⾯的章节中,我们了解了如何利⽤枚举针对正在开发的应⽤来定义特定类型。⽽本章中, 我们会定义可递归的枚举,并向⼤家展⽰,如何利⽤这个特性来定义⼀些⾼性能且⾮可变的数 据结构。 所谓纯函数式数据结构 (Purely Functional Data Structures) 指的是那些具有不变性的⾼效的 数据结构。像 C 或 C++ 这样的指令式语⾔中的数据结构往往是可变的,直接将这类数据结构使 ⽤在函数式语⾔中往往会⽔⼟不服。通过本章,我们想向您展⽰函数式语⾔中的纯函数式数据 结构的构建⽅式和特点。 二叉搜索树 在 Swift 发布的时候,并没有⼀个类似于 Objective-C 中 NSSet 的库来处理⽆序集合 (Set)。尽 管我们可以使⽤ Swift 封装 NSSet —— 就像我们曾经为 Core Image 与 String 构造⽅法所做 的那样 —— 但这⾥我们还是想探究⼀种不太⼀样的⽅式。我们的⽬标,依旧不是去定义⼀个 在 Swift 中处理⽆序集合的完备库,⽽是为了演⽰如何⽤递归式枚举来定义⾼效的数据结构。 在我们的迷你库中,我们会实现以下四种操作: → empty —— 返回⼀个空的⽆序集合 → isEmpty —— 检查⼀个⽆序集合是否为空 → contains —— 检查⽆序集合中是否包含某个元素 → insert —— 向⽆序集合中插⼊⼀个元素 我们最先想到的,可能是使⽤数组来表⽰⽆序集合。那么实现这四个操作,简直是探囊取物: func empty() -> [Element] { return [] } func isEmpty(set: [Element]) -> Bool { return set.isEmpty } func contains(x: Element, _ set: [Element]) -> Bool { return set.contains(x) } func insert(x: Element, _ set:[Element]) -> [Element] { return contains(x, set)? set : [x] + set } 尽管实现简单,可随之⽽来的痛点是,⼤部分操作的性能消耗与⽆序集合的⼤⼩是线性相关的。 如果⽆序集合过⼤,这可能会导致性能问题。 想要提⾼性能,这⾥有⼀些可⾏的⽅式。例如,我们可以确保数组是经过排序的,然后使⽤⼆ 分查找来定位特定元素。或者再彻底⼀些,索性定义⼀个⼆叉搜索树 (Binary Search Trees) 来 表⽰⽆序集合。我们可以⽤传统的 C 语⾔⻛格打造⼀个树形结构,在每个节点持有指向⼦树的 指针。当然,也可以利⽤ Swift 中的 indirect 关键字,直接将⼆叉树结构定义为⼀个枚举: indirect enum BinarySearchTree { case Leaf case Node(BinarySearchTree, Element, BinarySearchTree) } 这个定义规定了每⼀棵树,要么是: → ⼀个没有关联值的叶⼦ Leaf,要么是 → ⼀个带有三个关联值的节点 Node,关联值分别是左⼦树,储存在该节点的值和右⼦树。 在为⼆叉树定义函数之前,我们可以⼿动构造⼏棵树作为⽰例: let leaf: BinarySearchTree = .Leaf let ve : BinarySearchTree = .Node(leaf, 5, leaf) leaf 树是空的;ve 树在节点上存了值 5,但两棵⼦树都为空。我们可以编写两个构造⽅法来 ⽣成这两种树:⼀个会创建⼀棵空树,⽽另⼀个则创建含有某个单独值的树: extension BinarySearchTree { init () { self = .Leaf } init (_ value: Element) { self = .Node(.Leaf, value, .Leaf) } } 就像我们之前章节中看到的那样,我们可以编写⼀些函数,利⽤ switch 语句来处理这些树。由 于 BinarySearchTree 枚举是⽀持递归的,所以理所应当,我们编写的许多基于树的函数也都 会是递归的。举个例⼦,下⾯的函数⽤来计算⼀棵树中存值的个数: extension BinarySearchTree { var count: Int { switch self { case .Leaf: return 0 case let .Node(left,_, right ): return 1 + left .count + right.count } } } 在树是 .Leaf 的基本情况下,可以直接返回 0。⽽在 .Node 的情况下就⽐较有意思:我们递归 地计算了两个⼦树储存的元素个数,然后加 1 ,也就是当前节点存值的个数,再将它们的总和 返回。 类似地,我们可以写⼀个 elements 属性,⽤于计算树中所有元素组成的数组: extension BinarySearchTree { var elements: [Element] { switch self { case .Leaf: return [] case let .Node(left, x, right ): return left .elements + [x] + right.elements } } } 现在,让我们回到最初的⽬的,即利⽤树型结构编写⼀个⾼效的⽆序集合库。对于检查⼀棵树 是否为空,有⼀个现成的⽅案: extension BinarySearchTree { var isEmpty: Bool { if case .Leaf = self { return true } return false } } 遗憾地是,当我们试着去我们编写 insert 和 contains 函数的雏形时,看起来并没有什么可以 利⽤的特性。不过,如果为这个结构加上⼀个⼆叉搜索树的限制,问题就会迎刃⽽解。如果⼀ 棵 (⾮空) 树符合以下⼏点,就可以被视为⼀棵⼆叉搜索树: → 所有储存在左⼦树的值都⼩于其根节点的值 → 所有储存在右⼦树的值都⼤于其根节点的值 → 其左右⼦树都是⼆叉搜索树 我们在本章实现的 BinarySearchTree 有⼀个缺点:因为你可以 “⼿动” 构造出任何样式的树, 所以我们⽆法严格地将⼀棵树限制为⼆叉搜索树。在实际情况中,我们应该把枚举封装为⼀个 私有的实现细节,以确保我们⽣成的树是⼀棵⼆叉搜索树。为求简单,我们在这⾥忽略就好。 我们可以写⼀个 (低效率的) 属性来检查 BinarySearchTree 实际上是不是⼀棵⼆叉搜索树: extension BinarySearchTree where Element: Comparable { var isBST: Bool { switch self { case .Leaf: return true case let .Node(left, x, right ): return left .elements.all { y in y < x } && right.elements.all { y in y > x } && left.isBST && right.isBST } } } ⽅法 all 检查了⼀个数组中的元素是否都符合某个条件。它被定义为⼀个 SequenceType 协议 的拓展: extension SequenceType { func all (predicate: Generator.Element -> Bool) -> Bool { for x in self where !predicate(x) { return false } return true } } ⼆叉搜索树的关键特性在于它们⽀持⾼效的查找操作,类似于在⼀个数组中做⼆分查找。当我 们需要遍历⼀棵树来查找某个元素是否在树中时,我们可以在每⼀步都排除⼀半元素。举例来 说,这⾥有⼀个可⾏的 contains 函数定义,来查找⼀个元素是否在树中: extension BinarySearchTree { func contains(x: Element) -> Bool { switch self { case .Leaf: return false case let .Node(_, y, _) where x == y: return true case let .Node(left, y, _) where x < y: return left .contains(x) case let .Node(_, y, right) where x > y: return right.contains(x) default: fatalError("The impossible occurred") } } } contains 函数现在被分为四种可能的情况: → 如果树是空的,则 x 不在树中,返回 false。 → 如果树不为空,且储存在根节点的值与 x 相等,返回 true。 → 如果树不为空,且储存在根节点的值⼤于 x,那么如果 x 在树中的话,它⼀定是在左⼦ 树中,所以,我们在左⼦树中递归搜索 x。 → 类似地,如果根节点的值⼩于 x,我们就在右⼦树中继续搜索。 不幸地是,Swift 的编译器还没有聪明到能够发现这四种情况已经包括了所有的可能性,所以 我们还得再添加⼀个⽤来安抚编译器的 default。 插⼊操作也是⽤同样的⽅式对⼆叉搜索树进⾏搜索: extension BinarySearchTree { mutating func insert(x: Element) { switch self { case .Leaf: self = BinarySearchTree(x) case .Node(var left, let y, var right ): if x < y { left .insert(x) } if x > y { right.insert(x) } self = .Node(left, y, right) } } } 不同于先检查⼀个元素是否已经被包括在⼀棵⼆叉搜索树中,insert 会找到⼀个合适的位置来 添加新元素。如果树是空的,就构建⼀棵只有⼀个元素的树。如果元素已经存在,就返回树本 ⾝。否则,insert 函数持续地递归,直到找到⼀个合适的位置来插⼊新元素。 insert 函数被写作⼀个 mutating (可变的) 函数,然⽽,这与那种基于类的数据结构中的可变性 (motation) 有很⼤区别。实际的值并没有被修改,被修改的只是变量。举个例⼦,在执⾏插⼊ 操作的情况下,新的树是在旧树的分⽀之外构建的,分⽀本⾝并不会被修改。我们可以观察⼀ 个例⼦来验证这个特性: let myTree: BinarySearchTree = BinarySearchTree() var copied = myTree copied.insert(5) (myTree.elements, copied.elements) /** 结果为: ([], [5]) */ 在最坏的情况下,⼆叉搜索树中的 insert 与 contains 仍然是线性的 —— 毕竟,总会出现像是 所有的左⼦树都为空这种⾮常不平衡的树。⼀些更为巧妙的实现⽅案,⽐如 2-3 树,AVL 树, 或者红⿊树,可以通过使每棵树都保持合理平衡来避免这种情况。另外,我们并没有编写 delete 操作,这个操作也需要对树进⾏反复地平衡。关于这些内容,各种⽂献中有⼤量被充分 论证过的经典⽅案 —— 所以重申⼀下,这⾥的例⼦只是为了说明如何利⽤递归枚举,⽽并不 会构建⼀个完整的库。 基于字典树的自动补全 在了解了⼆叉树之后,这⼀⼩节会演⽰⼀个更加⾼级的纯函数式数据结构。假设,我们现在需 要⾃⼰实现⼀个⾃动补全算法 —— 在给定⼀组搜索的历史记录和⼀个现在待搜索字符串的前 缀时,计算出⼀个与之相匹配的补全列表。 如果使⽤数组的话,⼀句代码就可以解决问题: func autocomplete(history: [String], textEntered: String) -> [String] { return history. lter { $0.hasPrex(textEntered) } } 遗憾地是,这个函数依旧不是很⾼效。在历史记录很多,或是前缀很⻓的情况下,运算会慢得 离谱。按照之前的经验,我们可以将历史记录排序为⼀个数组,并对其使⽤某种⼆叉搜索来提 ⾼性能。不过这次,我们可以试试另⼀种解决⽅案,这会⽤到⼀种专⻔⽤于解决类似检索问题 的⾃定义数据结构。 字典树 (Tries),也被称作数字搜索树 (digital search trees),是⼀种特定类型的有序树,通常 被⽤于搜索由⼀连串字符组成的字符串。不同于将⼀组字符串储存在⼀棵⼆叉搜索树中,字典 树把构成这些字符串的字符逐个分解开来,储存在了⼀个更⾼效的数据结构中, 上⽂中,BinarySearchTree 类型的每个节点处都存在两棵⼦树。对于字典树来说,则是每⼀个 字符都 (潜在地) 对应着⼀棵⼦树,不过也因此,其每个节点的⼦树个数都是不确定的。⽐如, 我们可以想象⼀棵储存着 “cat”、“car”、“cart” 和 “dog” 的字典树,如下图所⽰: c d o g a tr t 图 9.1: 字典树 想知道 “care” 是不是在这棵字典树中,我们可以从根节点开始,沿着标记了 c、a 和 r 的路径找 下去。标记着 r 的节点并没有⼀个⼦节点被标记为 e,所以字符串 “care” 并不在这棵树中。⽽ 字符串 “cat” 是在这个字典树⾥的,因为我们可以从根节点找到⼀条标记了 c、a、和 t 的路径。 我们应该如何在 Swift 中表⽰⼀棵字典树呢?最先想到的,是写⼀个结构体,并以⼀个字典作 为属性,⽤来储存所有节点处字符与⼦字典树的映射关系: struct Trie { let children: [Character: Trie] } 在这个定义的基础上,我们可以再做两点优化。⾸先,我们需要在节点上添加⼀些额外信息。 从上图的字典树中可以看出,在添加 “cart” 到字典树中时,“cart” 的所有前缀,“c”、“ca” 和 “car” 也会被添加进来。为了区分出这些前缀是不是也作为⼀个元素储存在字典树中,我们会 在每个节点添加⼀个额外的布尔值 isElement。这个布尔值会标记截⽌于当前节点的字符串是 否在树中。此外,我们可以定义⼀棵泛型字典树,去掉只能储存字符的限制。据此定义字典树 如下: struct Trie { let isElement: Bool let children: [Element: Trie] } 接下来的⽂章中,我们有时会将 [Element] 类型的⼀组键 (以下简称键组) 看作字符串,⽽将 Element 类型的值看作字符。这种做法并不严谨 —— Element 可以被实例化为与字符不同的 类型,⽽字符串实际上也不是 [Character] —— 不过我们希望这可以让你更直观的感受到 “字 典树储存了⼀组字符串”。 在利⽤字典树定义⾃动补全函数之前,我们可以先写⼀些简单的定义来练练⼿。⽐如,⼀棵空 的字典树应该由⼀个字典为空的节点构成: extension Trie { init () { isElement = false children = [:] } } 如果将⼀棵空字典树的 isElement 赋值为 true ⽽不是 false,那么空字符串就会变成空字典树 的⼀个元素 —— 空字典树⾥却有⼀个字符串么?别闹! 接着,我们定义⼀个属性,⽤于将字典树展平 (atten) 为⼀个包含全部元素的数组: extension Trie { var elements: [[Element]] { var result: [[ Element]] = isElement ? [[]] : [] for (key, value) in children { result += value.elements.map { [key] + $0 } } return result } } 这个函数的内部实现⼗分精妙。⾸先,我们会检查当前的根节点是否被标记为⼀棵字典树的成 员。如果是,这个字典树就包含了⼀个空的键,反之,result 变量则会被实例化为⼀个空的数 组。接着,函数会遍历字典,计算出⼦树的所有元素 —— 这是通过调⽤ value.elements 实现 的。最后,每⼀棵⼦字典树对应的 “character” (也就是代码中的 key) 会被添加到⼦树 elements 的⾸位 —— 这正是 map 函数中所做的事情。虽然也可以使⽤ atmap 函数来替换 for 循环,不过现在的代码让整个过程能稍微清晰⼀些。 接下来,我们将定义查询和插⼊的函数。不过在这之前,我们还需要⼏个辅助函数。我们已经 使⽤了数组来表⽰键组,虽然我们将字典树定义为了⼀个 (递归的) 结构体,但数组却并不能递 归。⼀个能够被遍历的数组还是很有⽤的,为数组添加下⽂中的拓展可以便捷的实现这个功能: extension Array { var decompose: (Element, [Element])? { return isEmpty ? nil :(self[startIndex], Array(self.dropFirst ())) } } decompose 函数会检查⼀个数组是否为空。如果为空,就返回⼀个 nil;反之,则会返回⼀个 多元组,这个多元组包含该数组的第⼀个元素,以及去掉第⼀个元素之后由该数组其余元素组 成的新数组。我们可以通过重复调⽤ decompose 递归地遍历⼀个数组,直到返回 nil,⽽此时 数组将为空。 ⽐如,我们可以抛开 for 循环或是 reduce 函数,⽽使⽤ decompose 函数递归地对⼀个数组的 元素求和: func sum(xs: [Int]) -> Int { guard let (head, tail ) = xs.decompose else { return 0 } return head + sum(tail) } 递归式的实现还有另⼀个不太容易被想到的例⼦,即使⽤ decompose 来重写第六章的 qsort 函数: func qsort(input: [Int ]) -> [Int] { guard let (pivot, rest) = input.decompose else { return [] } let lesser = rest. lter { $0 < pivot } let greater = rest. lter { $0 >= pivot } return qsort(lesser) + [pivot] + qsort(greater) } 回到我们最开始的问题 —— 我们现在可以使⽤ decompose 来为数组编写⼀个查询函数:给 定⼀个由⼀些 Element 组成的键组,遍历⼀棵字典树,来逐⼀确定对应的键是否储存在树中: extension Trie { func lookup(key: [Element]) -> Bool { guard let (head, tail ) = key.decompose else { return isElement } guard let subtrie = children[head] else { return false } return subtrie.lookup(tail) } } 查询可以被分为三种情况: → 键组是⼀个空数组 —— 在这种情况下,我们返回当前节点的 isElement,即字典树中 ⽤于描述这个字符串是否存在于树中的布尔值。 → 键组不为空,但是不存在对应的⼦树 —— 在这种情况下,我们返回 false 就可以了, 因为字典树中没有储存这个键组。 → 键组不为空 —— 在这种情况下,我们会查询键组中第⼀个键对应的⼦树。如果⼦树存 在,我们就递归地调⽤该函数,来查询剩余的键是否在这棵⼦树中。 我们也可以对 lookup 函数⼩作修改,给定⼀个前缀键组,使其返回⼀个含有所有匹配元素的 ⼦树: extension Trie { func withPrex(prex: [Element]) -> Trie? { guard let (head, tail ) = prex.decompose else { return self } guard let remainder = children[head] else { return nil } return remainder.withPrex(tail) } } 该函数与 lookup 唯⼀的不同在于它不再返回⼀个 isElement 的布尔值,⽽是将整棵⼦树作为 返回值,其中包含了所有以参数作为前缀的元素。 终于,我们可以利⽤这个⾼性能的字典树数据结构,重新定义 autocomplete 函数: extension Trie { func autocomplete(key: [Element]) -> [[Element]] { return withPrex(key)?.elements ?? [] } } 要计算出字典树中与给定前缀相匹配的所有字符串,我们只需要调⽤ withPrex 函数,如果结 果是字典树,就将其中的元素提取出来。如果不存在与给定前缀匹配的⼦树,就返回⼀个空数 组。 我们可以使⽤与 decompose 相同的⽅式来创建字典树。⽐如,下⾯的代码可以创建只含有⼀ 个元素的字典树: extension Trie { init (_ key: [Element]) { if let (head, tail ) = key.decompose { let children = [head: Trie( tail )] self = Trie(isElement: false, children: children) } else { self = Trie(isElement: true, children: [:]) } } } 与之前⼀样,这⾥分为两种情况: → 如果传⼊的键组不为空,且能够被分解为 head 与 tail,我们就⽤ tail 递归地创建⼀棵 字典树。然后创建⼀个新的字典 children,以 head 为键存储这个刚才递归创建的字典 树。最后,我们⽤这个字典创建⼀棵新的字典树。因为输⼊的 key ⾮空,这意味着当前 键组尚未被全部存⼊,所以 isElement 应该是 false。 → 如果传⼊的键组为空,我们可以创建⼀棵没有⼦节点的空字典树,⽤于储存⼀个空字符 串,并将 isElement 赋值为 true。 我们还可以定义下⾯的插⼊函数来填充字典树: extension Trie { func insert(key: [Element]) -> Trie { guard let (head, tail ) = key.decompose else { return Trie(isElement: true, children: children) } var newChildren = children if let nextTrie = children[head] { newChildren[head] = nextTrie.insert(tail) } else { newChildren[head] = Trie(tail) } return Trie(isElement: isElement, children: newChildren) } } 这个插⼊函数被分为三种情况: → 如果键组为空,我们将 isElement 设置为 true,然后不再修改剩余的字典树。 → 如果键组不为空,且键组的 head 已经存在于当前节点的 children 字典中,我们只需要 递归地调⽤该函数,将键组的 tail 插⼊到对应的⼦字典树中。 → 如果键组不为空,且第⼀个键 head 并不是该字典树中 children 字典的某条记录,就创 建⼀棵新的字典树来储存键组中剩下的键。然后,以 head 键对应新的字典树,储存在 当前节点中,完成插⼊操作。 作为练习,你可以试着将 insert 写成⼀个 mutating 函数。 字符串字典树 为了使⽤我们⾃⼰的⾃动补全算法,现在我们可以为字符串字典树写⼀些简化操作的封装。⾸ 先,我们可以编写⼀个简单的封装,从⼀个单词列表来进⾏字典树的构建。先创建⼀棵空字典 树,然后将单词逐个插⼊,直到字典树包含了所有的单词。因为我们的字典树是基于数组⼯作 的,所以需要先将每⼀个字符串转换为⼀组字符。或者,我们也可以另写⼀个 insert,以⽀持 所有遵守 SequenceType 协议的类型: func buildStringTrie(words: [String]) -> Trie { let emptyTrie = Trie() return words.reduce(emptyTrie) { trie, word in trie .insert(Array(word.characters)) } } 然后,我们通过调⽤之前定义的 autocomplete 函数,并将结果转换回字符串,就能得到⼀组 经过我们⾃动补全的单词了。注意我们在每个结果前拼接输⼊字符串的⽅式,这么做是因为 autocomplete 函数的返回没有包含相同的前缀,只返回了单词剩下的部分: func autocompleteString(knownWords: Trie, word: String) -> [String] { let chars = Array(word.characters) let completed = knownWords.autocomplete(chars) return completed.map { chars in word + String(chars) } } 为了测试我们的函数,我们可以使⽤⼀个简单的单词列表,创建⼀颗字典树,然后列出⾃动补 全的选项: let contents = ["cat","car","cart","dog"] let trieOfWords = buildStringTrie(contents) autocompleteString(trieOfWords, word: "car") /** 结果为: ["car", "cart"] */ 眼下,我们的接⼝只允许添加数组。创建另⼀版 insert 函数,以⽀持我们插⼊任意的集合类型 也很容易。类型签名可能会更复杂,但是函数内部的实现其实⼤同⼩异。 func insert (key: Seq) -> Trie 本章还提供了⼀个⽰例项⽬ (可以在 GitHub 上找到),项⽬会加载 /usr/share/dict/words 路径 下所有的单词并通过上⽂的 buildStringTrie 将它们构造为⼀棵字典树。构造⼀个拥有 250,000 个单词的字典树需要耗费相当⼀段时间。不过我们可以利⽤这个单词列表已被排序的特点,来 重写构建函数对其进⾏优化。⽽且,这个操作是⾼度可并⾏的;将单词表分为从 a 到 m 与从 n 到 z 两部分,并⾏构建,然后合并结果,也是可以的。 使 Trie 数据类型遵守 SequenceType 协议⾮常简单,且这会让其获得很多函数式特性:协议⾃ 动地为元素提供了⼀些函数⽐如 contains, lter ,map 和 reduce。我们可以在⽣成器与迭代 器⼀章中看到 SequenceType 协议更详细的细节。 讨论 我们在本章中列举了两个例⼦,使⽤枚举和结构体编写了⾼性能的不可变数据类型。在 Chris Okasaki 所著的《纯函数式数据结构》(1999) 中,还有很多其它的范例,这本书也是该主题下 的标准参考书⽬之⼀。感兴趣的读者或许也会想阅读 Ralf Hinze 与 Ross Paterson 的关于 nger trees(2006) 的论述,这是⼀个可以满⾜诸多场景的通⽤纯函数式数据结构。最后, StackOverow 中有⼀份极好的清单,列出了该领域近些年来⼤部分的研究成果。 案例研究:图表 10 在本章中,我们会看到⼀种描述图表的函数式⽅式,并讨论如何利⽤ Core Graphics 来绘制它 们。通过对 Core Graphic 进⾏⼀层函数式的封装,可以得到⼀个更简单且易于组合的 API。 绘制正方形和圆 想象⼀下该如何绘制图 10.1 中的图表。在 Core Graphic 中,可以通过以下的代码来实现: 图 10.1: 简易图表 NSColor.blueColor().setFill() CGContextFillRect(context, CGRectMake(0.0, 37.5, 75.0, 75.0)) NSColor.redColor().setFill() CGContextFillRect(context, CGRectMake(75.0, 0.0, 150.0, 150.0)) NSColor.greenColor().setFill() CGContextFillEllipseInRect(context, CGRectMake(225.0, 37.5, 75.0, 75.0)) 这段代码虽然短⼩精悍,但却有⼀点难以维护。⽐如,该如何像图 10.2 ⼀样添加⼀个额外的圆 进去呢? 图 10.2: 添加额外的圆 我们可能需要添加⼀段绘制圆的代码,还要更新位于该圆形右边其它图形的代码来移动它们。 在 Core Graphic 中,我们总是在描述如何绘制物体。在本章中,我们将为图表构建⼀个库,以 允许我们表达想画的是什么。举个例⼦,第⼀个图表可以像这样表达: let blueSquare = square(side: 1).ll (. blueColor()) let redSquare = square(side: 2).ll (. redColor()) let greenCircle = circle(diameter: 1). ll (. greenColor()) let example1 = blueSquare ||| redSquare ||| greenCircle 添加第⼆个圆只需要简单地修改最后⼀⾏代码: let cyanCircle = circle(diameter: 1). ll (. cyanColor()) let example2 = blueSquare ||| cyanCircle ||| redSquare ||| greenCircle 上⾯的代码描述了⼀个相对边⻓为 1 的蓝⾊正⽅形。红⾊正⽅形的边⻓是它的两倍 (相对尺⼨ 为 2)。通过使⽤运算符 ||| ,可以将相邻的正⽅形和圆依次排列,继⽽组合为图表。修改这个图 表⾮常简单,且不⽤再去考虑计算边框或是移动周围的部分。这个例⼦描述了应该画什么,⽽ 不是如何画出来。 在函数式思想这⼀章中,我们已经构造了⼀些简易函数的组合来表⽰区域。尽管在讲解函数式 的编程概念时这样做很有效,但这个思路有⼀个硬伤:我们⽆法得知区域是如何被构造的 —— 唯⼀能知道的只是⼀个点是否在这个区域中。 本章会更进⼀步:不同于⽴刻执⾏绘制指令,我们会构造⼀个中间层数据结构来对图表进⾏描 述。这是⼀个⾮常强⼤的技巧;与之前区域的例⼦相反,这允许数据结构被检视,被修改,或 将其转换成为不同的格式。 图 10.3 展⽰了⼀个更复杂的⽰例图表,⼀张由同⼀个库⽣成的柱形图: Moscow Shanghai Istanbul Berlin New York 图 10.3: 柱形图 我们可以编写⼀个 barGraph 函数来处理⼀组由名称与值 (柱形的相对⾼度) 组成的多元组。对 应每个多元组中值的部分,我们会绘制⼀个合适⼤⼩的矩形,然后使⽤ hcat 函数以⽔平⽅向连 接这些矩形。最后,使⽤ --- 运算符,将⽂字依次放置在柱形下⽅: extension SequenceType where Generator.Element == CGFloat { func normalize() -> [CGFloat] { let maxVal = self.reduce(0) { max($0, $1) } return self.map { $0 / maxVal } } } func barGraph(input: [(String, Double)]) -> Diagram { let values: [CGFloat] = input.map { CGFloat($0.1) } let nValues = values.normalize() let bars = hcat(nValues.map { (x: CGFloat) -> Diagram in return rect(width: 1, height: 3 * x) . ll (. blackColor()).alignBottom() }) let labels = hcat(input.map { x in return text(x.0, width: 1, height: 0.3).alignTop() }) return bars --- labels } let cities = [ ("Shanghai", 14.01), ("Istanbul", 13.3), ("Moscow", 10.56), ("New York", 8.33), ("Berlin", 3.43) ] let example3 = barGraph(cities) 其中 normalized 函数⽤于等⽐规范所有的值,并确保最⼤值等于⼀。 核心数据结构 在我们的库中,将绘制三种类型的元素:椭圆、矩形与⽂字。利⽤枚举,可以为这三种情况定 义⼀个数据类型: enum Primitive { case Ellipse case Rectangle case Text(String) } 图表也会利⽤⼀个枚举来定义。⾸先,⼀个图表可以是⼀个有确定尺⼨的 Primitive,即椭圆、 矩形或者⽂字其中之⼀。注意,之所以将其写作 Prim,是因为在本书编写的时候,编译器还不 允许某个枚举成员的名称与另⼀个枚举的类型名称相同: case Prim(CGSize, Primitive) 接着,可以⽤两个枚举成员来表⽰⼀对左右相邻 (⽔平⽅向) 或上下相邻 (垂直⽅向) 的图表。注 意⼀个 Beside 图表是如何被递归地定义的 —— 它包含了两个相邻的图表: case Beside(Diagram, Diagram) case Below(Diagram, Diagram) 为了能定制图表的样式,我们可以为带有样式属性的图表添加⼀个枚举成员。该成员使得图表 的填充⾊可以被设置 (⽐如,填充椭圆和矩形的颜⾊)。Attribute 类型会在稍后定义: case Attributed(Attribute, Diagram) 最后的枚举成员⽤于描述对⻬⽅式。假设有⼀个⼩的矩形和⼀个⼤的矩形彼此相邻。默认情况 下,⼩矩形会在垂直⽅向被居中,如图 10.4 所⽰: 图 10.4: 垂直居中 不过通过为对⻬⽅式添加枚举成员,我们可以控制图表中较⼩部分的对⻬⽅式: case Align(CGVector, Diagram) ⽐如,图 10.5 展⽰了⼀张顶部对⻬的图表。绘制代码如下: 图 10.5: 垂直方向的对齐方式 Diagram.Align(CGVector(dx: 0.5, dy: 1), blueSquare) ||| redSquare 我们可以将 Diagram 定义为⼀个递归枚举。就像对枚举 Tree 所做得那样,需要将这个枚举标 记为 indirect: indirect enum Diagram { case Prim(CGSize, Primitive) case Beside(Diagram, Diagram) case Below(Diagram, Diagram) case Attributed(Attribute, Diagram) case Align(CGVector, Diagram) } Attribute 枚举是⼀个⽤来描述图表各类样式属性的数据结构。它现在只⽀持 FillColor,不过 将其拓展以⽀持描边、渐变、⽂字排版属性等等的样式属性并不会很⿇烦: enum Attribute { case FillColor(NSColor) } 计算与绘制 计算 Diagram 数据类型的尺⼨并不难。只是在 Beside 与 Below 条件下计算可能相对复杂些。 在 Beside 情况下,宽度等于两个图表宽度之和,⽽⾼度则等于左右图表中较⾼者的⾼度。 Below 也是以类似的⽅式进⾏计算。除此之外,其它情况只需要递归地调⽤ size: extension Diagram { var size: CGSize { switch self { case .Prim(let size, _): return size case .Attributed(_, let x): return x.size case .Beside(let l , let r ): let sizeL = l .size let sizeR = r.size return CGSizeMake(sizeL.width + sizeR.width, max(sizeL.height, sizeR.height)) case .Below(let l, let r ): return CGSizeMake(max(l.size.width, r.size.width), l .size.height + r.size.height) case .Align(_, let r ): return r.size } } } 在我们开始绘制之前,还需要再定义⼀个函数。 t 函数作⽤于⼀个给定的尺⼨值 (⽐如某个图 表的尺⼨),参数包括⼀个对⻬⽤的⽮量 (也即 Diagram 的 Align 成员的关联值),和⼀个希望将 该尺⼨值适配在内的矩形。这⾥的尺⼨值是与图表中其它的元素相对⽽⾔的。函数会等⽐地放 ⼤给定尺⼨,同时保持其⻓宽⽐: extension CGSize { func t (vector: CGVector, _ rect: CGRect) -> CGRect { let scaleSize = rect.size / self let scale = min(scaleSize.width, scaleSize.height) let size = scale * self let space = vector.size * (size - rect.size) return CGRect(origin: rect.origin - space.point, size: size) } } 为了让 t 函数中编写的计算过程更加直观,我们为 CGSize,CGPoint,和 CGVector 定义了 下列运算符: func *(l : CGFloat, r: CGSize) -> CGSize { return CGSize(width: l * r.width, height: l * r.height) } func /( l : CGSize, r: CGSize) -> CGSize { return CGSize(width: l.width / r.width, height: l .height / r.height) } func *(l : CGSize, r: CGSize) -> CGSize { return CGSize(width: l.width * r.width, height: l .height * r.height) } func -(l : CGSize, r: CGSize) -> CGSize { return CGSize(width: l.width - r.width, height: l .height - r.height) } func -(l : CGPoint, r: CGPoint) -> CGPoint { return CGPoint(x: l.x - r.x, y: l .y - r.y) } extension CGSize { var point: CGPoint { return CGPoint(x: self.width, y: self.height) } } extension CGVector { var point: CGPoint { return CGPoint(x: dx, y: dy) } var size: CGSize { return CGSize(width: dx, height: dy) } } 来试试 t 函数吧。举个例⼦,我们希望在⼀个 200x100 的矩形中适配并居中⼀个 1x1 的正⽅ 形,得出的结果如下: CGSize(width: 1, height: 1). t ( CGVector(dx: 0.5, dy: 0.5), CGRect(x: 0, y: 0, width: 200, height: 100)) /** 结果为: (50.0, 0.0, 100.0, 100.0) /* 如果要与矩形左对⻬,可以这样写: CGSize(width: 1, height: 1). t ( CGVector(dx: 0, dy: 0.5), CGRect(x: 0, y: 0, width: 200, height: 100)) /** 结果为: (0.0, 0.0, 100.0, 100.0) */ 既然我们已经可以描述图表并计算出它们的尺⼨,也就做好了绘制出它们的准备。使⽤模式匹 配可以很容易地得知要画什么。因为进⾏绘制的上下⽂总是相同的,由此可以为 CGContextRef 定义⼀个扩展。draw 函数需要两个参数:绘制的边界,与实际的图表。在给定 边界后,图表会试着利⽤之前定义的 t 函数将⾃⼰适配在边界内。举个例⼦,当绘制⼀个椭 圆的时候,我们使其居中并与边界相切: extension CGContextRef { func draw(bounds: CGRect, _ diagram: Diagram) { switch diagram { case .Prim(let size, .Ellipse ): let frame = size.t (CGVector(dx: 0.5, dy: 0.5), bounds) CGContextFillEllipseInRect(self, frame) 矩形的处理⽅式也差不多,只不过需要调⽤不同的 Core Graphics 函数。你可能会发现计算矩 形 frame 的⽅式与椭圆是相同的。虽然可以将这个步骤提取出来,再嵌套⼀层 switch 语句, 不过我们觉得下⾯的代码在图书形式下会更易读: case .Prim(let size, .Rectangle): let frame = size.t (CGVector(dx: 0.5, dy: 0.5), bounds) CGContextFillRect(self, frame) 在这个库的当前版本中,所有的⽂字都是固定⼤⼩的系统字体。如果想要对其进⾏配置的话, 不论是添加⼀个属性,还是修改 Text 图元,都是可⾏的选择。不过在当前版本下,绘制⽂字的 ⽅式如下: case .Prim(let size, .Text(let text )): let frame = size.t (CGVector(dx: 0.5, dy: 0.5), bounds) let font = NSFont.systemFontOfSize(12) let attributes = [NSFontAttributeName: font] let attributedText = NSAttributedString(string: text, attributes: attributes) attributedText.drawInRect(frame) 我们唯⼀⽀持的属性是填充⾊。为额外的属性提供⽀持很容易,为求简短,在此不作考虑。要 绘制⼀个带有 FillColor 属性的图表,需要保存当前的图形状态,设置填充⾊,绘制出图表,然 后恢复图形状态: case .Attributed(.FillColor (let color), let d): CGContextSaveGState(self) color.set() draw(bounds, d) CGContextRestoreGState(self) 绘制两个相邻的图表,需要先计算出它们各⾃的 frame。我们为 CGRect 增加了⼀个 split 函 数,⽤于根据某个⽐值 (在这⾥,我们选择⽤左侧图表的相对尺⼨) 来分割⼀个 CGRect。然后 根据两个图表的 frame 对它们进⾏绘制: case .Beside(let left , let right ): let (lFrame, rFrame) = bounds.split( left .size.width/diagram.size.width, edge: .MinXEdge) draw(lFrame, left) draw(rFrame, right) split 函数定义如下: extension CGRect { func split (ratio: CGFloat, edge: CGRectEdge) -> (CGRect, CGRect) { let length = edge.isHorizontal ? width : height return divide(length * ratio, fromEdge: edge) } } extension CGRectEdge { var isHorizontal: Bool { return self == .MaxXEdge || self == .MinXEdge; } } Below case 分⽀也⼀样,只不过不再是⽔平地分割 CGRect,⽽是垂直⽅向。这些代码是为了 运⾏在 Mac 上⽽编写的,也所以,其绘制顺序是先 bottom 后 top (不同于 UIKit,Cocoa 坐标 系的原点在左下⽅): case .Below(let top, let bottom): let (lFrame, rFrame) = bounds.split( bottom.size.height/diagram.size.height, edge: .MinYEdge) draw(lFrame, bottom) draw(rFrame, top) 最后⼀个 case 分⽀是对⻬图表。在这⾥,我们可以复⽤之前定义的 t 函数来计算新的边界来 适配图表: case .Align(let vec, let diagram): let frame = diagram.size.t(vec, bounds) draw(frame, diagram) } } } 现在,我们已经定义出了这个库的核⼼部分。所有其它的元素都可以基于以上这些图元来构建。 创建视图与 PDF 我们可以创建⼀个 NSView 的⼦类⽤于实现绘制,在使⽤ playground ,或是你想要在 Mac 应 ⽤中绘制这些图表时,这个⼦类会很实⽤: class Draw: NSView { let diagram: Diagram init (frame frameRect: NSRect, diagram: Diagram) { self.diagram = diagram super.init(frame:frameRect) } required init(coder: NSCoder) { fatalError("NSCoding not supported") } override func drawRect(dirtyRect: NSRect) { guard let context = NSGraphicsContext.currentContext() else { return } context.cgContext.draw(self.bounds, diagram) } } 既然我们已经有了⼀个 NSView,那为这些图表输出⼀份 PDF 也会很容易。计算尺⼨之后,只 需要使⽤ NSView 的 dataWithPDFInsideRect ⽅法,就可以得到 PDF 的数据。作为⼀个例⼦, 上述代码很好的展⽰了如何将已有的⾯向对象代码封装在⼀个函数式层中: extension Diagram { func pdf(width: CGFloat) -> NSData { let height = width * (size.height / size.width) let v = Draw(frame: NSMakeRect(0, 0, width, height), diagram: self) return v.dataWithPDFInsideRect(v.bounds) } } 额外的组合算子 为了更容易地构建图表,添加⼀些额外的函数 (也称作组合算⼦ (Combinator)) 会是个不错的选 择。这在函数式库中是⼀种很普遍的模式:选定⼀⼩部分核⼼的数据类型和函数,然后在它们 之上构建⼀些便利函数。举个例⼦,对于矩形,圆形,⽂字,正⽅形,我们可以定义如下的便 利函数: func rect(width width: CGFloat, height: CGFloat) -> Diagram { return .Prim(CGSizeMake(width, height), .Rectangle) } func circle(diameter diameter: CGFloat) -> Diagram { return .Prim(CGSizeMake(diameter, diameter), .Ellipse) } func text(theText: String, width: CGFloat, height: CGFloat) -> Diagram { return .Prim(CGSizeMake(width, height), .Text(theText)) } func square(side side: CGFloat) -> Diagram { return rect(width: side, height: side) } 事实证明,根据这种思路定义的运算符,会使⽔平或垂直地组合图表变得异常⽅便,且让代码 更加易读。它们只是对 Beside 和 Below 的封装: inx operator ||| { associativity left } func ||| ( l : Diagram, r: Diagram) -> Diagram { return Diagram.Beside(l, r) } inx operator --- { associativity left } func --- ( l : Diagram, r: Diagram) -> Diagram { return Diagram.Below(l, r) } 我们还可以拓展 Diagram 类型,添加填充和对⻬的⽅法。这些⽅法也可以被定义为框架的顶层 (top-level) 函数。这是⼀个⻛格问题:两者在功能上并没有太⼤区别: extension Diagram { func ll (color: NSColor) -> Diagram { return .Attributed(.FillColor (color), self) } func alignTop() -> Diagram { return .Align(CGVector(dx: 0.5, dy: 1), self) } func alignBottom() -> Diagram { return .Align(CGVector(dx: 0.5, dy: 0), self) } } 最后,我们可以定义⼀个空图表和⽔平连接⼀组图表的⽅式。只需要使⽤数组的 reduce 函数 就可以实现: let empty: Diagram = rect(width: 0, height: 0) func hcat(diagrams: [Diagram]) -> Diagram { return diagrams.reduce(empty, combine: |||) } 通过添加这些⼩巧的辅助函数,我们拥有了⼀个强⼤的图表绘制库。 讨论 本章代码的灵感来源于 Haskell 的图表库 (Yorgey 2012)。即便我们已经可以绘制简单的图表, 本章所展⽰的库还是有很多可以改进和拓展的⽅⾯。有⼀些功能仍未添加但却易于为之。⽐如, 添加更多的属性和⻛格选项应该是顺⼿拈来的。稍微复杂⼀些的,可能是添加⼀些转换功能 (⽐ 如旋转),不过这也确确实实是可⾏的。 当我们将本章中构建的库与第⼆章中的库进⾏对⽐时,可以看到很多相似点。两者都是针对某 个问题领域 (区域和图表),并且创建了⼀个⼩巧的函数库来描述这个领域。两个库都通过函数 提供了⼀个⾼度可组合的接⼝。这两个库都定义了⼀种领域特定语⾔ (domain-specic language,简称 DSL),并将其嵌⼊在 Swift 中。每种 DSL 都具有针对性,它们是⽤于解决特定 问题的⼩型编程语⾔。 你可能已经接触过很多 DSL,⽐如正则表达式,SQL,或者 HTML —— 这些语⾔都不是通⽤⽬ 的的编程语⾔,它们不⽤于编写任何应⽤,⽽是更聚焦于解决某种特定类型的问题。正则表达 式⽤于描述⽂本规则或词法分析,SQL ⽤于查询数据库,⽽ HTML ⽤于描述⽹⻚中的内容。 然⽽,我们在本书中构建的这两种 DSL 之间有⼀个很重要的区别:在函数式思想⼀章中,我们 创建的函数根据每⼀个位置返回⼀个布尔值。⽽为了绘制图表,我们构建了⼀个中间结构,那 就是 Diagram 枚举。浅嵌⼊的 DSL 在像 Swift 这样的通⽤⽬的的编程语⾔中不会创建中间数据 结构。相反,深嵌⼊则明确地创建了⼀个中间数据结构,就像本章中编写的 Diagram 枚举那样。 术语 “嵌⼊” 是指⽤于区域或图表的 DSL 是如何被嵌⼊进 Swift 当中的。它们都有各⾃的优势。 ⼀个浅嵌⼊ DSL 可以更容易被编写,执⾏开销更少,且更容易⽤新的函数进⾏拓展。⽽在使⽤ 深嵌⼊时,优点则在于我们很容易对整体结构进⾏分析,对它进⾏转换,或者为中间数据结构 指定不同的含义。 如果我们想换⽤深嵌⼊重写第⼆章中的 DSL,可能需要定义⼀个枚举来表⽰库中不同的函数。 枚举成员可以是某个基础区域,⽐如圆或者正⽅形,也可以是某个合成的区域,⽐如这些基础 区域组合形成的交集或并集。接着,就可以使⽤各种⽅式对这些区域进⾏分析和计算:⽣成图 ⽚,检查区域是否是⼀个基础组件,确定某个给定的点是否在区域中,或是在该中间数据结构 上执⾏任意的计算。 将本章的图表库重写为浅嵌⼊则复杂些。中间数据结构可以被检视,修改和转换。要定义为⼀ 个浅嵌⼊,我们可能需要对每个希望在 DSL 中⽀持的操作中直接调⽤ Core Graphics。⽐起先 创建⼀个中间结构,直接对绘制操作进⾏组合会困难得多,毕竟只需要⼀次渲染,图表就会被 完全合并。 生成器和序列 11 在本章中,我们将关注点放在了⽣成器 (Generators) 和序列 (Sequences) 上。它们组成了 Swift 中 for 循环的基础体系,同时也是我们在接下来章节中展⽰的解析库的基础部分。 生成器 在 Objective-C 和 Swift 中,我们常常使⽤数据类型 Array 来表⽰⼀组有序元素。虽然数组简 单⽽⼜快捷,然⽽总有些问题并不适合⽤数组来解决。举个例⼦,在总数接近⽆穷的时候,你 可能不想对数组对所有的元素进⾏计算;或者你并不想使⽤全部的元素。在这些情况下,你可 能会更希望使⽤⽣成器。 为了说明问题,我们会⾸先利⽤与数组运算相似的例⼦,使你觉得⽣成器或许会是更好的选择。 Swift 的 for 循环可以被⽤于迭代数组元素: for x in xs { // do something with x } 在这样⼀个 for 循环中,数组会被从头遍历到尾。不过,如果你想要⽤不同的顺序对数组进⾏ 遍历呢?这时,⽣成器就可能派上⽤场。 从概念上来说,⼀个⽣成器是每次根据请求⽣成数组新元素的 “过程”。⼀个⽣成器可以是遵守 以下协议的任何类型: protocol GeneratorType { typealias Element func next() -> Element? } 这个协议需要⼀个由 GeneratorType 定义的关联类型:Element。还有⼀个⽤于产⽣新元素的 next ⽅法,如果新元素存在就返回元素本⾝,反之则返回 nil。 举个例⼦,下⽂的⽣成器会从数组的末尾开始⽣成序列值,⼀直到 0。Element 的类型可以由 next 函数推断出来,我们不必显式地指定: class CountdownGenerator: GeneratorType { var element: Int init(array: [T]) { self.element = array.count - 1 } func next() -> Int? { return self.element < 0 ? nil : element-- } } 我们声明了⼀个输⼊参数为数组的构造⽅法,然后使⽤数组的最后⼀个合法序列值 (index) 初 始化 element 。 我们可以使⽤ CountdownGenerator 来倒序地遍历数组: let xs = ["A","B","C"] let generator = CountdownGenerator(array: xs) while let i = generator.next() { print("Element \(i) of the array is \(xs[i ])") } Element 2 of the array is C Element 1 of the array is B Element 0 of the array is A 尽管这个⼩例⼦看起来有点⼩题⼤做,可⽣成器却封装了数组序列值的计算。如果你想要⽤另 ⼀种⽅式排序序列值,我们只需要更新⽣成器,⽽不必再修改这⾥的代码。 在某些情况下,⽣成器并不需要⽣成 nil 值。⽐如,我们可以定义⼀个⽣成器⽤来⽣成 “⽆数 个” ⼆的幂值 (直到该值变为某个极⼤值,致使 NSDecimalNumber 溢出): class PowerGenerator: GeneratorType { var power: NSDecimalNumber = 1 let two: NSDecimalNumber = 2 func next() -> NSDecimalNumber? { power = power.decimalNumberByMultiplyingBy(two) return power } } 我们可以使⽤ PowerGenerator 来检视增⻓中的⼤数组序列值,⽐如,在实现⼀个在每次迭代 都为数组序列值乘以⼆的指数搜索算法时我们就需要这么做。 我们也可能想使⽤ PowerGenerator 做⼀些完全不同的事情。假设我们希望在⼆的幂值中搜索 ⼀些有趣的值。ndPower 函数带有⼀个 NSDecimalNumber -> Bool 类型的 predicate 参数 并返回符合该条件的最⼩值: extension PowerGenerator { func ndPower(predicate: NSDecimalNumber -> Bool) -> NSDecimalNumber { while let x = next() { if predicate(x) { return x } } return 0 } } 我们可以使⽤ ndPower 函数来计算⼆的幂值中⼤于 1000 的最⼩值: PowerGenerator().ndPower { $0.integerValue > 1000 } /** 结果为: 1024 */ 迄今为⽌,我们看到的⽣成器都⽤于⽣成数字元素,但这并不是必须的。我们也可以编写⽣成 其他类型值的⽣成器。⽐如,下⾯的⽣成器会⽣成⼀组字符串,与某个⽂件中以⾏为单位的内 容相对应: class FileLinesGenerator: GeneratorType { typealias Element = String var lines: [String] = [] init (lename: String) throws { let contents: String = try String(contentsOfFile: lename) let newLine = NSCharacterSet.newlineCharacterSet() lines = contents .componentsSeparatedByCharactersInSet(newLine) } func next() -> Element? { guard !lines.isEmpty else { return nil } let nextLine = lines.removeAtIndex(0) return nextLine } } 通过这种定义⽣成器的⽅式,我们将数据的⽣成与使⽤分离开来。⽣成过程可能会涉及到打开 ⼀个⽂件或是⼀个 URL,并且会处理过程中抛出的错误。将这些隐藏在⼀份简单的⽣成器协议 之后,可以确保代码在操作被⽣成的数据时不必再去考虑这些问题。 基于为⽣成器定义的协议,我们也编写了⼀些适⽤于所有⽣成器的泛型函数。举个例⼦,之前 的 ndPower 函数的泛型版本如下: extension GeneratorType { mutating func nd(predicate: Element -> Bool) -> Element? { while let x = self.next() { if predicate(x) { return x } } return nil } } nd 函数现在适⽤于任意的⽣成器。最有趣的事情是它的类型签名,由于调⽤了 next,⽣成器 可能会被这个查找函数修改,所以我们需要在类型声明中添加 mutating 标注。查询条件应当 是⼀个可以将⽣成元素映射为 Bool 值的函数。在 nd 的类型签名中,我们可以引⽤⽣成器的 关联类型 Element。最后,注意我们查询⼀个符合条件的值是可能失败的。为此,nd 会返回 ⼀个可选值,在⽣成器被耗尽时返回 nil。 层级式的组合⽣成器也是可⾏的。⽐如,你可能希望限制⽣成元素的个数,缓冲⽣成的值,或 是编码已⽣成的数据。这⾥有个⼩例⼦,我们构建了⼀个⽣成器转换器,它可以⽤参数中的 limit 值来限制只获取对应个数的⽣成器 G 所⽣成的结果: class LimitGenerator: GeneratorType { var limit = 0 var generator: G init ( limit : Int, generator: G) { self. limit = limit self.generator = generator } func next() -> G.Element? { guard limit >= 0 else { return nil } limit-- return generator.next() } } 在填充固定⼤⼩的数组,或是以某种⽅式缓冲已⽣成的元素时,这样的⽣成器可能会很有⽤。 在编写⽣成器时,为每个⽣成器引⼊⼀个新的类有时是⼀件很繁琐的事情。Swift 提供了⼀个 简单的 AnyGenerator 类,其中的元素类型是⼀个泛型。该类可以通过传⼊⼀个 next 函数来进⾏初始化: class AnyGenerator: GeneratorType, SequenceType { init (next: () -> Element?) ... 我们会在在稍后提供完整的 AnyGenerator 定义。这⾥想指出的是,AnyGenerator 不仅实现了 GeneratorType 协议,也实现了我们会在下⼀节进⾏讲解的 SequenceType。 使⽤ AnyGenerator 可以使我们更为简短地定义⽣成器。⽐如,我们可以像下⾯的代码⼀样重 写 CountdownGenerator: extension Int { func countDown() -> AnyGenerator { var i = self return anyGenerator { i < 0 ? nil : i-- } } } 我们甚⾄可以依据 AnyGenerator 来定义能够对⽣成器进⾏操作和组合的函数。⽐如,我们可 以拼接两个基础元素类型相同的⽣成器,代码如下: func + (var rst : G, var second: H) -> AnyGenerator { return anyGenerator { rst.next() ?? second.next() } } 返回的⽣成器会先读取 rst ⽣成器的所有元素;在该⽣成器被耗尽之后,则会从 second ⽣成 器中⽣成元素。如果两个⽣成器都返回 nil,该合成⽣成器也会返回 nil。 序列 ⽣成器为 Swift 另⼀个协议提供了基础类型,这个协议就是序列。⽣成器提供了⼀个 “单次触 发” 的机制以反复地计算出下⼀个元素。这种机制不⽀持返查或重新⽣成已经⽣成过的元素, 我们想要做到这个的话就只能再创建⼀个新的⽣成器。协议 SequenceType 则为这些功能提供 了⼀组合适的接⼝: protocol SequenceType { typealias Generator: GeneratorType func generate() -> Generator } 每⼀个序列都有⼀个关联的⽣成器类型和⼀个创建新⽣成器的⽅法。我们可以据此使⽤该⽣成 器来遍历序列。举个例⼦,我们可以使⽤ CountdownGenerator 定义⼀个序列,⽤于⽣成某个 数组的⼀系列倒序序列值: struct ReverseSequence: SequenceType { var array: [T] init (array: [T]) { self.array = array } func generate() -> CountdownGenerator { return CountdownGenerator(array: array) } } 每当我们希望遍历 ReverseSequence 结构体中存储的数组时,我们可以调⽤ generate ⽅法来 ⽣成⼀个需要的⽣成器。下⾯的例⼦展⽰了如何将上述的⽚段组合在⼀起: let reverseSequence = ReverseSequence(array: xs) let reverseGenerator = reverseSequence.generate() while let i = reverseGenerator.next() { print("Index \( i ) is \(xs[i ])") } /** 打印: Index 2 is C Index 1 is B Index 0 is A */ 对⽐之前仅仅使⽤⽣成器的例⼦,同⼀个序列可以被第⼆次遍历 —— 为此我们只需要调⽤ generate 来⽣成⼀个新的⽣成器就可以了。通过在 SequenceType 的定义中封装⽣成器的创 建过程,开发者在使⽤序列时不必再担⼼潜在的⽣成器创建问题。这与⾯向对象理念中的将使 ⽤和创建进⾏分离的思想是⼀脉相承的,代码亦由此具有了更⾼的内聚性。 Swift 在处理序列时有⼀个特别的语法。不同于创建⼀个序列的关联⽣成器,你可以编写⼀个 for-in 循环。⽐如,我们也可以将以上的代码⽚段写成这样: for i in ReverseSequence(array: xs) { print("Index \( i ) is \(xs[i ])") } /** 打印: Index 2 is C Index 1 is B Index 0 is A */ 实际上,Swift 做的只是使⽤ generate ⽅法⽣成了⼀个⽣成器,然后重复地调⽤其 next 函数 直到返回 nil。 之前的 CountdownGenerator 中⽐较明显的缺点是,尽管我们可能对⼀个数组中关联的元素更 感兴趣,但它却只⽣成了数字。不过好消息是,不单单是数组,在序列上也有标准的 map 和 lter 函数: public protocol SequenceType { public func map( @noescape transform: (Self.Generator.Element) throws -> T) rethrows -> [T] public func lter ( @noescape includeElement: (Self.Generator.Element) throws -> Bool) rethrows -> [Self.Generator.Element] } 想倒序⽣成数组中的元素,我们可以使⽤ map 来映射 ReverseSequence: let reverseElements = ReverseSequence(array: xs).map { xs[$0] } for x in reverseElements { print("Element is \(x)") } /** 打印: Element is C Element is B Element is A */ 类似地,我们当然也会想从序列中过滤掉某些元素。 值得指出的是,这些 map 和 lter 函数不会返回新的序列,⽽是遍历序列来⽣成⼀个数组。数 学家们或许是据此才反对将这样的操作称之为 map (映射) 的,毕竟它们⽆法原封不动地还原之 前的结构 (⼀个序列)。⽤于⽣成序列的 map 和 lter 也是存在的。它们被定义为 LazySequence 类的拓展。LazySequence 是对标准序列的封装,可以通过序列的⼀个 lazy 属 性获得: extension SequenceType { public var lazy: LazySequence { get } } 如果你需要映射或过滤⼀个序列,⽽该序列可能会产⽣⽆穷多个结果,或是包含很多你并不感 兴趣的元素时,请务必使⽤ LazySequence ⽽不是 Sequence。否则可能会导致你的程序⽆法 完结,或者花费的时间⽐预期多得多。 案例研究:遍历二叉树 为了讲解序列和⽣成器,我们来考虑定义⼀个⼆叉树的遍历⼯具。重新来看之前在第九章中定 义的⼆叉树: indirect enum BinarySearchTree { case Leaf case Node(BinarySearchTree, Element, BinarySearchTree) } 在定义⼀个⽣成树中元素的⽣成器之前,我们需要先定义⼀个辅助函数。在 Swift 的标准库中, 有⼀个 GeneratorOfOne 结构体可以⽤于将某个可选值封装为⼀个⽣成器: struct GeneratorOfOne: GeneratorType, SequenceType { init (_ element: Element?) // ... } 给定⼀个可选值元素,它会⽣成⼀个基于该元素的序列 (仅当该元素不为 nil 时): let three: [Int] = Array(GeneratorOfOne(3)) let empty: [Int] = Array(GeneratorOfOne(nil)) 为了⽅便,我们会为 GeneratorOfOne 构建⼀个⼩巧的封装函数: func one(x: T?) -> AnyGenerator { return anyGenerator(GeneratorOfOne(x)) } 我们可以使⽤函数 one 与之前拼接⽣成器的运算符 + ⼀起来⽣成⼆叉树元素的序列。⽐如,遍 历⼯具 inOrder 依次访问了左⼦树、根节点和右⼦树: extension BinarySearchTree { var inOrder: AnyGenerator { switch self { case .Leaf: return anyGenerator { return nil } case .Node(let left, let x, let right ): return left .inOrder + one(x) + right.inOrder } } } 如果树中没有元素,我们会返回⼀个空⽣成器。如果树有⼀个节点,我们会使⽤⽣成器的拼接 运算符,将两个递归调⽤与该根节点储存的值拼接起来,作为结果返回。 案例研究:优化 QuickCheck 的范围收缩 在这⼀节,我们准备了⼀个篇幅略⻓的序列定义案例,即改进我们在QuickCheck 中实现的 Smaller 协议。在之前,这个协议的定义是这样的: protocol Smaller { func smaller() -> Self? } 我们之前使⽤ Smaller 协议去判断和收缩我们测试中发现的反例。smaller 函数会被重复地调 ⽤来⽣成⼀个更⼩的值;如果这个值依然⽆法通过测试,这意味着存在⼀个⽐之前 “更好” 的反 例。我们为数组定义的 Smaller 实例只是尝试着重复地移除数组的第⼀个元素: extension Array: Smaller { func smaller() -> [T]? { guard !self.isEmpty else { return nil } return Array(dropFirst()) } } 尽管在某些情况下,这确实有助于收缩反例,但依旧有很多不同的途径可以⽤于收缩数组。计算 出所有可能的⼦数组是⼀个⾮常昂贵的操作。对于⼀个⻓度为 n 的数组,会有 2^n 个可能的⼦ 数组,它们可能是也可能不是符合条件的反例 —— ⽣成和测试这些⼦数组并不是⼀个好主意。 不同于之前,我们会展⽰如何使⽤⽣成器来⽣成⼀系列更⼩的值。接着,我们就据此修改我们 的 QuickCheck 库来使⽤以下协议: protocol Smaller { func smaller() -> AnyGenerator } 在 QuickCheck 中发现了⼀个反例时,我们可以在⼀系列更⼩值中重复运⾏我们的测试,直到 我们找到⾜够⼩的反例。我们只需要再做⼀件事,为数组 (和其他我们可能希望收缩的类型) 编 写⼀个 smaller 函数。 ⾸先,不同于只移除数组的第⼀个元素,我们将计算出⼀系列数组,每⼀个新数组都被移除了 ⼀个元素。这并不会⽣成所有可能的⼦列表,⽽是⽣成⼀个以数组为元素的序列,其中的每个 数组都⽐原始数组少⼀个元素。利⽤ AnyGenerator,我们可以定义这样的⼀个函数如下: extension Array { func generateSmallerByOne() -> AnyGenerator<[Element]> { var i = 0 return anyGenerator { guard i < self.count else { return nil } var result = self result.removeAtIndex(i) i++ return result } } } generateSmallerByOne 函数持续地追踪变量 i。当请求下⼀个元素时,函数会检查 i 是否⼩于 数组的⻓度。如果⼩于,就计算⼀个新数组 result,并使 i ⾃增。如果已经访问到原始数组的 末尾,则返回 nil。 现在,我们可以看到这个函数返回所有可能的数组,每⼀个数组都⽐原始数组少⼀个元素: [1, 2, 3].generateSmallerByOne() 不幸地是,这个调⽤并没有⽣成期望的结果 —— 返回值被定义为⼀个 AnyGenerator<[Int]>, ⽽我们想看到的是⼀个以数组为元素的数组。好消息是,Array 有个构造⽅法带有⼀个 SequenceType 类型的参数。利⽤这个构造⽅法,我们可以测试⽣成器如下: Array([1, 2, 3].generateSmallerByOne()) /** 结果为: [[2, 3], [1, 3], [1, 2]] */ 利⽤ (在第九章定义的) decompose 函数,我们可以重新为数组定义 smaller 函数。如果想为之 前 generateSmallerByOne 函数的计算结果设计⼀个递归的伪代码定义,我们需要做到以下⼏ 点: → 如果数组为空,返回 nil → 如果数组可以被分割为 head 与 tail 两部分,我们可以遵守以下规则并递归地计算剩余 地⼦数组: → 后⼀部分是⼀个⼦数组 → 如果我们为 tail 的所有⼦数组⾸位拼接 head,可以计算出原始数组的⼦数组 我们可以根据已经定义的函数将这些规则直接翻译为 Swift: extension Array { func smaller1() -> AnyGenerator<[Element]> { guard let (head, tail ) = self.decompose else { return one(nil)} return one(tail) + Array<[Element]>(tail.smaller1()).map { smallerTail in [head] + smallerTail }.generate() } } 我们现在已经做好了测试新版函数的准备,并且验证可知,它与 generateSmallerByOne 的返 回值是相同的: Array([1, 2, 3].smaller1()) /** 结果为: [[2, 3], [1, 3], [1, 2]] */ 还有最后⼀个值得做的改进:那就是另⼀种可以判断和减⼩ QuickCheck 所找到的反例的⽅式。 不同于只是移除元素,我们也会希望尝试让元素⾃⼰进⾏缩⼩。想实现这个功能,我们需要多 处理⼀种情况,即 Element 也遵守 Smaller 协议: extension Array where Element: Smaller { func smaller() -> AnyGenerator<[Element]> { guard let (head, tail ) = self.decompose else { return one(nil)} let gen1 = one(tail).generate() let gen2 = Array<[Element]>(tail.smaller()).map { xs in [head] + xs }.generate() let gen3 = Array(head.smaller()).map { x in [x] + tail }.generate() return gen1 + gen2 + gen3 } } 可以检查新 smaller 函数的结果: Array([1, 2, 3].smaller()) /** 结果为: [[2, 3], [1, 3], [1, 2], [1, 2, 2], [1, 1, 3], [0, 2, 3]] */ 除了⽣成⼦列表之外,新版的 smaller 函数也可以⽤于⽣成元素值更⼩的数组。 不止是 Map 与 Filter 在未来的章节⾥,我们会需要更多基于序列和⽣成器的运算。为了定义这些运算,我们需要定 义⼀个与前⽂中 AnyGenerator 类似的结构体 AnySequence。本质上,它封装了⼀个⽤于返回 ⼀个序列中⽣成器的函数。它的定义⼤概如下: struct AnySequence: SequenceType { init (_ makeUnderlyingGenerator: () -> G) func generate() -> AnyGenerator } 我们已经利⽤运算符 + 为⽣成器定义了拼接运算。⼀开始定义序列的拼接⽅法,可能会像下⽂ 中的定义: func +(l: AnySequence, r: AnySequence) -> AnySequence { return AnySequence(l.generate() + r.generate()) } 这个定义调⽤了两个参数序列的 generate ⽅法,然后将经过拼接得到的⽣成器指定给序列。不 幸地是,实际效果并不符合预期。来看看下⾯的例⼦: let s = AnySequence([1, 2, 3]) + AnySequence([4, 5, 6]) print("First pass:") for x in s { print(x) } print("Second pass:") for x in s { print(x) } 我们构建了⼀个包含元素 [1, 2, 3, 4, 5, 6] 的序列,然后遍历两次,打印出我们即将得到的元 素。可能会感到⼀点点讶异,这段代码⽣成了如下的输出: 第一次输出:123456 第二次输出: 第⼆个 for 循环没有输出任何东西 — 发⽣什么了?其实问题出在对于序列进⾏连接的定义上。 我们将两个⽣成器进⾏了组装 l .generate() + r.generate()。这个新的⽣成器在上⾯例⼦的第 ⼀次循环中将会⽣成所有我们需要的元素。然⽽,在所合成的序列进⾏第⼆次循环时,并没有 ⽣成⼀个新的⽣成器,所使⽤的依然是之前那个耗尽状态的⽣成器。 不过还好,这个问题很容易被修复。我们需要确保拼接操作的结果可以⽣成新的⽣成器。要做 到这点,我们要向 AnySequence 的构造⽅法中传⼊⼀个可以⽣成⽣成器的函数,⽽不是⼀个 固定的⽣成器: func +(l: AnySequence, r: AnySequence) -> AnySequence { return AnySequence { l.generate() + r.generate() } } 现在,我们可以多次迭代同⼀个序列。在编写我们⾃⼰的序列组合⽅法时,要确保每次调⽤ generate() 都会⽣成⼀个新的⽣成器以忽略之前的遍历操作,这点⾮常重要。 到⽬前为⽌,我们可以拼接两个序列。将⼀个序列展开为⼀组序列⼜该如何呢?在处理序列之 前,让我们试着编写⼀个 join 操作:给定⼀个 AnyGenerator>,⽣成⼀个 AnyGenerator: struct JoinedGenerator: GeneratorType { var generators: AnyGenerator> var current: AnyGenerator? init(var _ g: G) { generators = g.map(anyGenerator) current = generators.next() } mutating func next() -> Element? { guard let c = current else { return nil } if let x = c.next() { return x } else { current = generators.next() return next() } } } JoinedGenerator 维护了两部分可变状态:⼀个可选的当前⽣成器 current,和剩余的⽣成器 generators。在请求⽣成下⼀个元素的时候,如果当前⽣成器存在,则调⽤该⽣成器的 next 函 数。如果不存在,则更新当前⽣成器 current,并再次递归地调⽤ next。仅当所有⽣成器都被 耗尽后,next 函数返回 nil。 接着,我们使⽤ JoinedGenerator 来连接⼀组序列: extension SequenceType where Generator.Element: SequenceType { typealias NestedElement = Generator.Element.Generator.Element func join () -> AnySequence { return AnySequence { () -> JoinedGenerator in var generator = self.generate() return JoinedGenerator(generator.map { $0.generate() }) } } } 我们先为嵌套序列的元素定义⼀个类型别名 NestedElement。返回类型也由此修改为 AnySequence。要创建这样⼀个实例,需要利⽤⼀个返回 JoinedGenerator 类型值的函数来初始化 AnySequence。为了创建嵌套的 ⽣成器,我们调⽤ self 的 generate(),返回⼀个 AnyGenerator> 类型的值。 最后要做的,则是为所有的序列进⾏⼀次映射,并调⽤每个序列的 generate(),这恰好可以依 靠调⽤ map 来完成。 最后,我们也可以将 join 与 map 结合起来编写下⾯的 atMap 函数: extension AnySequence { func atMap (f: Element -> Seq) -> AnySequence { return AnySequence(self.map(f)).join() } } 给定⼀个以 Emlement 为元素类型的序列,以及⼀个函数 f。f 以⼀个 Element 类型的值为参 数,⽣成⼀个元素类型为 T 的序列。只要符合以上条件,我们就可以构建⼀个 T 元素的单层序 列。实现的⽅式很简单,对当前序列做⼀次 f 的映射,构建⼀个 AnySequence>,然后对其调⽤ join,就可以获得期望的 AnySequence。 现在我们已经充分掌握了序列和它⽀持的运算,我们可以继续进⾏下⼀个案例研究了:编写⼀ 个组合算⼦解析库。 案例研究:解析 器组合算子 12 解析器 (Parser) 是⾮常有⽤的⼯具:它们接收⼀组符号 (通常是⼀组字符) 并将它们转换为某种 结构。通常,解析器是由像 Bison 或 YACC 这样的外部⼯具⽣成的。不过在本章,不同于引⼊ 外部⼯具,我们将构建⼀个解析器库,它将被⽤于在后⾯的章节中⽣成我们⾃⼰的解析器。将 这个任务交由函数式语⾔来完成再合适不过。 编写⼀个解析库有⼏种⽅案。在这⾥,我们会构建⼀个解析器组合算⼦ (Parser Combinators) 库。每个解析器组合算⼦都是⼀个⾼阶函数,它接受若⼲ (在本章中被定义为函数的) 解析器作 为参数,并返回⼀个新的解析器作为结果。我们即将构建的库基本上是对⼀个 Haskell ⼯具库 (2009) 的移植,当然,也做了⼀些微调。 我们将从定义⼀些核⼼的组合算⼦开始。接着,会在此之上定义⼀些额外的便利函数。在结尾 处,我们会展⽰⼀个例⼦,解析⽐如 1+3*3 这样的算数表达式,并将结果计算出来。 核心部分 在这个库中,我们会重度使⽤序列与数组切⽚。 译者注:也因此,在本⽂中会出现⼀些未详加提及却加以引⽤的拓展⽅法来简化使⽤, 建议您下载本书 GitHub 仓库中本章的⽰例代码对照阅读。 我们将解析器定义为⼀个函数,其参数是⼀个符号数组的切⽚,函数会处理这些符号中的⼀部 分,并返回⼀个序列,其元素是⼀系列包含处理结果与剩余符号的多元组。为了让以后的编码 更为简单,我们将这样的函数封装在⼀个结构体⾥ (否则,我们可能得⼀直把这个函数完整的类 型签名写出来)。我们使解析器⽀持两种泛型:Token 和 Result: struct Parser { let p: ArraySlice -> AnySequence<(Result, ArraySlice)> } 如果不是因为不⽀持泛型,我们会更愿意为解析器类型定义⼀个类型别名。所以这个案例⾥, 我们只能使⽤结构体来作为载体。 让我们从最简单的解析器开始,先定义⼀个⽤于解析单个字⺟ "a" 的解析器。为此,我们编写 了⼀个返回字符 "a" 解析器的函数: func parseA() -> Parser 要返回单个结果,我们可以使⽤函数 one 构建⼀个只包含单个元素的序列。该函数的定义与前 ⼀章中⼀个返回单个 Generator 的函数 one 类似: func one(x: A) -> AnySequence { return AnySequence(GeneratorOfOne(x)) } 如果第⼀个字符不是 “a”,解析器将返回⼀个空序列 none() 来表⽰失败。完整的 parseA 函 数如下: func none() -> AnySequence { return AnySequence(anyGenerator { nil }) } func parseA() -> Parser { let a: Character = "a" return Parser { x in guard let (head, tail ) = x.decompose where head == a else { return none() } return one((a, tail )) } } 可以使⽤⼀个名为 testParser 的函数来测试解析器: func testParser(parser: Parser, _ input: String) -> String { var result: [String] = [] for (x, s) in parser.p(input.slice) { result += ["Success, found \(x), remainder:\(Array(s))"] } return result.isEmpty ? "Parsing failed.": result.joinWithSeparator("\n") } 这个函数会使⽤作为第⼀个参数传⼊的解析器,来解析第⼆个参数 input 对应的字符串。解析 器将可能的结果⽣成为⼀个序列,由 testParser 函数获取并返回⼀个说明结果的字符串: testParser(parseA(), "abcd") /** 结果为: Success, found a, remainder:["b", "c", "d"] */ 在上⽂的例⼦中,如果我们对⼀个不包括 “a” 的字符串运⾏解析器,就会得到⼀个失败的提 ⽰: testParser(parseA(), "test") /** 结果为: Parsing failed. */ 对函数 parseA 做⼀次抽象,使其返回⼀个⽀持任意字符的解析器并不难。我们可以向函数传 ⼊⼀个需要被解析的字符参数,仅当被解析字符串的第⼀个字符与参数字符相同时返回结果: func parseCharacter(character: Character) -> Parser { return Parser { x in guard let (head, tail ) = x.decompose where head == character else { return none() } return one((character, tail )) } } 可以对 parseCharacter 做如下调⽤: testParser(parseCharacter("t"), "test") /** 结果为: Success, found t, remainder:["e", "s", "t"] */ 我们可以再对这个⽅法做⼀次抽象,将符号类型定义为泛型。不同于检查符号是否相等,这次 我们会传⼊⼀个类型为 Token -> Bool 的函数,当该函数在处理输⼊流中⾸个 token 的结果为 true 时,我们将解析的结果返回: func satisfy(condition: Token -> Bool) -> Parser { return Parser { x in guard let (head, tail ) = x.decompose where condition(head) else { return none() } return one((head, tail )) } } 现在我们可以定义⼀个与 parseCharacter 功能类似的函数 token,不同之处只在于该函数适 ⽤于任何遵守 Equatable 协议的数据类型: func token(t: Token) -> Parser { return satisfy { $0 == t } } 选择 解析单独的符号可能会稍显鸡肋,所以我们会添加⼀些可以合并两个解析器的函数。我们要介 绍的第⼀个函数是选择运算符 <|>。该运算符可以同时使⽤左边与右边的运算对象进⾏解析。 选择运算符实现起来很简单:假定传⼊⼀个字符串,运算符运⾏左侧的解析器,提供⼀个可能 结果的序列。然后运⾏右侧解析器,也提供⼀个可能结果的序列,再将两个序列拼接起来就可 以了。要注意的是:这两个序列既可能同时为空,也可能同时包含⼤量的元素。只不过因为它 们实际的运算时机是被延迟的,所以并不会有什么影响: inx operator <|> { associativity right precedence 130 } func <|> (l: Parser, r: Parser) -> Parser { return Parser { l .p($0) + r.p($0) } } 我们可以构建⼀个同时解析 "a" 和 "b" 的解析器来测试我们的新运算符: let a: Character = "a" let b: Character = "b" testParser(token(a) <|> token(b), "bcd") /** 结果为: Success, found b, remainder:["c", "d"] */ 顺序解析 在实现了对两个解析器的合并之后,我们想实现⼀个更⼤胆的⽅案,并在之后将其拓展为更为 ⽅便和强⼤的⼯具。⾸先,我们编写⼀个 sequence 函数: func sequence(l: Parser, _ r: Parser) -> Parser 该函数返回的解析器会先使⽤左解析器来解析类型为 A 的某个值。假设我们希望解析字符串 “xyz” 中⼀个 "x" 紧接着⼀个 "y" 的部分。左侧 (查找 "x" 的) 解析器会⽣成⼀个序列,这个序 列中只包含⼀个 (result, remainder) 的多元组,就像这样: [("x","yz")] 左解析器返回的结果多元组中,剩余部分为 "yz",对剩余部分使⽤右解析器,会得到另⼀个包 含单个多元组的序列: [("y","z")] 接着,将 "x" 与 "y" 组合为⼀个新的多元组 (“x”, “y”): [ (("x","y"), "z")] 在左侧解析器返回序列中的每⼀个多元组都经历了这个过程之后,我们会得到⼀个嵌套序列: [ [ (( "x","y"), "z")]] 最后,我们将这个结构展平为⼀个单层序列,其元素是类型为 (( A, B), ArraySlice) 的 多元组。完整的 sequence 函数代码如下: func sequence(l: Parser, _ r: Parser) -> Parser { return Parser { input in let leftResults = l .p(input) return leftResults.atMap { (a, leftRest) -> [(( A, B), ArraySlice)] in let rightResults = r.p(leftRest) return rightResults.map { b, rightRest in (( a, b), rightRest) } } } } 我们可以试着依次解析 "x" 与 "y" 来测试我们的解析器: let x: Character = "x" let y: Character = "y" let p: Parser = sequence(token(x), token(y)) testParser(p, "xyz") /** 结果为: Success, found ("x", "y"), remainder:["z"] */ 改进 为了合并多个解析器并使之依次⽣效,最初的思路正如我们在上⽂中编写的 sequence 函数。 假设我们仍旧想解析上⽂的字符串 "xyz",不过这次是要依次对 "x"、"y" 与 "z" 进⾏解析。先 来试着参照之前的⽅式调⽤ sequence 函数合并三个解析器: let z: Character = "z" let p2 = sequence(sequence(token(x), token(y)), token(z)) testParser(p2, "xyz") /** 结果为: Success, found (("x", "y"), "z"), remainder:[] */ 这个⽅法并不可⾏,返回的结果是⼀个多元组 (( "x","y"),"z"),⽽不是预期的被展平的 ("x","y","z")。要解决这个问题,可以对函数 sequence 进⾏⼀点拓展,增加参数的个数,编 写⼀个合并三个解析器的函数 sequence3 : func sequence3(p1: Parser, _ p2: Parser, _ p3: Parser) -> Parser { typealias Result = ((A, B, C), ArraySlice) typealias Results = [Result] return Parser { input in let p1Results = p1.p(input) return p1Results.atMap { a, p1Rest -> Results in let p2Results = p2.p(p1Rest) return p2Results.atMap {b, p2Rest -> Results in let p3Results = p3.p(p2Rest) return p3Results.map { (c, p3Rest) -> Result in (( a, b, c), p3Rest) } } } } } let p3 = sequence3(token(x), token(y), token(z)) testParser(p3, "xyz") /** 结果为: Success, found ("x", "y", "z"), remainder:[] */ 虽然返回了预期的结果,可这种⽅式既不灵活,也不易被拓展。其实,还有另⼀种更为简便的 ⽅法来依次合并多个解析器。 ⾸先,我们需要创建⼀个不会消耗 token 的解析器,并使其返回⼀个形如 A -> B 的函数。这个 函数⽤于将其余的若⼲解析器结果转换为我们指定的形式。下⾯的⼩例⼦就是⼀个这样的解析 器: func integerParser() -> Parser Int> { return Parser { input in return one(({ x in Int(String(x ))! }, input)) } } 这个解析器并没有消耗任何符号,它返回了⼀个将字符转换为整数的函数。传⼊⼀个⾮常简单 的字符串 "3" 作为例⼦。对该传⼊值使⽤ integerParser 会返回以下序列: [ (A -> B, "3")] 再使⽤另⼀个 (⽤于解析符号的) 解析器来解析剩余的符号 "3"(由于 integerParser 并没有消耗 任何符号,剩余的符号与原始的符号是相同的): [("3","")] 现在我们只需要创建⼀个函数来合并这两个解析器,并返回⼀个新解析器,就可以使 integerParser 返回的函数被应⽤在 (前⽂提到的) 符号解析器返回的字符 "3" 上。这个函数看 起来和函数 sequence ⾮常相似 —— 函数在第⼀个解析器所返回的序列上调⽤了 atMap 得 到转换函数,第⼆个解析器会对剩余部分进⾏解析并得到另⼀个序列,最后在这个序列上使⽤ 转换函数进⾏映射就能得到最终结果。 最关键的不同点在于闭包内部并没有像 sequence 那样,将两个解析器的结果都返回,⽽是将 第⼀个解析器⽣成的函数应⽤在了第⼆个解析器的解析结果上: func combinator(l: Parser B>, _ r: Parser) -> Parser { typealias Result = (B, ArraySlice) typealias Results = [Result] return Parser { input in let leftResults = l .p(input) return leftResults.atMap { f, leftRemainder -> Results in let rightResults = r.p(leftRemainder) return rightResults.map { x, rightRemainder -> Result in (f(x), rightRemainder) } } } } 将上述过程结合在⼀起,解析结果会像这样: let three: Character = "3" testParser(combinator(integerParser(), token(three)), "3") /** 结果为: Success, found 3, remainder:[] */ 到这⾥,我们已经做好了准备来建⽴⼀个真正优雅的解析器合并机制。 我们要做的第⼀件事是将 integerParser 重构为⼀个泛型函数,它会接收⼀个参数,并返回⼀ 个总是成功的解析器,这个解析器不会消耗符号,且会将我们向函数中传⼊的参数作为解析结 果返回: func pure(value: A) -> Parser { return Parser { one((value, $0)) } } 有了这个函数,我们可以像这样重写前⼀个例⼦: func toInteger(c: Character) -> Int { return Int(String(c ))! } testParser(combinator(pure(toInteger), token(three)), "3") /** 结果为: Success, found 3, remainder:[] */ 利⽤这个机制来合并多个解析器的技巧得益于柯⾥化这⼀概念。从第⼀个解析器中返回⼀个柯 ⾥化函数,可以让我们基于这个柯⾥化函数的参数个数,多次地执⾏合并过程。就像这样: func toInteger2(c1: Character) -> Character -> Int { return { c2 in let combined = String(c1) + String(c2) return Int(combined)! } } testParser(combinator(combinator(pure(toInteger2), token(three)), token(three)), "33") /** 结果为: Success, found 33, remainder:[] */ 鉴于在代码中调⽤太多的 combinator 会使可读性下降,我们为此定义了⼀个运算符: inx operator <*> { associativity left precedence 150 } func <*>(l: Parser B>, r: Parser) -> Parser { typealias Result = (B, ArraySlice) typealias Results = [Result] return Parser { input in let leftResults = l .p(input) return leftResults.atMap { (f, leftRemainder) -> Results in let rightResults = r.p(leftRemainder) return rightResults.map { (x, y) -> Result in (f(x), y) } } } } 现在我们将之前的⽰例改写如下: testParser(pure(toInteger2) <*> token(three) <*> token(three), "33") /** 结果为: Success, found 33, remainder:[] */ 需要注意的是,我们刚刚定义的运算符 <*> 是左向优先的。这意味着运算符会先对左侧的两个 解析器进⾏运算,之后才会将运算所得的结果与右侧的解析器结合。换句话说,这个⾏为与我 们之前嵌套地调⽤ combinator 函数是完全相同的。 还有另⼀个运⽤该运算符的例⼦,通过将多个字符合并为字符串,也可以来构造⼀个字符串的 解析器: let aOrB = token(a) <|> token(b) func combine(a: Character)-> Character -> Character -> String { return { b in {c in String([a, b, c]) } } } let parser = pure(combine) <*> aOrB <*> aOrB <*> token(b) testParser(parser, "abb") /** 结果为: Success, found abb, remainder:[] */ 在第三章中我们曾经定义过⼀个对双参函数进⾏柯⾥化的函数 curry。我们可以通过定义多个 版本的 curry 函数,来⽀持不同数量参数的函数。举个例⼦,可以定义⼀个⽀持三参函数的版 本,使我们能够以另⼀种⽅式编写上例中的解析器: func curry(f: (A, B, C) -> D) -> A -> B -> C -> D{ return { x in { y in { z in f(x, y, z) } } } } let parser2 = pure(curry { String([$0, $1, $2]) }) <*> aOrB <*> aOrB <*> token(b) testParser(parser2, "abb") /** 结果为: Success, found abb, remainder:[] */ 便利组合算子 利⽤上⽂中的组合算⼦,我们已经可以解析很多有意思的语⾔。只不过,以上⽂中的⽅式编写 解析器还是有些枯燥。幸运地是,我们可以定义⼀些额外的函数来简化编码。⾸先我们将定义 ⼀个返回解析器的函数,该解析器可以从⼀个 NSCharacterSet 中解析出⼀个字符。这可以⽤ 于创建⼀个解析⼗进制数字的解析器,如下例: func characterFromSet(set: NSCharacterSet) -> Parser { return satisfy(set.member) } let decimals = NSCharacterSet.decimalDigitCharacterSet() let decimalDigit = characterFromSet(decimals) 想验证解析器 decimalDigit 的作⽤,我们可以⽤它来解析下例中被传⼊的字符串: testParser(decimalDigit, "012") /** 结果为: Success, found 0, remainder:["1", "2"] */ 接下来要编写的便利组合算⼦是⼀个 zeroOrMore 函数,它可以选择将⼀个解析器执⾏多次, 或是不执⾏: func zeroOrMore(p: Parser) -> Parser { return (pure(prepend) <*> p <*> zeroOrMore(p)) <|> pure([]) } ⽰例中的 prepend 函数可以将类型为 A 的值与⼀个形如 [A] 的数组合并为⼀个新数组: func prepend(l: A)->([A]) -> [A] { return {r in [ l ] + r} } 然⽽,如果我们尝试使⽤函数 zeroOrMore 时,我们会陷⼊⼀个死循环中。这是因为我们在返 回语句中递归地调⽤了 zeroOrMore。 不过还好,我们可以将 zeroOrMore 的递归调⽤延迟到真正需要的时候再做执⾏,并利⽤这种 ⽅式将死循环中断。要实现这个功能,需要先定义⼀个辅助函数 lazy。该函数返回⼀个仅在必 要的时候被执⾏⼀次的解析器: func lazy(f: () -> Parser) -> Parser { return Parser { f (). p($0) } } 现在我们可以⽤这个函数封装对 zeroOrMore 的递归调⽤: func zeroOrMore(p: Parser) -> Parser { return (pure(prepend) <*> p <*> lazy { zeroOrMore(p) } ) <|> pure ([]) } 我们来测试下组合算⼦ zeroOrMore, 看看它是否会⽣成多个结果吧。就像即将在本章看到的那 样,我们通常只使⽤解析器的第⼀个成功结果,⽽剩余的结果由于是被延迟解析的,所以它们 将不会被实际使⽤: testParser(zeroOrMore(decimalDigit), "12345") /** 结果为: Success, found ["1", "2", "3", "4", "5"], remainder:[] Success, found ["1", "2", "3", "4"], remainder:["5"] Success, found ["1", "2", "3"], remainder:["4", "5"] Success, found ["1", "2"], remainder:["3", "4", "5"] Success, found ["1"], remainder:["2", "3", "4", "5"] Success, found [], remainder:["1", "2", "3", "4", "5"] */ 另⼀个有⽤的组合算⼦是 oneOrMore,这是⼀个将某解析器执⾏⾄少⼀次的函数。可以借助 zeroOrMore 来定义该函数: func oneOrMore(p: Parser) -> Parser { return pure(prepend) <*> p <*> zeroOrMore(p) } 如果解析出⾄少⼀个数字,我们会得到⼀个 [Character] 类型的数组,其中每个元素都是⼀个 数字字符。要将结果数组转换为⼀个整数,我们可以先将元素类型为 Character 的数组转换为 ⼀个字符串,然后据此构造⼀个 Int 类型的值。尽管 Int 的该构造⽅法被标记为可选值,可因为 我们知道构造⼀定会成功,所以我们可以使⽤运算符 ! 强制解值: let number = pure { Int(String($0))! } <*> oneOrMore(decimalDigit) testParser(number, "205") /** 结果为: Success, found 205, remainder:[] Success, found 20, remainder:["5"] Success, found 2, remainder:["0", "5"] */ 在我们已经编写的所有代码⾥,可以看到⼀个重复出现的模式: pure(x) <*> y 实际上,由于对这种模式的使⽤太过普遍,以⾄于有必要为其定义⼀个额外的运算符。如果将 着眼点放在类型上,我们可以看到这个模式与 map 函数⾮常相似 —— 该模式接收⼀个类型为 A -> B 的函数与⼀个结果类型为 A 的解析器,并返回⼀个结果类型为 B 的解析器: inx operator { precedence 170 } func (l: A -> B, r: Parser) -> Parser { return pure(l) <*> r } 现在我们已经定义了许多有⽤的函数,是时候开始将这些函数 (所返回的解析器) 合并为真正的 解析器了。举个例⼦,如果我们希望创建⼀个解析器来计算两个整数的和,可以以下⽂的⽅式 来编写这个解析器: let plus: Character = "+" func add(x: Int)-> Character -> Int -> Int { return {_ in { y in x + y } } } let parseAddition = add number <*> token(plus) <*> number 再⼀次,我们来验证解析器的功能: testParser(parseAddition, "41+1") /** 结果为: Success, found 42, remainder:[] */ 有时候会出现这么⼀种情况,我们希望在解析⼀些东西的同时将解析结果忽略掉。就像上⽂的 加法符号⼀样,我们只需要知道它被成功解析,却并不关注解析器返回了什么样的结果。可以 定义另⼀个与运算符 <*> 功能⼗分相似的运算符 <*,只不过该运算符会将右侧解析器的解析结 果丢弃 (也所以在运算符定义时去掉了右侧的尖括号)。类似的,我们也定义了运算符 *> 来丢弃 左侧解析器的解析结果: inx operator <* { associativity left precedence 150 } func <* (p: Parser, q: Parser) -> Parser { return { x in {_ in x } } p <*> q } inx operator *> { associativity left precedence 150 } func *> (p: Parser, q: Parser) -> Parser { return {_ in { y in y } } p <*> q } 让我们再为乘法编写⼀个解析器。内容与 parseAddition 函数基本相同,只不过这次使⽤了新 运算符 <*,在解析过 "*" 后会将结果丢弃: let multiply: Character = "*" let parseMultiplication = curry(*) number <* token(multiply) <*> number testParser(parseMultiplication, "8*8") /** 结果为: Success, found 64, remainder:[] */ 一个简单的计算器 接下来,我们会拓展之前的⽰例,来解析类似于 10+4*3 的算术表达式。在计算结果时,最需 要注意的地⽅在于,乘法具有⽐加法更⾼的优先级。这是因为数学 (与程序) 中有⼀个被称为运 算符优先级的规则。在我们的解析器中描述这个规则⾮常⾃然。在这⾥具有最⾼优先级的操作 是解析作为运算单元的数字,让我们从它开始: typealias Calculator = Parser func operator0(character: Character, _ evaluate: (Int, Int) -> Int, _ operand: Calculator) -> Calculator { return curry { evaluate($0, $1) } operand <* token(character) <*> operand } func pAtom0() -> Calculator { return number } func pMultiply0() -> Calculator { return operator0("*", *, pAtom0()) } func pAdd0() -> Calculator { return operator0("+", +, pMultiply0()) } func pExpression0() -> Calculator { return pAdd0() } testParser(pExpression0(), "1+3*3") /** 结果为: Parsing failed. */ 解析为什么会失败呢? ⾸先被解析的是⼀个加法表达式。加法表达式被上⽂的 pAdd0() 函数定义为由⼀个乘法表达式, ⼀个 "+" 与另⼀个乘法表达式依次组合⽽成。⽽实际上,3*3 是⼀个乘法表达式,但 1 却只是 ⼀个数字。要修复这个问题,我们可以改写 operator 函数,使其既能解析⼀个形如 “运算对象 运算符运算对象” 的表达式,也能解析⼀个只包括单个运算对象的表达式: func operator1(character: Character, _ evaluate: (Int, Int) -> Int, _ operand: Calculator) -> Calculator { let withOperator = curry { evaluate($0, $1) } operand <* token(character) <*> operand return withOperator <|> operand } 现在,我们终于有了⼀个具有实际功能的版本: func pAtom1() -> Calculator { return number } func pMultiply1() -> Calculator { return operator1("*", *, pAtom1()) } func pAdd1() -> Calculator { return operator1("+", +, pMultiply1()) } func pExpression1() -> Calculator { return pAdd1() } testParser(pExpression1(), "1+3*3") /** 结果为: Success, found 10, remainder:[] Success, found 4, remainder:["*", "3"] Success, found 1, remainder:["+", "3", "*", "3"] */ 如果想添加更多的运算符,并将解析过程再做⼀次抽象,我们可以创建⼀个多元组的数组,在 每个多元组中包含⼀个运算符字符和⼀个实现该运算的函数,然后使⽤ reduce 函数将它们合 并到⼀个解析器⾥: typealias Op = (Character, (Int, Int) -> Int) let operatorTable: [Op] = [("*", *), ("/", /), ("+", +), ("-", -)] func pExpression2() -> Calculator { return operatorTable.reduce(number) { (next: Calculator, op: Op) in operator1(op.0, op.1, next) } } testParser(pExpression2(), "1+3*3") /** 结果为: Success, found 10, remainder:[] Success, found 4, remainder:["*", "3"] Success, found 1, remainder:["+", "3", "*", "3"] */ 然⽽,我们的解析器随着运算符的不断增加时会明显地变慢。这是因为解析器需要不断地回溯: 解析器在解析的时候,如果失败,就需要接着尝试替代⽅案。⽐如,在尝试去解析 "1+3*3" 时, 会先去尝试 "-" 运算符 (根据之前的 reduce 的顺序,减法表达式应该由加法表达式、运算符 "-" 以及另⼀个加法表达式依次组成)。式中的第⼀个加法表达式会被解析成功,但接下来却找不到 字符 "-",因此解析器会去尝试另⼀种可能性:即只有⼀个加法表达式作为运算对象的情况。如 果按照这样的⽅式进⾏解析,会有⼤量不必要的步骤被执⾏。 编写⼀个类似于上⽂的解析器⾮常简单。不过,却并不⾼效。如果我们回退⼀步来看,之前利 ⽤解析器组合算⼦定义的语法,其实可以像下⽂⼀样 (使⽤伪语法描述语⾔) 写出来 (下例中, 符号 | 表⽰或,即先对左侧进⾏运算。如左侧失败,运算右侧,如也失败,则整个表达式返回 失败,否则,返回第⼀次成功的结果): expression = min min = add "-" add | add add = div "+" div | div div = mul "/" mul | mul mul = num "*" num | num 为了移除⼤量重复表达,我们可以将语法重构为下⽂的样式 (下例中,符号 ? ⽤于修饰左侧括号 内的表达式可能出现⼀次): expression = min min = add ("-" add)? add = div ("+" div)? div = mul ("/" mul)? mul = num ("*" num)? 在我们定义新的运算符函数之前,我们先额外定义⼀版运算符 。新版的运算符同样会使右侧 的解析器进⾏解析,但会将解析结果丢弃,从⽽将左侧的运算过程保留⾄下⼀次合并运算中。 我们将新运算符定义为 (l: A, r: Parser) -> Parser { return pure(l) <* r } 同样地,我们将定义⼀个函数 optionallyFollowed,解析可能伴随着额外部分的左侧运算对象: func optionallyFollowed(l: Parser, _ r: Parser A>) -> Parser { let apply: A -> (A -> A) -> A = { x in { f in f(x) } } return apply l <*> (r <|> pure { $0 }) } 最后,我们可以定义我们的运算符函数 op 了。它会解析类型为 Calculator 的参数 operand, 以及可能随之⽽来的⼀个运算符和另⼀个 operand 参数。注意,这⾥并不能直接执⾏ evaluate,⽽是要先使⽤ ip 函数将其翻转 (交换参数的顺序)。对于⼀些运算符来说,这并不 是必须的 (⽐如 a + b 与 b + a 的运算结果是相同的),但对于另外⼀些来说,却是必须的 (⽐如 在 b 不等于零时,a - b 与 b - a 的结果是不同的): func op(character: Character, _ evaluate: (Int, Int) -> Int, _ operand: Calculator) -> Calculator { let withOperator = curry(ip(evaluate)) operand return optionallyFollowed(operand, withOperator) } ip 函数的定义如下: func ip(f: (B, A) -> C) -> (A, B) -> C { return { (x, y) in f(y, x) } } 我们现在已经为再⼀次定义完整的解析器做完了铺垫,不过这次的⽅式会更为⾼效: func eof() -> Parser { return Parser { stream in if (stream.isEmpty) { return one (((), stream)) } return none() } } func pExpression() -> Calculator { return operatorTable.reduce(number) { next, inOp in op(inOp.0, inOp.1, next) } } testParser(pExpression() <* eof(), "10-3*2") /** 结果为: Success, found 4, remainder:[] */ 我们在以上⽰例中构建的计算器依然有明显的缺陷,最致命的问题在于每个运算符只能被使⽤ ⼀次。我们会在下⼀章中解决这个问题,并使⽤我们的解析器库构建⼀个⼩型的表格应⽤。 案例研究:构建 一个表格应用 13 在本章的研究中,我们会着⼿为⼀个简易的表格应⽤构建⼀个解析器、⼀个求值器以及⼀个 GUI (图形化⽤⼾界⾯)。表格是由⼀些按照⾏与列进⾏排列的单元格组成的。单元格可以包含像 是 10*2 这样的公式。在对公式进⾏解析后,我们会构造⼀棵被称为抽象语法树的树形结构来 描述公式,并将其作为结果返回。随后,我们会对这些公式语法树进⾏求值,计算出每⼀个单 元格的结果,再将其展⽰在 GUI 的表格视图中。 在我们的表格中,会使⽤从 0 开始的数字作为⾏坐标,⽽列坐标则使⽤从 A 到 Z 的字⺟来命名。 ⽐如,表达式 C10 ⽤于表⽰位于第 10 ⾏第 C 列的单元格。⽽形如 A0:A10 的表达式会⽤来表 ⽰⼀个单元格的列表。在本章的案例中,即是从第 A 列的第⼀个单元格开始,⼀直到⾏坐标为 10 的单元格为⽌ (该单元格也被包含在内)。 在实现这个表格应⽤的过程中,会⽤到前⼀章中构建的解析器库。在之前,我们就已经使⽤该 库实现过⼀个简单的计算器。⽽在这次的表格应⽤中,我们会拓展该库来解析⼀些更复杂的公 式:除了简单的数学计算,它还需要⽀持对单元格与函数的解析,⽐如 SUM(A0:A10)。 最后,在具备了解析器与求值器的函数式内核之后,我们会引⼊标准的 Cocoa 框架,来向⼤家 展⽰如何将纯粹的函数式内核与⾯向对象的⽤⼾界⾯结合起来。 示例代码 不同于其它章节的 Playground ⽰例,本章会引⼊⼀个⽰例项⽬。这是因为项⽬中会附带⼀个 简易的 GUI ,仅在 Playground 中处理的话,例⼦中所涉及的内容会相当繁琐。在接下来的介 绍中,请务必打开 “Spreadsheet” 项⽬来查阅完整的⽰例。 解析器 我们将把解析阶段分为两个步骤:符号化与解析。符号化阶段 (也被称为词法或词法分析) 会将 输⼊的字符串转化为⼀个符号序列。在此期间,我们还会移除空⽩符、并对运算符与括号进⾏ 解析,如此⼀来,就不必再担⼼它们可能会出现在接下来的解析过程中了。 ⽽接下来的解析阶段中,则会对符号⽣成器返回的符号序列进⾏操作,将其转化为⼀个抽象语 法树:⼀个⽤于表⽰公式表达式的树形结构。 符号化 ⼀般⽽⾔,我们可以使⽤苹果 Foundation 框架中的 NSScanner 类来⽣成所需要的符号列表。 可若是希望能有⼀个好⽤的 Swift API 来实现功能,可能得先对这个类进⾏封装。此外,我们还 需要关掉该类⾃动跳过空⽩符的功能,并⾃⾏编写处理空⽩符的逻辑。因此在本书中,我们选 择了更简单且有具⽰范性的做法:不使⽤ NSScanner ,⽽是利⽤我们构建的解析器库编写⼀ 个扫描器。 和之前以函数式解决问题的⽅式相同,我们会先定义⼀个数据类型 Token。这是⼀个包含五个 成员的枚举:数字、运算符、单元格的⾏列坐标 (references,⽐如 A10 或 C42)、标点符号 (punctuation,在本例中,只有圆括号),以及函数名 (⽐如 SUM 或 AVG): enum Token: Equatable { case Number(Int) case Operator(String) case Reference(String, Int) case Punctuation(String) case FunctionName(String) } 接下来,我们会为每个成员各定义⼀个解析器,解析器被⽤于从输⼊字符串读取字符,并返回 ⼀个由对应符号与字符串剩余部分组成的多元组。举个例⼦,对于公式字符串 10+2,数字成员 (Number) 对应的解析器函数应当返回⼀个 (Token.Number(10), "+2")。 不过在此之前,让我们先定义⼀组可以使代码更为简洁的辅助函数。这些函数本⾝可能并没有 太⼤的意义,不过还请拭⽬以待,它们在稍后必有⽤武之地。 第⼀个辅助函数被命名为 const,实现并不复杂: func const(x: A) -> B -> A { return {_ in x } } 向 const 传⼊⼀个类型为 A 的值,它将返回⼀个形如 B -> A 的常量函数。也就是说,⽆论你向 返回的函数中传⼊ (类型为 B 的) 什么值,它总是会返回 const 函数第⼀次接收的那个 (类型为 A 的) 参数。 另⼀个有⽤的辅助函数是 tokens。该函数会根据你传⼊的数组 input 构建⼀个解析器并将其返 回。返回的解析器将消耗 input 中的那些元素,并返回⼀个由传⼊数组与剩余元素组成的多元 组: func tokens(input: [A]) -> Parser { guard let (head, tail ) = input.decompose else { return pure([]) } return prepend token(head) <*> tokens(tail) } 如果以上内容对你来说宛若天书,建议你先阅读解析器组合算⼦章节,该章节讲解了解析器库 中所有的基本构建单元,⽐如运算符 与 <*>。 tokens 函数会被递归地应⽤在传⼊的数组上:先将数组拆分为 head 部分 (第⼀个元素) 与 tail 部分 (包含剩余元素的数组)。接着使⽤上⼀章中定义的 token 函数来解析元素 head,并为 tail 递归地调⽤ tokens,将这⼆者依序合并之后,便会得到最终的解析器。 在具备了这个函数后,我们可以很容易地创建⼀个函数 string 来构造⼀个⽤于解析特定字符串 的解析器: func string(string: String) -> Parser { return const(string) tokens(Array(string.characters)) } 最后要介绍的辅助函数是 oneOf,⼀个以相互独⽴的⽅式结合多个解析器的函数 (⽐如,我们希 望对 +、-、/ 与 * 中的某⼀个运算符进⾏解析): func fail() -> Parser { return Parser { _ in none() } } func oneOf(parsers: [Parser]) -> Parser { return parsers.reduce(fail(), combine: <|>) } 函数中⽤到的 fail 辅助函数会直接返回⼀个总是解析失败的解析器,⽆论传⼊的是什么。 所有需要的辅助函数都已经准备就绪,让我们从数字成员 Number 的解析器开始吧。为了实现 它,我们可以定义⼀个解析⾃然数的解析器,对字符输⼊流中⾄少⼀个⼗进制的数字进⾏消费, 然后藉由reduce 函数的功能,将这些数字合并为⼀个整数: let pDigit = oneOf(Array(0...9).map { const($0) string("\($0)") }) func toNaturalNumber(digits: [Int]) -> Int { return digits .reduce(0) { $0 * 10 + $1 } } let naturalNumber = toNaturalNumber oneOrMore(pDigit) 现在,我们就可以定义函数 tNumber 了,该函数所做的就是解析出⼀个⾃然数,并通过将解析 结果封装在 Number 中来⽣成⼀个 Token: let tNumber = { Token.Number($0) } naturalNumber 想测试这个数字符号⽣成器,我们可以传⼊ "42" 来运⾏这个解析器: parse(tNumber, "42") /** 结果为: Optional(main.Token.Number(42)) */ 接下来是对运算符的解析:我们需要解析出对应运算符的字符串并将它封装为 Token 的 .Operator 成员中去。通过使⽤ string 函数,我们会将数组内定义的每个运算符都转换为⼀个 解析器,然后利⽤ oneOf 函数将这些单个运算符的解析器结合起来: let operatorParsers = ["*","/","+","-",":"]. map { string($0) } let tOperator = { Token.Operator($0) } oneOf (operatorParsers) 对于⾏列坐标 Reference,我们需要解析出⼀个⼤写字⺟以及尾随其后的⾃然数。⾸先我们利 ⽤上⼀章中的辅助函数 characterFromSet 构建⼀个解析器 capital: let capitalSet = NSCharacterSet.uppercaseLetterCharacterSet() let capital = characterFromSet(capitalSet) 现在我们可以将解析器 capital 与解析器 naturalNumber 拼接起来以解析⾏列坐标。由于是对 两个解析器进⾏合并,这⾥还需要将构造⾏列坐标的函数柯⾥化: let tReference = curry { Token.Reference(String($0), $1) } capital <*> naturalNumber 标点符号的解析器也⾮常简单:将左右圆括号的字符封装在 Punctuation 中即可: let punctuationParsers = ["(",")"].map { string($0) } let tPunctuation = { Token.Punctuation($0) } oneOf(punctuationParsers) 最后,由⾄少⼀个⼤写字⺟组成的函数名 (如 SUM) 可以被视为⼀个字符串,再将其封装在 FunctionName 中: let tName = { Token.FunctionName(String($0)) } oneOrMore(capital) 到这⾥,为公式表达式⽣成⼀串符号输⼊流所需要的函数就已经基本准备完毕了。不过考虑到 我们还需要忽略掉表达式中的各种空⽩符,这⾥我们额外定义⼀个辅助函数 ignoreLeadingWhitespace,把所有符号间的空⽩符 “吃掉”: let whitespaceSet = NSCharacterSet.whitespaceAndNewlineCharacterSet() let whitespace = characterFromSet(whitespaceSet) func ignoreLeadingWhitespace(p: Parser) -> Parser { return zeroOrMore(whitespace) *> p } ⾄此,⼀个完整的表达式符号⽣成器就可以被定义出来了。利⽤ oneOf 函数将以上所有的解析 器结合在⼀起,再在外层封装⼀个 ignoreLeadingWhitespace 函数来清除空⽩符,最后将这些 内容传⼊⼀个 zeroOrMore,就可以得到⼀个符号的列表: func tokenize() -> Parser { let tokenParsers = [tNumber, tOperator, tReference, tPunctuation, tName] return zeroOrMore(ignoreLeadingWhitespace(oneOf(tokenParsers))) } 关于符号⽣成器的内容就这么多,现在可以试着解析⼀个表达式的例⼦: parse(tokenize(), "1+2*3+SUM(A4:A6)") /** 结果为: Optional([main.Token.Number(1), main.Token.Operator("+"), main.Token. Number(2), main.Token.Operator("*"), main.Token.Number(3), main.Token. Operator("+"), main.Token.FunctionName("SUM"), main.Token. Punctuation("("), main.Token.Reference("A", 4), main.Token.Operator(":"), main.Token. Reference("A", 6), main.Token.Punctuation(")")]) */ 解析 接下来,我们要从符号⽣成器⽣成的符号列表中创建⼀个表达式。表达式可以是数字、单元格 的⾏列坐标、带有⼀个运算符的双⽬运算表达式,也可以是对某个函数的调⽤。在下⽂的递归 枚举中可以看到以上描述的所有情况,这便是该表格应⽤所⽀持表达式的抽象语法树了: indirect enum Expression { case Number(Int) case Reference(String, Int) case BinaryExpression(String, Expression, Expression) case FunctionCall(String, Expression) } 到⽬前为⽌,我们构造出来的解析器只能处理⼀些字符串 (更确切地说,是⼀个字符列表)。⽽ 实际上,之前定义解析器的⽅式是⽀持任意类型的符号的,⾃然也包括上⼀⼩节中定义的 Token 类型。 在最开始,我们会为接下来的解析器定义⼀个简洁的类型别名,⽤以声明该解析器会从⼀连串 类型为 Token 的值中,解析出 Expression 类型的值作为结果: typealias ExpressionParser = Parser 先从解析数字开始吧。在试图将⼀个符号解析为⼀个数字表达式时,可能会发⽣两件事:要么 在符号为数字的情况下成功,要么在其它所有情况下失败。为了构造数字解析器,我们还需要 定义⼀个额外的辅助函数 optionalTransform。借助该函数,我们可以将被解析的符号转换为 ⼀个表达式 Expression,或是在符号⽆法被转为对应表达式时返回⼀个 nil: func optionalTransform(f: T -> A?) -> Parser { return { f($0)! } satisfy { f($0) != nil } } 现在,我们就可以定义数字表达式的解析器了。要注意的是 Number 在这⾥被使⽤了两次:第 ⼀个实例其实是 Token.Number,⽽第⼆个实例才是⼀个 Expression.Number: let pNumber: ExpressionParser = optionalTransform { guard case let .Number(number) = $0 else { return nil } return Expression.Number(number) } 对单元格坐标的解析也是⼤同⼩异: let pReference: ExpressionParser = optionalTransform { guard case let .Reference(column, row) = $0 else { return nil } return Expression.Reference(column, row) } 再将这两个解析器合并在⼀起: let pNumberOrReference = pNumber <|> pReference 现在,我们可以使⽤这个解析器分别对数字和⾏列坐标进⾏解析,来测试它是否按照预期⼯作: parse(pNumberOrReference, parse(tokenize(), "42")!) /** 结果为: Optional(main.Expression.Number(42)) */ parse(pNumberOrReference, parse(tokenize(), "A5")!) /** 结果为: Optional(main.Expression.Reference("A", 5)) */ 接下来,我们来看看类似于 SUM(...) 的函数调⽤。为了实现这个功能,我们先定义⼀个解析器, 与上⽂中数字和⾏列坐标的解析器类似,该解析器会解析⼀个函数名符号,或者在符号不是 Token.FunctionName 时返回 nil: let pFunctionName: Parser = optionalTransform { guard case let .FunctionName(name) = $0 else { return nil } return name } 在本案例中,调⽤函数时传⼊的参数总是形如 A1:A3 的⼀个以单元格⾏列坐标为元素的列表。 ⽽列表解析器则需要解析由运算符符号 : 分割开来的两个⾏列坐标符号: func makeList(l: Expression, _ r: Expression) -> Expression { return Expression.BinaryExpression(":", l, r) } let pList: ExpressionParser = curry(makeList) pReference <* op(":") <*> pReference 上⽂中的 op 是⼀个简单的辅助函数,它负责接收⼀个运算符字符串并返回⼀个解析器。该解 析器能够解析对应的运算符号,同时将传⼊的运算符字符串作为解析结果返回: func op(opString: String) -> Parser { return const(opString) token(Token.Operator(opString)) } 要想将以上功能结合起来对某个函数的调⽤进⾏解析,我们还需要⼀个⽤来解析括号表达式的 函数: func parenthesized(p: Parser) -> Parser { return token(Token.Punctuation("(")) *> p <* token(Token.Punctuation(")")) } 这个函数以任意解析器 p 作为参数,然后将左右圆括号的解析器分别合并在该解析器左右两侧。 由于使⽤了组合算⼦运算符 *> 与 <* 对括号解析器的解析结果进⾏丢弃,parenthesized 与 p 的解析结果是完全相同的。 现在我们可以将以上各个要素合并在⼀起,来解析⼀个完整的函数调⽤: func makeFunctionCall(name: String, _ arg: Expression) -> Expression { return Expression.FunctionCall(name, arg) } let pFunctionCall = curry(makeFunctionCall) pFunctionName <*> parenthesized(pList) 做个测试吧: parse(pFunctionCall, parse(tokenize(), "SUM(A1:A3)")!) /** 结果为: Optional(main.Expression.FunctionCall("SUM", main.Expression. BinaryExpression(":", main.Expression.Reference("A", 1), main.Expression. Reference("A", 3)))) */ 在解析完整的公式时,我们需要从表达式所包含的最⼩组成单元开始解析,并选择合适的⽅式 实现运算符的优先级。举个例⼦,我们希望在解析时,乘法拥有⽐加法更⾼的优先级。在这⾥, 我们先为公式的基本元素定义⼀个解析器: let pParenthesizedExpression = parenthesized(lazy { expression() }) let pPrimitive = pNumberOrReference <|> pFunctionCall <|> pParenthesizedExpression 公式的基本元素可以是⼀个数字、⼀个单元格的⾏列坐标,⼀个函数调⽤,也可以是另⼀个包 含在括号中的表达式。对于后者⽽⾔,我们需要使⽤辅助函数 lazy,以确保 expression 函数仅 在真正需要的时候被调⽤,否则我们就会陷⼊⼀个死循环中。(这⾥的 expression 函数应当返 回另⼀个完整表达式的解析器,我们会在稍后实现。) 我们已经可以解析出基本元素了,现在可以将以上内容放在⼀起来解析乘法表达式 (或是除法 —— 在这⾥,我们可以认为两种运算具有相同的运算优先级,所以会被同等对待)。⼀个乘法 表达式可能只是⼀个因数,也可能还包括尾随在因数之后的多个像 * 被乘数 或 / 被除数 这样 配对的运算符与运算对象。这些成对的表达式可以被定义成如下的样式: let pMultiplier = curry { ($0, $1) } (op("*") <|> op("/")) <*> pPrimitive 解析器 pMultiplier 的结果是⼀个由运算符字符串 (如 "*") 与其右侧表达式组成的多元组。鉴于 此,完整的乘法解析器看起来会像这样: let pProduct = curry(combineOperands) pPrimitive <*> zeroOrMore(pMultiplier) 这⾥的关键是 combineOperands 函数,它可以根据单个基本元素和可能存在的多个 pMultiplier 结果多元组,构建出⼀棵表达式树。在构建这棵语法树时,我们需要确保运算符的 计算是符合左向优先的 (不⽤担⼼ *, 因为乘法运算满⾜交换律,但 / 则不然)。幸运地是, reduce 函数就是以左向优先的⽅式⽣效的,也就是说,⼀个类似于 1, 2, 3, 4 的序列会被 reduce 函数以 (((1, 2), 3), 4) 的⽅式处理。我们可以利⽤这个特性来将⼀个表达式⾥的乘法 运算对象进⾏组合: func combineOperands(rst: Expression, _ rest: [(String, Expression)]) -> Expression { return rest.reduce(rst) { result, pair in let (op, exp) = pair return Expression.BinaryExpression(op, result, exp) } } 到这⾥,我们就可以对基本元素和由它们组成的乘法表达式进⾏解析了。最后⼀个没有解决的 问题是⼀个或多个乘法表达式之间的加法运算。鉴于这与乘法表达式的解析模式如出⼀辙,我 们在这⾥就不多啰嗦了: let pSummand = curry { ($0, $1) } (op("-") <|> op("+")) <*> pProduct let pSum = curry(combineOperands) pProduct <*> zeroOrMore(pSummand) 完整的公式表达式现在可以⽤解析器 pSum 来解析。我们为这个解析器起⼀个别名,也就是上 ⽂中已经引⽤过的 expression: func expression() -> ExpressionParser { return pSum } 最后,可以在函数 parseExpression 中对符号⽣成器和解析器进⾏合并。先将输⼊的字符串符 号化,如果符号化成功,就解析该表达式: func parseExpression(input: String) -> Expression? { return parse(tokenize(), input).atMap { parse(expression(), $0) } } 只需要将该函数应⽤在⼀个完整的公式上,就可以⽣成我们预期的表达式语法树: parseExpression("1 + 2*3 - MIN(A5:A9)") /** 结果为: Optional(main.Expression.BinaryExpression("-", main.Expression. BinaryExpression("+", main.Expression.Number(1), main.Expression. BinaryExpression("*", main.Expression.Number(2), main.Expression. Number(3))), main.Expression.FunctionCall("MIN", main.Expression. BinaryExpression(":", main.Expression.Reference("A", 5), main.Expression. Reference("A", 9))))) */ 求值器 我们得到了⼀个 (使⽤ Expression 类型值来表⽰的) 抽象语法树,现在要做的是将这个表达式 的结果计算出来。在这个⼩例⼦中,我们会假设我们的表格是⼀维的 (即只有⼀列)。将⽰例拓 展为⼀个⼆维表格并不会太难,不过出于演⽰的⽬的,只关注⼀个维度会让问题更简单⼀些。 为了让代码更易读,我们会做另⼀个简化:不对单元格中表达式的计算结果进⾏缓存。在⼀个 “真正的” 表格应⽤中,定义运算的顺序是⾮常重要的。举个例⼦,假设单元格 A2 需要⽤到 A1 的值,通常会先计算 A1 并将结果缓存,以便我们在计算 A2 的结果时能够使⽤该缓存的结果。 添加这个特性并不难,但在这⾥,为了让整个讲解更为清晰,这个功能的实现就留给读者作为 练习了。 让我们从定义 Result 枚举开始吧。在计算⼀个表达式时,存在三种可能的结果。如果遇到类似 10+10 这种简单的算术表达式,结果会是⼀个由 IntResult 成员来表⽰的数字。如果是形如 A0:A10 的表达式,则会得到⼀个计算结果的列表,这可以⽤ ListResult 来表⽰。最后,在计算 时很有可能出现⼀个错误,⽐如⼀个表达式⽆法被解析,或是使⽤了⼀个不合法的表达式。在 这种情况下,我们会希望显⽰⼀个错误,我们⽤ EvaluationError 来代表这种情况: enum Result { case IntResult(Int) case StringResult(String) case ListResult([Result]) case EvaluationError(String) } 在继续进⾏求值之前,我们先来定义⼀个名为 lift 的辅助函数。它可以使平常进⾏整数计算时 所使⽤的函数对 Result 枚举也⽣效。⽐如,运算符 + 是⼀个将两个整数参数求和的函数。借助 lift ,我们可以将运算符 + 提升为⼀个接收两个 Result 型参数的函数,如果两个参数都是 IntResult,则将两个参数合并为⼀个新的 IntResult。如果某⼀个 Result 参数并不是⼀个 IntResult,该函数则返回⼀个求值错误: typealias IntegerOperator = (Int, Int) -> Int func lift (f: IntegerOperator) -> ((Result, Result) -> Result) { return { l , r in guard case let (.IntResult(x), .IntResult(y)) = ( l , r) else { return .EvaluationError("Couldn't evaluate \(l, r)") } return .IntResult(f(x, y)) } } 虽然符号⽣成器⽀持运算符 "+"、"/"、"*" 与 "-",然⽽,在表达式枚举的 BinaryExpression 成 员中运算符是作为⼀个 String 类型的值被存储的。因此,我们需要⼀种⽅式将这些字符串映射 为⼀个可以合并两个 Int 参数的函数。这⾥定义了⼀个字典 integerOperators 来解决这个问题: let integerOperators: [String: IntegerOperator] = [ "+": op(+), "/": op(/), "*": op(*), "-": op(-) ] 现在,我们可以编写⼀个名为 evaluateIntegerOperator 的函数,给定⼀个运算符字符串 op 以 及两个表达式,它将返回⼀个可选的 Result。我们将函数 evaluate 作为额外的参数传⼊,⽤来 对单个表达式进⾏求值: func evaluateIntegerOperator(op: String, _ l: Expression, _ r: Expression, _ evaluate: Expression? -> Result) -> Result? { return integerOperators[op].map { lift ($0)(evaluate(l), evaluate(r)) } } 运算符字符串 op 被⽤于在 integerOperators 字典中进⾏查询,它返回⼀个类型为 (Int, Int) -> Int 的可选值函数。我们对该可选值函数进⾏映射,然后使⽤ evaluate 参数对左 右两个参数 l 与 r 进⾏求值,得到两个计算结果。最后使⽤ lift 函数将两个结果结合为⼀个新 的结果。这⾥要注意,如果运算符⽆法被识别,会返回⼀个 nil: 为了对⼀个列表运算符 (⽐如 A1:A5) 进⾏求值,还要定义函数 evaluateListOperator,与上⽂ 中 evaluateIntegerOperator 类似: func evaluateListOperator(op: String, _ l: Expression, _ r: Expression, _ evaluate: Expression? -> Result) -> Result? { switch (op, l , r) { case (":", .Reference("A", let row1), .Reference("A", let row2)) where row1 <= row2: return Result.ListResult(Array(row1...row2).map { evaluate(Expression.Reference("A", $0)) }) default: return nil } } 我们先检查运算符字符串是否为列表运算符 :,同时由于这⾥的⽰例仅⽀持单列的表格,我们 需要确保 l 与 r 都是⾏列坐标,且 row2 ⼤于或等于 row1。注意,在这⾥我们使⽤⼀个 switch 条件分⽀就可以同时检查所有满⾜条件的情况。在其它所有情况下,我们都只需要返回 nil。如 果所有前提条件都满⾜的话,我们就可以映射各个单元格并进⾏求值了。 到这⾥,我们就可以对任意双⽬运算符进⾏求值了。⾸先,我们会尝试整数运算符是否能求值 成功。如果返回了 nil,我们会尝试去对列表运算符进⾏求值。如果都不成功,我们则返回⼀个 错误: func evaluateBinary(op: String, _ l : Expression, _ r: Expression, _ evaluate: Expression? -> Result) -> Result { return evaluateIntegerOperator(op, l, r, evaluate) ?? evaluateListOperator(op, l, r, evaluate) ?? .EvaluationError("Couldn't nd operator \(op)") } 接下来,我们将为列表添加两个函数,SUM 和 MIN,它们分别⽤于计算某个列表的总和以及最 ⼩值。给定⼀个函数名和⼀个类型为 Result 的参数 (我们现在仅⽀持单参函数),如果参数是⼀ 个 ListResult,就利⽤ switch 语句对所有可能的函数名进⾏检测。如果匹配成功,就可以对参 数列表的总和或者最⼩值进⾏求值了。因为这些列表的元素并不是 Int,⽽是 Result,所以还 需要使⽤ lift 函数对运算进⾏升级: func evaluateFunction(functionName: String, _ parameter: Result) -> Result { switch (functionName, parameter) { case ("SUM", .ListResult(let list )): return list .reduce(Result.IntResult(0), combine: lift(+)) case ("MIN", .ListResult(let list )): return list .reduce(Result.IntResult(Int.max), combine: lift { min($0, $1) }) default: return .EvaluationError("Couldn't evaluate function") } } 将这些函数集合到⼀起,就可以对单个表达式进⾏求值了。不过在真正求值时,我们还需要所 有其它的表达式的信息 (因为表达式可能包含对另⼀个单元格的引⽤)。因此,函数 evaluateExpression 将接收⼀个元素类型为 Expression? 的数组,作为参数 context :数组中 的每个元素对应了每⼀个单元格的表达式 (数组的第⼀个元素是 A0 的表达式,第⼆个是 A1 的, 以此类推)。在解析失败的情况下,可选 Expression 会是⼀个 nil。evaluateExpression 所需 要做的只是对表达式进⾏匹配并调⽤对应的求值函数: func evaluateExpression(context: [Expression?]) -> Expression? -> Result { return { (e: Expression?) in e.map { expression in let recurse = evaluateExpression(context) switch (expression) { case let .Number(x): return Result.IntResult(x) case let .Reference("A", idx): return recurse(context[idx]) case let .BinaryExpression(s, l, r ): return evaluateBinary(s, l, r, recurse) case let .FunctionCall(f, p): return evaluateFunction(f, recurse(p)) default: return .EvaluationError("Couldn't evaluate expression") } } ?? .EvaluationError("Couldn't parse expression") } } 最后,我们可以定义⼀个便利函数 evaluateExpressions,它接收⼀个由可选 Expression 组成 的列表,通过 evaluateExpression 对列表进⾏映射,⽣成⼀组 Result 的列表: func evaluateExpressions(expressions: [Expression?]) -> [Result] { return expressions.map(evaluateExpression(expressions)) } 如同本章简介和通篇中所说明的那样,当前的解析器和求值器还有很⼤的局限性。更好的错误 信息,对单元格的依赖性分析,循环检测,⼤规模的优化,类似这样可以做的事情还有很多。 不过值得注意的是,我们只⽤了⼤约 200 ⾏代码,就已经为这个表格应⽤定义了完整的模型层, 解析器与求值器。这正是函数式编程的强⼤之处:在考虑了类型与数据结构之后,许多的函数 编写起来都⾮常简单。⽽代码只要被写出来,就总会有办法进⾏改进和优化。 另外,我们在表达式解析和求值的编写过程中复⽤了很多的标准库函数。组合算⼦ lift 让复⽤ 任意的双整数参数的函数变得格外简单。像 SUM 和 MIN 这样的函数可以使⽤ reduce 来计算。 ⽽且,在具备了前⼀章的解析库之后,我们很快就编写出了词法分析器与解析器。 GUI 现在,我们已经具备了公式的解析器和求值器,就可以围绕着它们来构建⼀个简单的 GUI (图形 化⽤⼾界⾯) 了。由于基本上所有的 Cocoa 框架都是⾯向对象的,我们将不得不在接下来的⼯ 作中将⾯向对象编程与函数式编程进⾏结合。幸好,Swift 使这项⼯作变得⾮常简单。 我们会创建⼀个包含窗⼝的 XIB ⽂件,窗⼝中只包含⼀个单列的 NSTableView。NSTableView 由⼀个数据源来提供数据,并由代理来处理编辑操作。数据源与代理是两个不同的对象。数据 源会存储公式以及它们的结果,⽽代理则告诉数据源当前情况下,正在被编辑的是哪⼀⾏。处 于编辑状态的⼀⾏会展⽰公式⽽不是结果。 数据源 数据源是真正意义上结合函数式与⾯向对象编程的部分。该对象遵守 NSTableViewDataSource 协议,并为 NSTableView 提供数据。在我们的案例中,数据就是指 单元格。我们会检查⼀个单元格是否正处于编辑状态,如果处于编辑状态的话,我们会显⽰该 单元格的公式,否则显⽰计算结果。在这个类中,我们需要做的是保存像是公式字符串、计算 得出的结果、以及正在处于编辑状态的⾏的⾏标 (它可能为 nil) 这些信息: class SpreadsheetDatasource: NSObject, NSTableViewDataSource, EditedRow { var formulas: [String] var results: [Result] var editedRow: Int? = nil // ... } 在这个类的构造⽅法中,我们会将公式初始化为从⼀到⼗的数字字符串。并将结果初始化为上 述公式对应的结果值: override init () { let initialValues = Array(1..<10) formulas = initialValues.map { "\($0)"} results = initialValues .map { Result.IntResult($0) } } 在这⾥,我们需要实现 NSTableViewDataSource 协议中的两个⽅法:第⼀个⽅法返回表格的 ⾏数 (只需要返回数组 formulas 的元素个数)。第⼆个⽅法返回每⼀个单元格的内容。在本案例 中,我们还要关注每⼀⾏的状态。如果该⾏正处于编辑状态,我们会返回公式。否则,则返回 运算结果: func numberOfRowsInTableView(tableView: NSTableView) -> Int { return formulas.count } func tableView(aTableView: NSTableView, objectValueForTableColumn: NSTableColumn?, row: Int) -> AnyObject? { return editedRow == row ? formulas[row] : results[row].description } 当⽤⼾编辑完⼀个单元格时,我们会重新计算所有的公式: func tableView(aTableView: NSTableView, setObjectValue: AnyObject?, forTableColumn: NSTableColumn?, row: Int) { let string = setObjectValue as! String formulas[row] = string calculateExpressions() aTableView.reloadData() } 为了计算表达式,我们会使⽤之前定义的函数 parseExpression,对每⼀条公式进⾏映射,将 其符号化并解析。映射会返回⼀组元素为 Expression? 的数组 (符号化和解析有可能会失败)。 然后,使⽤函数 evaluateExpressions 来计算⼀组对应的结果: func calculateExpressions() { results = evaluateExpressions(formulas.map(parseExpression)) } 代理 代理唯⼀的任务是告诉数据源,当前处于编辑状态的单元格是哪⼀个。由于我们希望两个类之 间是松耦合的,所以定义了⼀个额外的协议 EditedRow。协议要求实现⼀个名为 editedRow 的属性,在某些情况下会包含正处于编辑状态的⾏标 (你可能已经在 SpreadsheetDatasource 类的类型声明中注意到了这个协议): protocol EditedRow { var editedRow: Int? { get set } } 现在,当某⾏处于编辑状态的时候,我们只需要为数据源的该属性赋值就可以了: class SpreadsheetDelegate: NSObject, NSTableViewDelegate { var editedRowDelegate: EditedRow? func tableView(aTableView: NSTableView, shouldEditTableColumn aTableColumn: NSTableColumn?, row rowIndex: Int) -> Bool { editedRowDelegate?.editedRow = rowIndex return true } } 窗口控制器 最后⼀个部分,我们会在窗⼝控制器中将所有元素连接在⼀起。(如果你是⼀名 iOS 的开发者, 窗⼝控制器 NSWindowController 与视图控制器 UIViewController 的作⽤如出⼀辙)。在窗⼝ 控制器中,有三个属性被声明为 IBOutlet,它们分别是⼀个 NSTableView,⼀个数据源以及⼀ 个代理。所有这些对象都会由 nib ⽂件进⾏实例化: class SheetWindowController: NSWindowController { @IBOutlet var tableView: NSTableView! = nil @IBOutlet var dataSource: SpreadsheetDatasource? @IBOutlet var delegate: SpreadsheetDelegate? 当窗⼝被载⼊时,我们将代理与数据源链接起来,以确保代理能够通知数据源某⼀⾏的编辑状 态。另外,我们会注册⼀个观察者以得知编辑在何时结束。在编辑结束的通知发送之后,我们 会将数据源的 editedRow 属性置为 nil: override func windowDidLoad() { delegate?.editedRowDelegate = dataSource NSNoticationCenter.defaultCenter().addObserver(self, selector: NSSelectorFromString("endEditing:"), name: NSControlTextDidEndEditingNotication, object: nil) } func endEditing(note: NSNotication) { if note.object as! NSObject === tableView { dataSource?.editedRow = nil } } } ⼤功告成。现在我们拥有了⼀个单列表格应⽤的⾼度可⽤的原型。在⽰例⼯程中可以查阅本案 例的完整⽰例。 函子、适用函子 与单子 14 在本章中,我们会解释⼀些函数式编程中的专⽤术语和⼀些常⻅模式,⽐如函⼦ (Functor)、适 ⽤函⼦ (Applicative Functor) 和单⼦ (Monad) 等。理解这些常⻅的模式,会有助于你设计⾃⼰ 的数据类型,并为你的 API 选择更合适的函数。 函子 迄今为⽌,我们已经遇到了三个被命名为 map 的函数,类型签名分别如下 (为求简洁,以下签 名都做了柯⾥化处理): func map(xs: [T]) -> (T -> U) -> [U] func map(optional: T?) -> (T -> U) -> U? func map(result: Result) -> (T -> U) -> Result 为什么这三个截然不同的函数会拥有相同的函数名呢?要回答这个问题,我们需要先探究这三 个函数的相似之处。在开始之前,将 Swift 使⽤的⼀些简写符号进⾏展开会帮助我们更容易理 解发⽣了什么。像 Int? 这样的可选值也可以被显式地写作 Optional。同样地,我们也可以 使⽤ Array 来替换 [T]。如果我们按照上⾯的书写⽅式定义数组和可选值的 map 函数,相似 之处就变得明显了起来: extension Optional { func map(transform: Wrapped -> U) -> Optional } extension Array { func map(transform: Element -> U) -> Array } Optional 与 Array 都是需要⼀个泛型作为参数来构建具体类型的类型构造体 (Type Constructor)。对于⼀个实例来说,Array 与 Optional 是合法的类型,⽽ Array 本⾝却 并不是。每个 map 函数都需要两个参数:⼀个即将被映射的数据结构,和⼀个类型为 T -> U 的函数 transform。对于数组或可选值参数中所有类型为 T 的值,map 函数会使⽤ transform 将它们转换为 U。这种⽀持 map 运算的类型构造体 —— ⽐如可选值或数组 —— 有时候也被 称作函⼦ (Functor)。 实际上,我们之前定义的很多类型都是函⼦。⽐如,我们可以为第⼋章中的 Result 类型实现⼀ 个 map 函数: extension Result { func map(f: T -> U) -> Result { switch self { case let .Success(value): return .Success(f(value)) case let .Error(error ): return .Error(error) } } } 类似地,我们之前看到的⼆叉搜索树、字典树以及解析器组合算⼦等类型都是函⼦。函⼦有时 会被描述为⼀个储存特定类型值的 “容器”。⽽ map 函数则⽤来对储存在容器中的值进⾏转换。 作为⼀个直观的印象,这样理解或许会有帮助,但却有点过于狭隘。还记得我们在第⼆章中看 到的 Region 类型么? typealias Region = Position -> Bool 如果只使⽤ Region 的定义 (⼀个返回布尔值的函数),我们⾄多能⽣成⼀些⾮⿊即⽩的位图。可 以将这个定义泛型化,对每⼀个位置关联信息的类型做⼀次抽象: struct Region { let value: Position -> T } extension Region { func map(transform: T -> U) -> Region { return Region { pos in transform(self.value(pos)) } } } 就反驳 “函⼦是⼀个容器” 这⼀直观印象来说,上⽂定义的 Region 算得上是⼀个绝佳的反例。 在这⾥,我们⽤来表⽰区域的类型是⼀个函数,与 “容器” 可谓是⻛⻢⽜不相及了。 基本上,你在 Swift 中定义的所有泛型枚举都是⼀个函⼦。如果能为这些枚举提供⼀个强⼤⽽ ⼜熟悉的 map 函数,我们的开发者⼩伙伴们⼀定会乐不可⽀的。 适用函子 除了 map ,许多函⼦还⽀持其它的运算。⽐如第⼗⼆章中的解析器,它除了是⼀个函⼦之外, 还定义了以下两种运算: func pure(value: A) -> Parser func <*>(l: Parser B>, r: Parser) -> Parser 函数 pure 阐明了如何将任意值转化为⼀个返回该值的解析器。同时,运算符 <*> 将两个解析器 顺序化合并:第⼀个解析器返回⼀个函数,⽽第⼆个解析器为这个函数返回⼀个参数。对于任 意类型构造体,如果我们可以为其定义恰当的 pure 与 <*> 运算,我们就可以将其称之为⼀个适 ⽤函⼦ (Applicative Functor)。或者再严谨⼀些,对任意⼀个函⼦ F,如果能⽀持以下运算,该 函⼦就是⼀个适⽤函⼦: func pure(value: A) -> F func <*>(f: F B>, x: F) -> F 实际上,适⽤函⼦⼀直隐藏在本书的各个⻆落中。⽐如,上⼀节定义的 Region 也是⼀个适⽤函 ⼦: func pure(value: A) -> Region { return Region { pos in value } } func <*>(regionF: Region B>, regionX: Region) -> Region { return Region { pos in regionF.value(pos)(regionX.value(pos)) } } 现在,函数 pure 可以使任意区域都返回某个特定的值。⽽运算符 <*> 则会将结果区域的参数 Position 分别传⼊它的两个参数区域,其中⼀个参数会⽣成⼀个类型为 A -> B 的函数,另⼀个 则会⽣成⼀个类型为 A 的值。接着,合并这两个区域的⽅法相信你也猜得到,将第⼀个参数返 回的函数应⽤在第⼆个参数⽣成的值上。 许多为 Region 定义的函数都可以借由这两个基础构建模块简短地描述出来。参照第⼆章中的 内容,这⾥以适⽤函⼦的形式编写了⼀⼩部分函数作为⽰例: func everywhere() -> Region { return pure(true) } func invert(region: Region) -> Region { return pure(!) <*> region } func intersection(region1: Region, region2: Region) -> Region { let and: Bool -> Bool -> Bool = { x in { y in x && y } } return pure(and) <*> region1 <*> region2 } 上述代码展⽰的,便是利⽤ Region 类型的适⽤实例逐个定义区域运算的⽅式。 适⽤函⼦并不仅限于区域和解析器。Swift 内建的可选类型就是适⽤函⼦的另⼀个例⼦。对应 的定义简单明了: func pure(value: A) -> A? { return value } func <*>(optionalTransform: (A -> B)?, optionalValue: A?) -> B? { guard let transform = optionalTransform, value = optionalValue else { return nil } return transform(value) } pure 函数将⼀个值封装在可选值中。由于这个过程通常会被 Swift 做隐式的处理,我们定义的 这个版本并不是很实⽤。⽽运算符 <*> 就会更有意思⼀些:传⼊⼀个 (可能为 nil 的) 函数和⼀ 个 (可能为 nil 的) 参数,它会在两个可选值同时有值时,将函数 “适⽤” 在参数上,并将结果返 回。如果两个参数中任意⼀个为 nil,则运算结果也会是 nil。我们也可以为第⼋章中的 Result 类型定义类似的 pure 与 <*>。 这些定义单独来看并不能发挥什么特别厉害的功效,但组合起来就会很有意思。我们可以回顾 ⼀些之前的例⼦,还能回想起之前的 addOptionals 函数么?⼀个尝试计算两个可能为 nil 的整 数之和的函数: func addOptionals(optionalX: Int?, optionalY: Int?) -> Int? { guard let x = optionalX, y = optionalY else { return nil } return x + y } 利⽤之前的定义,只需要⼀条 return 语句,我们就可以定义出⼀版更为简洁的 addOptionals: func addOptionals(optionalX: Int?, optionalY: Int?) -> Int? { return pure(curry(+)) <*> optionalX <*> optionalY } ⼀旦你理解了 <*> 以及类似运算符中所包含的控制流,以上述⽅式去组织⼀些复杂的计算就变 得轻⽽易举了。 在可选值章节中还有⼀个值得回顾的例⼦: func populationOfCapital(country: String) -> Int? { guard let capital = capitals[country], population = cities[capital] else { return nil } return population * 1000 } 在这⾥,我们需要查阅⼀个字典 capitals,来检索特定国家的⾸都城市。接着查阅另⼀个字典 cities 来确定该城市的⼈⼝数量。尽管与前例中 addOptionals 的运算过程差不太多,可这个函 数却⽆法以适⽤的⽅式来编写。如果我们尝试这样做的话,会发⽣以下的情况: func populationOfCapital(country: String) -> Int? { return { pop in pop * 1000 } <*> capitals[country] <*> cities [...] } 这⾥的问题在于,之前的版本中,第⼀次查询的结果被绑定在了变量 capital 上,⽽第⼆次查询 需要调⽤这个变量。如果只使⽤适⽤⽅式来运算,就会被卡在这⾥:⼀个适⽤运算的结果 (上例 中的 capitals[country]) 是⽆法影响另⼀个适⽤运算 (在 cities 中进⾏查询) 的。要解决这个问 题,我们还需要其它的接⼝。 单子 在第五章中,我们给出了 populationOfCapital 的另⼀种定义⽅式: func populationOfCapital3(country: String) -> Int? { return capitals[country].atMap { capital in return cities [capital] }.atMap { population in return population * 1000 } } 在这⾥,我们使⽤了内建的 atMap 函数来组合可选值的计算过程。这与适⽤接⼝的不同点在 哪⾥呢?答案是,类型。在适⽤运算符 <*> 中,两个参数都是可选值,反观 atMap 函数,第 ⼆个参数却是⼀个返回可选值的函数。也正是因此,我们才得以将第⼀个字典的检索结果传递 给第⼆个字典来进⾏查阅。 只在适⽤函数之间进⾏组合是⽆法定义出 atMap 函数的。实际上,atMap 是单⼦结构⽀持 的两个函数之⼀。更通俗地说,如果⼀个类型构造体 F 定义了下⾯两个函数,它就是⼀个单⼦ (Monad): func pure(value: A) -> F func atMap(x: F) -> (A -> F) -> F atMap 函数有时会被定义为⼀个运算符 >>=。鉴于它将第⼀个参数的计算结果绑定到第⼆个 参数的输⼊上去,这个运算符也被称为 “绑定 (bind)” 运算。 除了 Swift 的可选值类型之外,第⼋章中定义的枚举 Result 也是⼀个单⼦。按照这个思路,将 ⼀些可能返回 ErrorType 的运算连接在⼀起就变得可⾏了。⽐如,我们可以定义⼀个⽤来将⼀ 个⽂件的内容复制到另⼀个⽂件的函数,如下例: func copyFile(sourcePath: String, targetPath: String, encoding: Encoding) -> Result<()> { return readFile(sourcePath, encoding).atMap { contents in writeFile(contents, targetPath, encoding) } } 如果对 readFile 与 writeFile 中任何⼀个⽅法的调⽤失败,⼀个 NSError 就会被抛出。虽然上 例不如 Swift 的可选值绑定机制那么好⽤,但也⼗分接近了。 除了处理错误之外,单⼦还有很多其它的⽤武之地。⽐如,数组也是⼀个单⼦。在标准库中, 数组的 atMap ⽅法已经被定义了,不过你也可以像这样来实现: func pure(value: A) -> [A] { return [value] } extension Array { func atMap(f: Element -> [B]) -> [B] { return self.map(f).reduce([]) { result, xs in result + xs } } } 我们能从这些定义中得到什么呢?作为⼀个单⼦结构,数组提供了⼀种便利的⽅式来定义各种 各样的可组合函数,⼜或是解决搜索相关的问题。举个例⼦,假设我们需要计算两个数组 xs 与 ys 的笛卡尔积。笛卡尔积是⼀个由多元组组成的新数组,每个多元组的第⼀部分都从 xs 中抽 取,第⼆部分则从 ys 中抽取。直接使⽤ for 循环的话,我们可能会这样写: func cartesianProduct1(xs: [A], ys: [B]) -> [( A, B)] { var result: [( A, B)] = [] for x in xs { for y in ys { result += [(x, y)] } } return result } 我们现在可以使⽤ atMap 替换 for 循环来重写 cartesianProduct: func cartesianProduct2(xs: [A], ys: [B]) -> [( A, B)] { return xs.atMap { x in ys.atMap { y in [( x, y)] } } } 函数 atMap 允许我们从第⼀个数组 xs 中提取⼀个元素 x,然后在 ys 中提取⼀个元素 y。对于 每⼀组 x 与 y,我们都会返回⼀个数组 [( x, y)]。最后,atMap 函数会将所有这些数组合并为 ⼀个结果集。 尽管这个例⼦看起来有点勉强,atMap 函数在数组中还是⽤很多很重要的应⽤场景。像是 Haskell 与 Python 这样的语⾔,会专⻔提供⼀些语法糖来定义列表,这被称作列表推导式 (list comprehensions)。这些列表推导式允许你从已经存在的列表中抽取元素并检查这些元素是否 符合某些指定的规则。所有列表推导式语法糖都可以脱糖为 map、 lter 与 atMap 的组合。 列表推导式与 Swift 中的可选值绑定很相似,只不过它们作⽤于列表⽽不是可选值。 讨论 为什么要关注这些概念呢?知道某些类型是不是⼀个适⽤函⼦或⼀个单⼦真的重要么?我们觉 得,是的。 回忆⼀下第⼗⼆章中的解析器组合算⼦吧。定义⼀种合适的⽅式来使两个解析器顺序解析并不 容易:这需要对解析器的⼯作原理有⼀些相对深⼊的了解。在我们的库中,这是必不可少的⼀ 个环节,否则,我们连最简单的解析器都写不出来。如果你能发觉我们的解析器被构造为了⼀ 个适⽤函⼦,你就会意识到之前定义的运算符 <*> 为顺序合并两个解析器提供了最优的解决⽅ 案。在寻求如此复杂的定义时,对⾃定义类型所⽀持的抽象运算有所了解会很有帮助。 像函⼦这样的抽象概念,也为我们提供了很重要的词汇。如果你偶然遇到了⼀个名为 map 的函 数,你或许能把这个函数的功能猜个⼋九不离⼗。若是没有使⽤精确的术语来描述类似于函⼦ 这样的通⽤结构的话,你可能就要花时间从各种拍脑⻔的函数名中发觉,这其实就是个 map 函 数。 另外,你也可以在设计⾃⼰的 API 时以这些结构作为参考。如果你定义了⼀个泛型枚举或是结 构体,且恰好⽀持 map 运算。这是你希望暴露给⽤⼾的接⼝吗?你的数据结构是不是也是⼀个 适⽤函⼦呢?它是⼀个单⼦么?这个操作会做些什么?⼀旦你熟悉了这些抽象结构,问题就会 ⼀个接⼀个的蹦出来。 尽管在 Swift 中⽐在 Haskell 中要难⼀些,但你依旧可以为任意适⽤函⼦定义合适的泛型函数。 就好像解析器运算符 这样的函数,可以借助适⽤函数 pure 与 <*> 来定义⼀般。此外,我们 可能会想为解析器以外的其它适⽤函⼦重新定义这些函数。若是如此,我们便可以利⽤这些抽 象的结构来编写⼀些程序,继⽽在编写过程中接触到⼀些通⽤的模式;⽽这些模式本⾝,也会 在很多场合下派上⽤场。 在函数式编程的世界中,单⼦还有⼀段趣的发展史。最开始,单⼦是在数学领域⾥⼀个被称作 范畴论的分⽀中被发展起来的。其与计算机科学的联系通常被归结为 Moggi (1991) 的发现,随 后由 Wadler (1992a; 1992b) 将其发扬光⼤。从那时起,他们就已经开始在 Haskell 这样的函 数式语⾔中使⽤单⼦,并对单⼦的副作⽤与 I/O (Peyton Jones 2001) 做了控制。⽽适⽤函⼦虽 然⾸先由 McBride and Paterson (2008) 提出,但在当时,其实已经有了很多已知的范例。关 于本章中提到的许多抽象概念之间的关系,可以在 Typeclassopedia (Yorgey 2009) 中找到⼀ 篇完整的概述。 尾声 15 那么,函数式编程到底是什么呢?许多⼈ (错误地) 认为函数式编程只是使⽤像 map 与 lter 这样的⾼阶函数进⾏编程,这可能有点管中窥豹,只⻅⼀斑。 我们在引⾔中提到过,⼀段被精⼼设计的 Swift 函数式程序所应该具有三种特性:模块化、对 可变状态的谨慎处理,以及选择合适的类型。⽽在后续的章节中,这三个概念也被⼀再地提及。 ⽆论是第三章中的 Filter 类型,还是第⼆章的 Region,⾼阶函数都是⼀柄定义抽象概念的利 器。不过,这只是开始,⽽⾮全部。我们为 Core Image 库定义的函数式封装提供了⼀个类型安 全且模块化的⽅法来组织复杂的图像滤镜。⽽第⼗⼀章的⽣成器和序列则帮助我们对循环迭代 进⾏了抽象。 Swift 先进的类型系统甚⾄可以在运⾏代码之前就捕获到许多错误。⽐如第五章中讲述的可选 值类型会对可能为 nil 的值做不可信标记;⽽泛型不仅使代码复⽤变得简单,更使得类型安全 能够被可靠地执⾏,这些内容都在第四章中有所提及。在第⼋章与第九章中介绍的枚举和结构 体,则为你在⾃⼰的代码中精确地构建数据模型时,提供了基本的构建单元。 引⽤透明的函数更易于被推导和测试。我们在第六章中实现的 QuickCheck 库展⽰了使⽤⾼阶 函数为引⽤透明的函数⽣成随机单元测试的⽅式。第七章则告诉我们,Swift 对于值类型的谨 慎处理使我们得以在程序中⾃由地共享数据,⽽⽆需担⼼那些⽆⼼之失或是预料之外的变化。 译者注:“引⽤透明的函数” 指那些不会产⽣副作⽤ (side effect),或者说不会改变程 序运⾏状态和修改函数外变量的函数。纯函数式的函数因为不会对函数外的变量产⽣ 作⽤,因此具有引⽤透明性。 我们可以汇总以上所有的思路来构建⼀个强⼤的特定领域的语⾔。在第⼗章中构建的图表库 与第⼗⼆章中的解析器组合算⼦都定义了⼀个⼩的函数集,它们提供的模块化构建单元,⾜以 为错综复杂的问题组合出可⾏的解决⽅案。⽽第⼗三章中的最后⼀个案例研究,则展⽰了如何 将这些特定领域语⾔应⽤到⼀个完整的程序中去。 最后,我们在本书中⻅到的许多类型都承担着相似的功能。在第⼗四章中,我们展⽰了如何将 它们分类,也揭⽰了其彼此间的关联。 拓展阅读 想更好地磨练函数式编程技能,⼀种⽅式是学习 Haskell。其实函数式语⾔还有很多种,⽐如 F#,OCaml, Standard ML, Scala, 以及 Racket。⽆论是哪种语⾔,作为对 Swift 语⾔的学习 补充,都是很不错的选择。然⽽,Haskell 却是最能够考验编程思想的语⾔。随着对 Haskell 理 解的深⼊,你编写 Swift 的⽅式也将会随之改变。 如今,优秀的 Haskell 书籍与课程遍地⽣花。Graham Hutton 所著的《Programming in Haskell》(2007) 作为⼀本优秀的⼊⻔教程,可以让你熟悉语⾔基 础。《Learn You a Haskell for Great Good!》是⼀本囊括了许多⾼级话题的免费在线读 物。《Real World Haskell》中讲解了⼀些⼤型的案例研究,以及⼤量在其它书⽬中难以⻅到的 技术讲解,内容遍及语⾔特性,调试与⾃动化测试。Richard Bird 以他的 “Functional Pearl” ⽽闻名 —— 这是⼀些优雅且具有指导性的函数式编程范例,你可以在他的著作《Pearls of Functional Algorithm Design》(2010) 以及在线版中看到这些内容。《The Fun of Programming》则集合了 Haskell 中嵌⼊的领域特定语⾔,涵盖的领域从⾦融合约到硬件设计 (Gibbons and de Moor 2003)。 如果你希望学习更多关于泛型编程语⾔设计的内容,Benjamin Pierce 的《Types and Programming Languages》(2002) 不失为明智之选。Bob Harper 的《Practical Foundations for Programming Languages》(2012) 虽然发布较新且更为严谨,可如果你的计算机科学与数 学功底不够扎实,可能会觉得像在读天书。 不必觉得对以上资源的涉猎都是必须的,其中绝⼤部分其实都不是你的菜。不过你需要知道的 是,编程语⾔设计、函数式编程与数学的演进在很⼤程度上直接影响了 Swift 的设计。 如果你希望继续提⾼⾃⼰的 Swift 技艺 —— 且并不只是函数式的部分 —— 我们已经编写了⼀ 整本关于 Swift 进阶主题的书,选题从低级编程到⾼级抽象均有涉及。 结语 对 Swift 来说,现在是最令⼈激动的时代。这⻔语⾔仍然在蹒跚学步。与 Objective-C 相⽐,它 有许多借鉴于其它已存在的函数式编程语⾔的全新特性,这预⽰着我们为 iOS 与 OS X 编写程 序的⽅式将产⽣颠覆性的变化。 与此同时,Swift 的社区发展尚不明朗。⼈们会接受这些特性么?⼜或是写着与 Objective-C 相 同的代码,只不过少个分号?时间终会给我们答案。在此,我们仅希望能够通过编写此书,使 你对⼀些函数式编程的概念有所了解。⽽是否要将本书的内容加以实践,可能要取决于你对 Swift 抱有何种期待。⾔尽于此,愿我们可以⼀同续写 Swift 的未来! 参考文献 Barendregt, H.P. 1984. The Lambda Calculus, Its Syntax and Semantics. Studies in Logic and the Foundations of Mathematics. Elsevier. Bird, Richard. 2010. Pearls of Functional Algorithm Design. Cambridge University Press. Church, Alonzo. 1941. The Calculi of Lambda-Conversion. Princeton University Press. Claessen, Koen, and John Hughes. 2000. “QuickCheck: A Lightweight Tool for Random Testing of Haskell Programs.” In ACM SIGPLAN Notices, 268–79. ACM Press. doi:10.1145/357766.351266. Gibbons, Jeremy, and Oege de Moor, eds. 2003. The Fun of Programming. Palgrave Macmillan. Girard, Jean-Yves. 1972. “Interprétation Fonctionelle et élimination Des Coupures de L’arithmétique d’ordre Supérieur.” PhD thesis, Université Paris VII. Harper, Robert. 2012. Practical Foundations for Programming Languages. Cambridge University Press. Hinze, Ralf, and Ross Paterson. 2006. “Finger Trees: A Simple General-Purpose Data Structure.” Journal of Functional Programming 16 (02). Cambridge Univ Press: 197–217. doi:10.1017/S0956796805005769. Hudak, P., and M.P. Jones. 1994. “Haskell Vs. Ada Vs. C++ Vs. Awk Vs. . An Experiment in Software Prototyping Productivity.” Research Report YALEU/DCS/RR-1049. New Haven, CT: Department of Computer Science, Yale University. Hutton, Graham. 2007. Programming in Haskell. Cambridge University Press. McBride, Conor, and Ross Paterson. 2008. “Applicative Programming with Effects.” Journal of Functional Programming 18 (01). Cambridge Univ Press: 1–13. Moggi, Eugenio. 1991. “Notions of Computation and Monads.” Information and Computation 93 (1). Elsevier: 55–92. Okasaki, C. 1999. Purely Functional Data Structures. Cambridge University Press. Peyton Jones, Simon. 2001. “Tackling the Awkward Squad: Monadic Input/output, Concurrency, Exceptions, and Foreign-Language Calls in Haskell.” In Engineering Theories of Software Construction, edited by Tony Hoare, Manfred Broy, and Ralf Steinbruggen, 180:47. IOS Press. Pierce, Benjamin C. 2002. Types and Programming Languages. MIT press. Reynolds, John C. 1974. “Towards a Theory of Type Structure.” In Programming Symposium, edited by B.Robinet, 19:408–25. Lecture Notes in Computer Science. Springer. ———. 1983. “Types, Abstraction and Parametric Polymorphism.” Information Processing. Strachey, Christopher. 2000. “Fundamental Concepts in Programming Languages.” Higher-Order and Symbolic Computation 13 (1-2). Springer: 11–49. Swierstra, S Doaitse. 2009. “Combinator Parsing: A Short Tutorial.” In Language Engineering and Rigorous Software Development, 252–300. Springer. doi:10.1.1.184.7953. Wadler, Philip. 1989. “Theorems for Free!” In Proceedings of the Fourth International Conference on Functional Programming Languages and Computer Architecture, 347–59. ———. 1992a. “Comprehending Monads.” Mathematical Structures in Computer Science 2 (04). Cambridge Univ Press: 461–93. ———. 1992b. “The Essence of Functional Programming.” In POPL ’92: Conference Record of the Nineteenth Annual ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages, 1–14. ACM. Yorgey, Brent. 2009. “The Typeclassopedia.” The Monad. Reader 13: 17. Yorgey, Brent A. 2012. “Monoids: Theme and Variations (Functional Pearl).” In Proceedings of the 2012 Haskell Symposium, 105–16. Haskell ’12. Copenhagen, Denmark. doi:10.1145/2364506.2364520.
还剩193页未读

继续阅读

pdf贡献者

398818703

贡献于2017-03-02

下载需要 10 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!