在Scala中将F 有界多态性与继承结合起来
我在学习Scala。作为学习的一部分,我决定为事件建立一个类型系统。一个对象(通过trait或一个类型类实现的任意类)可以有多个事件订阅者(事件现在用字符串表示)。一个订阅者(或处理程序)是一个包装函数的对象。处理程序应该是逆变的,也就是说一个对象可以绑定到它自身类型或任意父类型的订阅者(想象一个按钮既可以有按钮特定的处理程序,也可以有通用小部件的处理程序)。
这是Handler类型定义:
class Handler[-A](val name: String = null)(val fn: A => Unit) {
def apply(obj: A): Unit = fn(obj)
}
于是我直接遇到了著名的“self type”(自类型)问题。我尝试用F-bound多态来解决它:
trait Eventable[A <: Eventable[A]] { this: A =>
private val subscribers = mut.HashMap[String, mut.Buffer[Handler[A]]]()
def subscribe(event: String, handler: Handler[A]): Unit = {
subscribers.getOrElseUpdate(event, ArrayBuffer()).append(handler)
}
def fireEvent(event: String): Unit = {
subscribers.get(event).foreach{subs =>
for(sub <- subs) { sub(this) }
}
}
}
object Eventable {
def fireAll(event: String, eventables: Seq[? <: Eventable[?]]): Unit = {
for(e <- eventables) { e.fireEvent(event) }
}
}
问题在于这在继承时不起作用:
class Thing(val name: String) extends Eventable[Thing]
class Gadget(name: String, val temp: Int) extends Thing(name)
// ...
val thing = Thing("Item")
val gadget = Gadget("Gadget", 85)
val handler1 = Handler() { (a: Thing) => println(a.name) }
val handler2 = Handler() { (a: Object) => println(a.hashCode()) }
val handler4 = Handler("GadgetHandler"){ (a: Gadget) => println(s"${a.name} [${a.temp}]") }
thing.subscribe("evt", handler1)
thing.subscribe("evt", handler2)
// gadget.subscribe("evt", handler4) — This doesn't compile because gadget wants Handler[Thing]
而且由于基类冲突,没法把 Gadget 定义为继承自 Thing(name) with Eventable[Gadget]。
有没有办法解决这个?
解决方案
两个最小的改动就能让它工作:
- 让
Eventable在A上实现协变:trait Eventable[+A <: Eventable[A]]。 - 在子类处重新混入trait:
class Gadget(...) extends Thing(name) with Eventable[Gadget]。
你可以在 这里的Scastie 上试试代码。
为什么它现在能编译
在一个不变的 Eventable[A] 下,Gadget 将通过 Thing 继承 Eventable[Thing],并且直接继承 Eventable[Gadget],编译器会把这两者视为冲突的基类型并拒绝(正如你已经注意到的那样)。把 A 标记为协变会使 Eventable[Gadget] <: Eventable[Thing],于是这两个基类型就会调和为更具体的 Eventable[Gadget]。
你可以在 Tour of Scala 了解更多关于变异性的内容。
变异性为什么重要?
变异性注解是一种关于可替换性的承诺:一个 Eventable[Gadget] 可以替代一个 Eventable[Thing],编译器的任务是确保这一承诺不会被利用来在运行时破坏类型安全。经典的陷阱是一个可变的协变容器:如果一个 Buffer[Gadget] 能作为一个 Buffer[Thing] 使用,你可能插入一个裸露的 Thing,随后又把它读回成一个 Gadget。因此,+A 参数只允许出现在输出(协变)的位置,绝不出现在输入(逆变或不变)的位置。一个相关的历史例子是Java的数组(参见 ArrayStoreException 并查看 Why are arrays covariant but generics are invariant?)。
为什么它是正确的
乍一看规则似乎错了:A 出现在 subscribe 的参数中,属于输入位置。但这个参数并不是一个 A,它是一个 Handler[A],而一个 Handler[A] 是需要在其中接受一个 A 的东西(这也是 -A 在 Handler 上声明的)。因此 subscribe 并不是在输入中接受一个 A,而是接受一个对 A 的消费者。每一层都只翻转一次方向:参数槽翻转了方向,而 Handler 的 -A 再次翻转,两次翻转抵消,最终把 A 留在“输出”位置。因此 +A 是可以的。
具体来说:一个 Gadget 是一个 Thing,因此它可以安全地接受任何处理 Thing 的处理程序——但一个普通的 Thing 不能接受只处理 Gadget 的处理程序,它会读取不存在的 .temp。协变正是强制如此:把一个 Gadget 视作一个 Eventable[Thing],让 subscribe 接受一个 Handler[Thing],而永远不接受一个 Handler[Gadget]。因此 thing.subscribe("evt", handler4) 被拒绝。
有一个微妙之处:+A 的类型检查之所以通过,是因为 subscribers 是 private。这个字段把 A 放在一个不变位置(mutable.Buffer[Handler[A]]),而 +A 通常会禁止这样。Scala之所以允许,是因为仅通过 this 访问的 private 成员被视为 private[this],且这类成员会从方差检查中排除(参考)。
这一豁免与访问路径有关:this 总是具备确切的类型 Eventable[A],从不是一个方差被放宽的视图,因此不可变位置的字段不能通过向上转型访问。将其暴露为除 this 之外的任意形式(即将其设为 protected 或公开),或在另一实例上读取它,豁免就会被撤销:
covariant type A occurs in invariant position in type scala.collection.mutable.HashMap[String, scala.collection.mutable.Buffer[Playground.Handler[A]]] of value subscribers