请考虑以下事项:
trait Platform {
type Arch <: Architecture
def parseArch(str: String): Option[Arch]
}
object Platform {
def parse(str: String): Option[Platform] = ???
}
trait Architecture
def main() {
def exec(p: Platform)(a: p.Arch) = ???
Platform.parse("ios")
.flatMap(p => p.parseArch("arm64").map(a => (p, a)))
.flatMap { case (p, a) => exec(p)(a) } // <----- This fails to compile
}
exec(p)(a)
无法编译,并显示错误消息:
错误:(17,40)类型不匹配;
发现:a.type(底层类型为A $ A2.this.Platform #Arch)
要求:p.Arch .flatMap {case(p,a)=&gt; exec(p)(a)}
从错误消息中,似乎scalac无法保留p
所依赖的值(Arch
),因此它选择键入投影(尽管我不太清楚是什么A$A2.this
)表示。
对于它的价值,用以下代码替换最后一行将编译:
.flatMap(p => exec(p)(p.parseArch("arm64").get))
这是scala编译器的限制,还是我在这里遗漏了什么?
答案 0 :(得分:6)
处理路径依赖类型时最好的选择是始终保持所有者的价值,因为Scala的推理和推理能力非常有限。
例如,您的代码示例可以重写为:
Platform.parse("ios") flatMap {
p => p.parseArch("arm64").map(exec(p))
}
通常可以执行这样的重写,尽管代码通常会变得不那么简洁和优雅。通常的做法是使用依赖函数和参数类。
在您的示例中,代码:
Platform.parse("ios").flatMap(p => p.parseArch("arm64").map(a => (p, a)))
具有类型Option[(Platform, Platform#Arch)]
,因为Scala的推断不能保留元组的第二个元素依赖于第一个元素的事实。 (你得到A$A2.this.Platform
,因为你在某些内部语境中声明了Platform
。)
换句话说,Scala的Tuple2
类型不依赖。我们可以通过创建自己的类来纠正这个问题:
case class DepPair(p: Platform)(a: p.Arch)
但是,Scala还不支持依赖类签名,也不会编译。相反,我们设置使用特征:
trait Dep {
val plat: Platform
val arch: plat.Arch
}
Platform.parse("ios")
.flatMap { p => p.parseArch("arm64").map { a =>
new Dep { val plat: p.type = p; val arch: p.Arch = a }}}
.flatMap { dep => exec(dep.plat)(dep.arch) }
请注意val plat
和val arch
上的归属,因为没有它们,Scala会尝试推断一种将使类型检查失败的精炼类型。
事实上,我们处于Scala(恕我直言)的合理范围内。例如,如果我们有参数化trait Dep[P <: Platform]
,我们就会遇到各种各样的问题。值得注意的是:
Error:(98, 15) type mismatch;
found : Platform => Option[Dep[p.type]] forSome { val p: Platform }
required: Platform => Option[B]
Scala推断出一种存在函数类型,但我们所希望的实际上是将内部存在函数类型。我们必须引导Scala了解这一点,我们最终会得到类似的结果:
Platform.parse("ios").flatMap[Dep[p.type] forSome { val p: Platform }]{
case p => p.parseArch("arm64").map{case a: p.Arch =>
new Dep[p.type] { val plat: p.type = p; val arch = a }}}
.flatMap { dep => exec(dep.plat)(dep.arch) }
现在我会让你决定哪种方式最好:坚持主人val
(简单的解决方案),或冒着失去理智感的风险!
但谈到失去理智和存在感,让我们再试一下......
代码中的中间结果有问题的类型是Option[(Platform, Platform#Arch)]
。
实际上有一种方法可以更好地表达它,使用存在主义,如:
Option[(p.type, p.Arch) forSome {val p: Platform}]
我们可以通过明确指定Scala来帮助Scala,因此中间结果具有预期的类型:
val tmp: Option[(p.type, p.Arch) forSome {val p: Platform}] =
Platform.parse("ios")
.flatMap { case p => p.parseArch("arm64").map { a => (p, a): (p.type, p.Arch) }}
但是,我们现在触摸Scala类型系统的一个非常敏感的区域,它经常会导致问题。事实上,我没有找到表达第二个flatMap
...
尝试tmp.flatMap { case (p, a) => exec(p)(a) }
非常有帮助:
Error:(30, 30) type mismatch;
found : a.type (with underlying type p.Arch)
required: p.Arch
另一项试验:
tmp.flatMap {
(tup: (p.type, p.Arch) forSome {val p: Platform}) => exec(tup._1)(tup._2)
}
Error:(32, 79) type mismatch;
found : tup._2.type (with underlying type p.Arch)
required: tup._1.Arch
此时,我认为任何合理的人都会放弃 - 并且可能会远离Scala编程几天; - )
答案 1 :(得分:3)
我已经学会了解scala编译器的当前限制(如 LP 的回答所示),而是想出了这个解决方法:
p
值得庆幸的是,内部特征可以引用scala中的外部特征。这样,就不需要将p.Arch
和a: Platform#Architecture
放在一起,而是每个p: Platform
都会引用自己的{{1}}。