如何在存在高级类型的情况下控制模式匹配中绑定变量的推断类型



(这是基于 http://bertails.org/2015/02/15/abstract-algebraic-data-type 的文章)

首先,我正在定义scala.Option的抽象版本。

import scala.language.higherKinds
trait OptionSig {
  type Option[+_]
  type Some[+A] <: Option[A]
  type None <: Option[Nothing]
}
abstract class OptionOps[Sig <: OptionSig] extends Extractors[Sig] {
  def some[A](x: A): Sig#Some[A]
  def none: Sig#None
  def fold[A, B](opt: Sig#Option[A])(ifNone: => B, ifSome: A => B): B
}

我希望能够在Sig#Option[A]上使用模式匹配Extractors看起来像这样:

trait Extractors[Sig <: OptionSig] { self: OptionOps[Sig] =>
  object Some {
    def unapply[A](opt: Sig#Option[A]): scala.Option[A] =
      fold(opt)(scala.None, a => scala.Some(a))
  }
  object None {
    def unapply[A](opt: Sig#Option[A]): Option[Unit] =
      fold(opt)(scala.Some(()), _ => scala.None)
  }
}

现在我可以编写这个程序了:

class Program[Sig <: OptionSig](implicit ops: OptionOps[Sig]) extends App {
  import ops._
  val opt: Sig#Option[Int] = some(42)
  opt match {
    case None(_)  => sys.error("")
    case Some(42) => println("yay")
    case Some(_)  => sys.error("")
  }
}

我可以用这个实现来测试它。

trait ScalaOption extends OptionSig {
  type Option[+A] = scala.Option[A]
  type Some[+A]   = scala.Some[A]
  type None       = scala.None.type
}
object ScalaOption {
  implicit object ops extends OptionOps[ScalaOption] {
    def some[A](x: A): ScalaOption#Some[A] = scala.Some(x)
    val none: ScalaOption#None = scala.None
    def fold[A, B](opt: ScalaOption#Option[A])(ifNone: => B, ifSome: A => B): B =
      opt match {
        case scala.None    => ifNone
        case scala.Some(x) => ifSome(x)
      }
  }
}
object Main extends Program[ScalaOption]

看起来它有效,但有一件烦人的事情我无法弄清楚。

scala.OptionOption(42) match { case s @ Some(42) => s }中的s类型是Some[Int]。但是通过我上面的片段,它Sig#Option[Int],我想让它Sig#Some[Int]

因此,我尝试了以下内容,以更接近 scalac 为其案例类生成的内容:

trait Extractors[Sig <: OptionSig] { self: OptionOps[Sig] =>
  object Some {
    def unapply[A](s: Sig#Some[A]): scala.Option[A] =
      fold(s)(scala.None, a => scala.Some(a))
  }
  object None {
    def unapply(n: Sig#None): Option[Unit] =
      fold(n)(scala.Some(()), (_: Any) => scala.None)
  }
}

但是现在我收到如下警告:

[warn] Main.scala:78: abstract type pattern Sig#None is unchecked since it is eliminated by erasure
[warn]     case None(_)  => sys.error("")

我不确定为什么会发生这种情况,因为Sig#NoneSig#Option[Int]的子类型,并且在编译时已知

此外,运行时还可以,但推断的类型仍然不是我期望的类型。

所以问题是

  • 为什么不顾子类型信息,这里还是提到了类型擦除?
  • 如何在(some(42): Sig#Option[Int]) match { case s @ Some(42) => s }获得s Sig#Option[Int]

不幸的是,你不能做你想做的事。问题是,仅仅 scalac 知道Sig#None <: Sig#Option[A]是不够的,它必须能够验证它传递给unapply的值实际上是Sig#None的。这适用于scala.Option,因为编译器可以生成一个instanceof检查,以验证一个类型实际上是SomeNone,然后再将其传递给unapply方法。如果检查失败,它将跳过该模式(并且永远不会调用unapply)。

在您的情况下,由于 scalac 只知道 opt 是一个Sig#Option[Int],因此在将值传递给 unapply 之前,它无法保证该值实际上是SomeNone

那么它有什么作用呢?无论如何,它都会传递值!这是什么意思?好吧,让我们稍微修改一下您的提取器:

trait Extractors[Sig <: OptionSig] { self: OptionOps[Sig] =>
  object Some {
    def unapply[A](s: Sig#Some[A]): scala.Option[A] =
      fold(s)(scala.None, a => scala.Some(a))
  }
  object None {
    def unapply(n: Sig#None): Option[Unit] =
      scala.Some(())
  }
}

我们所做的只是在None的情况下停止使用fold。既然我们知道论证一定是Sig#None,为什么还要费心打电话给fold,对吧?我的意思是,我们不希望在这里通过Sig#Some,对吧?

当我们运行此示例时,您将遇到RuntimeException,因为我们的第一个模式匹配成功并调用???。在scala.Option的情况下,模式会失败,因为它是由生成的instanceof检查保护的。

我举了另一个例子,展示了另一种危险,我们向Sig#Some添加一个小约束,让我们也避免Some情况下的fold:https://gist.github.com/tixxit/ab99b741d3f5d2668b91

无论如何,您的特定情况在技术上是安全的。我们知道您使用了fold,因此可以安全地与Sig#Option一起使用。问题是scalac不知道这一点。

最新更新