传递路径依赖类型无法保留依赖值

Dan*_*hin 7 scala path-dependent-type

考虑以下:

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
}
Run Code Online (Sandbox Code Playgroud)

exec(p)(a) 无法编译并显示错误消息:

错误:(17,40)类型不匹配;
实测值:a.type(与下面的A型$ A2.this.Platform#拱)
需要:p.Arch .flatMap {情况下(P,A)=> EXEC(P)的(a)}

从错误消息来看,似乎scalac无法保留依赖于的值(p)Arch,因此它选择键入投影(尽管我不太确定是什么A$A2.this)的意思.

为了它的价值,用以下代码替换最后一行将编译:

.flatMap(p => exec(p)(p.parseArch("arm64").get))
Run Code Online (Sandbox Code Playgroud)

这是scala编译器的限制还是我在这里遗漏了一些东西?

LP_*_*LP_ 6

简单的解决方案

处理路径依赖类型时最好的选择是始终保持所有者的价值,因为Scala的推理和推理能力非常有限.

例如,您的代码示例可以重写为:

Platform.parse("ios") flatMap {
   p => p.parseArch("arm64").map(exec(p))
}
Run Code Online (Sandbox Code Playgroud)

通常可以执行这样的重写,尽管代码通常会变得不那么简洁和优雅.通常的做法是使用依赖函数和参数类.

使用依赖类型

在您的示例中,代码:

Platform.parse("ios").flatMap(p => p.parseArch("arm64").map(a => (p, a)))
Run Code Online (Sandbox Code Playgroud)

有类型Option[(Platform, Platform#Arch)],因为Scala的推断不能保留元组的第二个元素依赖于第一个元素的事实.(你得到的A$A2.this.Platform是因为你Platform在某些内部环境中声明了.)

换句话说,Scala的Tuple2类型不依赖.我们可以通过创建自己的类来纠正这个问题:

case class DepPair(p: Platform)(a: p.Arch)
Run Code Online (Sandbox Code Playgroud)

但是,Scala还不支持依赖类签名,也不会编译.相反,我们设置使用特征:

trait Dep {
  val plat: Platform
  val arch: plat.Arch
}
Run Code Online (Sandbox Code Playgroud)
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) }
Run Code Online (Sandbox Code Playgroud)

注意上面的ascription,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]
Run Code Online (Sandbox Code Playgroud)

斯卡拉推断一个存在的函数类型,但是我们想实际上是有存在量化内部函数类型.我们必须引导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) }
Run Code Online (Sandbox Code Playgroud)

现在我会让你决定哪种方式是最好的:坚持与主人val(简单的解决方案),或冒着失去你已经离开的任何理智的风险!

但谈到失去理智和存在感,让我们再尝试进一步研究......

使用存在(失败)

您的代码中的中间结果有问题的类型是Option[(Platform, Platform#Arch)].实际上有一种方法可以更好地表达它,使用存在主义,如:

Option[(p.type, p.Arch) forSome {val p: Platform}]
Run Code Online (Sandbox Code Playgroud)

我们可以通过显式指定它来帮助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) }}
Run Code Online (Sandbox Code Playgroud)

但是,我们现在触摸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
Run Code Online (Sandbox Code Playgroud)

另一项试验:

tmp.flatMap {
  (tup: (p.type, p.Arch) forSome {val p: Platform}) => exec(tup._1)(tup._2)
}
Run Code Online (Sandbox Code Playgroud)
Error:(32, 79) type mismatch;
 found   : tup._2.type (with underlying type p.Arch)
 required: tup._1.Arch
Run Code Online (Sandbox Code Playgroud)

在这一点上,我认为任何合理的人都会放弃 - 并且可能会远离Scala编程几天;-)