企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
上一章介绍了类型类的概念,这种模式使设计出来的程序既拥抱扩展性,又不放弃具体的类型信息。这一章,我们还将继续探究 Scala 的类型系统,讲讲另一个特性,这个特性可以将 Scala 与其他主流编程语言区分开:依赖类型,特别是,路径依赖的类型和依赖方法类型。 一个广泛用于反对静态类型的论点是 “the compiler is just in the way”,最终得到的都是数据,为什么还要建立一个复杂的类型层次结构? 到最后,静态类型的唯一目的就是,让“超级智能”的编译器来定期“羞辱”编程人员,以此来预防程序的 bug,在事情变得糟糕之前,保证你做出正确的选择。 路径依赖类型是一种强大的工具,它把只有在运行期才知道的逻辑放在了类型里,编译器可以利用这一点减少甚至防止 bug 的引入。 有时候,意外的引入路径依赖类型可能会导致难堪的局面,尤其是当你从来没有听说过它。因此,了解和熟悉它绝对是个好主意,不管以后要不要用。 ### 问题 先从一个问题开始,这个问题可以由路径依赖类型帮我们解决:在同人小说中,经常会发生一些骇人听闻的事情。比如说,两个主角去约会,即使这样的情景有多么的不合常理,甚至还有穿越的同人小说,两个来自不同系列的角色互相约会。 不过,好的同人小说写手对此是不屑一顾的。肯定有什么模式来阻止这样的错误做法。下面是这种领域模型的初版: ~~~ object Franchise { case class Character(name: String) } class Franchise(name: String) { import Franchise.Character def createFanFiction( lovestruck: Character, objectOfDesire: Character): (Character, Character) = (lovestruck, objectOfDesire) } ~~~ 角色用 `Character` 样例类表示, `Franchise` 类有一个方法,这个方法用来创建有关两个角色的小说。下面代码创建了两个系列和一些角色: ~~~ val starTrek = new Franchise("Star Trek") val starWars = new Franchise("Star Wars") val quark = Franchise.Character("Quark") val jadzia = Franchise.Character("Jadzia Dax") val luke = Franchise.Character("Luke Skywalker") val yoda = Franchise.Character("Yoda") ~~~ 不幸的是,这一刻,我们无法阻止不好的事情发生: ~~~ starTrek.createFanFiction(lovestruck = jadzia, objectOfDesire = luke) ~~~ 多么恐怖的事情!某个人创建了一段同人小说,婕琪戴克斯和天行者卢克竟然在约会!我们不应该容忍这样的事情。 > 婕琪戴克斯:星际迷航中的角色:[http://en.wikipedia.org/wiki/Jadzia_Dax](http://en.wikipedia.org/wiki/Jadzia_Dax)天行者卢克:星球大战中的角色:[http://en.wikipedia.org/wiki/Luke_Skywalker](http://en.wikipedia.org/wiki/Luke_Skywalker) 你的第一直觉可能是,在运行期做一些检查,保证约会的两个角色来自同一个特许商。比如说: ~~~ object Franchise { case class Character(name: String, franchise: Franchise) } class Franchise(name: String) { import Franchise.Character def createFanFiction( lovestruck: Character, objectOfDesire: Character): (Character, Character) = { require(lovestruck.franchise == objectOfDesire.franchise) (lovestruck, objectOfDesire) } } ~~~ 现在,每个角色都有一个指向所属发行商的引用,试图创建包含不同系列角色的小说会引发 `IllegalArgumentException` 异常。 ### 路径依赖类型 这挺好,不是吗?毕竟这是被灌输多年的行为方式:快速失败。然而,有了 Scala,我们能做的更好。有一种可以更快速失败的方法,不是在运行期,而是在编译期。为了实现它,我们需要将 `Character` 和它的 `Franchise` 之间的联系编码在类型层面上。 Scala **嵌套类型** 工作的方式允许我们这样做。一个嵌套类型被绑定在一个外层类型的实例上,而不是外层类型本身。这意味着,如果将内部类型的一个实例用在包含它的外部类型实例外面,会出现编译错误: ~~~ class A { class B var b: Option[B] = None } val a1 = new A val a2 = new A val b1 = new a1.B val b2 = new a2.B a1.b = Some(b1) a2.b = Some(b1) // does not compile ~~~ 不能简单的将绑定在 `a2` 上的类型 `B` 的实例赋值给 `a1` 上的字段:前者的类型是 `a2.B` ,后者的类型是 `a1.B` 。中间的点语法代表类型的路径,这个路径通往其他类型的具体实例。因此命名为路径依赖类型。 下面的代码运用了这一技术: ~~~ class Franchise(name: String) { case class Character(name: String) def createFanFictionWith( lovestruck: Character, objectOfDesire: Character): (Character, Character) = (lovestruck, objectOfDesire) } ~~~ 这样,类型 `Character` 嵌套在 `Franchise` 里,它依赖于一个特定的 `Franchise` 实例。 重新创建几个角色和发行商: ~~~ val starTrek = new Franchise("Star Trek") val starWars = new Franchise("Star Wars") val quark = starTrek.Character("Quark") val jadzia = starTrek.Character("Jadzia Dax") val luke = starWars.Character("Luke Skywalker") val yoda = starWars.Character("Yoda") ~~~ 把角色放在一起构成小说: ~~~ starTrek.createFanFictionWith(lovestruck = quark, objectOfDesire = jadzia) starWars.createFanFictionWith(lovestruck = luke, objectOfDesire = yoda) ~~~ 顺利编译!接下来,试着去把 `jadzia` 和 `luke` 放在一起: ~~~ starTrek.createFanFictionWith(lovestruck = jadzia, objectOfDesire = luke) ~~~ 不应该的事情就会编译失败!编译器抱怨类型不匹配: ~~~ found : starWars.Character required: starTrek.Character starTrek.createFanFictionWith(lovestruck = jadzia, objectOfDesire = luke) ~~~ 即使这个方法不是在 `Franchise` 中定义的,这项技术同样可用。这种情况下,可以使用依赖方法类型,一个参数的类型信息依赖于前面的参数。 ~~~ def createFanFiction(f: Franchise)(lovestruck: f.Character, objectOfDesire: f.Character) = (lovestruck, objectOfDesire) ~~~ 可以看到, `lovestruck` 和 `objectOfDesire` 参数的类型依赖于传递给该方法的 `Franchise` 实例。不过请注意:被依赖的实例只能在一个单独的参数列表里。 ### 抽象类型成员 依赖方法类型通常和抽象类型成员一起使用。假设我们在开发一个键值存储,只支持读取和存放操作,但是类型安全的。下面是一个简化的实现: ~~~ object AwesomeDB { abstract class Key(name: String) { type Value } } import AwesomeDB.Key class AwesomeDB { import collection.mutable.Map val data = Map.empty[Key, Any] def get(key: Key): Option[key.Value] = data.get(key).asInstanceOf[Option[key.Value]] def set(key: Key)(value: key.Value): Unit = data.update(key, value) } ~~~ 我们定义了一个含有抽象类型成员 `Value` 的类 `Key`。`AwesomeDB` 中的方法可以引用这个抽象类型,即使不知道也不关心它到底是个什么表现形式。 定义一些想使用的具体的键: ~~~ trait IntValued extends Key { type Value = Int } trait StringValued extends Key { type Value = String } object Keys { val foo = new Key("foo") with IntValued val bar = new Key("bar") with StringValued } ~~~ 之后,就可以存放键值对了: ~~~ val dataStore = new AwesomeDB dataStore.set(Keys.foo)(23) val i: Option[Int] = dataStore.get(Keys.foo) dataStore.set(Keys.foo)("23") // does not compile ~~~ ### 实践中的路径依赖类型 在典型的 Scala 代码中,路径依赖类型并不是那么无处不在,但它确实是有很大的实践价值的,除了给同人小说建模之外。 最普遍的用法是和 **cake pattern** 一起使用,cake pattern 是一种组件组合和依赖管理的技术。冠以这一点,可以参考 Debasish Ghosh 的 [文章](http://debasishg.blogspot.ie/2013/02/modular-abstractions-in-scala-with.html) 。 把一些只有在运行期才知道的信息编码到类型里,比如说:异构列表、自然数的类型级别表示,以及在类型中携带大小的集合,路径依赖类型和依赖方法类型有着至关重要的角色。Miles Sabin 正在 [Shapeless](https://github.com/milessabin/shapeless) 中探索 Scala 类型系统的极限。