有界变体泛型变成 Any?

Bounded variant generic becomes Any?

我正在修补一些泛型、懒惰和隐式,但碰壁了,我很确定这只与我的泛型类型的边界有关(但我可能错了......)我试图构建类似 Stream 的东西:

object MyStream {
  def empty = new MyStream[Nothing] {
    def isEmpty = true
    def head = throw new NoSuchElementException("tead of empty MyStream")
    def tail = throw new NoSuchElementException("tail of empty MyStream")
  }

  def cons[T, U >: T](h: U, t: => MyStream[T]): MyStream[U] = new MyStream[U] {
    def isEmpty = false
    def head = h
    lazy val tail = t
  }

  implicit class MyStreamOps[T](t: => MyStream[T]) {
    def #:: [U >: T](h: U): MyStream[U] =
      cons(h, t)
  }
}

abstract class MyStream[+T] {
  def isEmpty: Boolean
  def head: T
  def tail: MyStream[T]
  @tailrec final def foreach(op: T => Unit): Unit = {
    if (!isEmpty) {
      op(head)
      tail.foreach(op)
    }
  }
}

它实际上似乎工作得很好,除了一件事,(至少就我的测试而言是这样,所以我可能会遗漏其他问题)。一件事是,由于我在 cons 和 #:: 行为中使用了界限,每个 MyStream 都会退化为 MyStream[Any].

但是,如果我使用天真的泛型:

def cons[T](h: T, t: => MyStream[T]): MyStream[T] = new MyStream[T] ...

类型保持稳定,但我不能使用 cons / #:: 将任何内容附加到 MyStream.empty,因为那是 MyStream[Nothing],我的类型也不能有任何其他变体使用这些操作(这显然会破坏事情)。

我认为我正在相当密切地关注 Martin Odersky 在 List 的方差上下文中给出的示例,这里唯一的关键区别似乎是我的缺点 / # 的 "static" 性质: : 操作(我 相信 是必不可少的,因为我不认为我可以拥有 "lazy this" (从概念上讲,至少对我来说这似乎是不可能的!

我错过了什么?

我有几点。首先,声称

That one thing is that with the bounds I've used in the cons and #:: behaviors, every MyStream degenerates to a MyStream[Any].

其实不然。你可以在这个 live demo 看到它。请注意如何将 ssGood 轻松分配给类型化的 ssGood2 而无需强制转换,并且您不能将 ssBad 显式类型化为 MyStream[Any]。这里的要点是 Scala 编译器在这种情况下非常正确地获取类型。我 怀疑 你真正的意思是 Intellij IDEA 推断出错误的类型并做了一些不好的突出显示等。不幸的是,出于技术原因,IDEA 使用自己的编译器而不是标准编译器,当代码复杂时有时会出错。有时您实际上必须编译代码以查看其是否正确。

第二个关于朴素泛型的说法在我看来也不正确。

However, if I go with naive generics:

def cons[T](h: T, t: => MyStream[T]): MyStream[T] = new MyStream[T] ...

The types remain stable, but I can't use cons / #:: to attach anything to a MyStream.empty ...

当我使用下面的代码时(available online)

object MyStream {
  val empty: MyStream[Nothing] = new MyStream[Nothing] {
    override def isEmpty = true

    override def head = throw new NoSuchElementException("tead of empty MyStream")

    override def tail = throw new NoSuchElementException("tail of empty MyStream")
  }

  def cons[T](h: T, t: => MyStream[T]): MyStream[T] = new MyStream[T] {
    def isEmpty = false

    def head = h

    lazy val tail = t
  }

  implicit class MyStreamOps[T](t: => MyStream[T]) {
    def #::(h: T): MyStream[T] = cons(h, t)
  }

}

abstract class MyStream[+T] {
  def isEmpty: Boolean

  def head: T

  def tail: MyStream[T]

  @tailrec final def foreach(op: T => Unit): Unit = {
    if (!isEmpty) {
      op(head)
      tail.foreach(op)
    }
  }
}

import MyStream._

val ss0 = 1 #:: empty
val ss1: MyStream[Int] = ss0
val ss2: MyStream[Int] = 1 #:: empty

只要有 [+T] 就可以编译和运行 MyStream[+T] 声明。这次我不确定你到底做错了什么(而且你没有提供任何实际的编译器错误,所以很难猜到)。

此外,如果您的 empty 是非通用且不可变的,则无需 def - 它也可以是 val

如果您仍然遇到一些问题,您可能应该提供有关如何重现它以及您遇到的错误的更多详细信息。


更新(回复评论)

托比,抱歉,我还是不明白您的问题 #2。您能否提供一个无法在您的问题或评论中编译的代码示例?

我唯一的猜测是,你的意思是,如果你只使用一个通用的代码 T 作为主要答案,那么一段这样的代码会失败:

def test() = {
  import MyStream._

  val ss0: MyStream[String] = "abc" #:: empty
  val sb = new StringBuilder
  val ss1: MyStream[CharSequence] = ss0                          //OK
  val ss2: MyStream[CharSequence] = cons(sb, ss0)                //OK
  val ss3: MyStream[CharSequence] = sb #:: ss0                   //Bad?
}

是的,这是真的,因为 AFAIU Scala 编译器在检查隐式包装器时不会尝试遍历所有泛型类型的所有可能替代品,而只使用最具体的替代品。所以 ss0 被尝试转换为 MyStreamOps[String] 而不是 MyStreamOps[CharSequence]。要解决该问题,您需要将另一个泛型类型 U >: T 添加到 MyStreamOps 中的 #::,但不必添加到 cons。因此,使用以下 MyStream 定义

object MyStream {
  val empty: MyStream[Nothing] = new MyStream[Nothing] {
    override def isEmpty = true

    override def head = throw new NoSuchElementException("tead of empty MyStream")

    override def tail = throw new NoSuchElementException("tail of empty MyStream")
  }

  def cons[T](h: T, t: => MyStream[T]): MyStream[T] = new MyStream[T] {
    def isEmpty = false

    def head = h

    lazy val tail = t
  }

  implicit class MyStreamOps[T](t: => MyStream[T]) {
    //def #::(h: T): MyStream[T] = cons(h, t)  // bad
    def #::[U >: T](h: U): MyStream[U] = cons(h, t) //good
  }  
}

即使 ss3 编译也没有错误(使用 consss2 即使没有 U 也能编译,正是因为 +T 有效)。

所以,我上面的第一点似乎反映了 IntelliJ 编译器中的错误 SergGr 上面的回答指出他没有看到问题。而且,果然,如果我使用相同的代码并在命令行上编译它,它会完美运行。然而,这就是 IntelliJ 向我展示的内容:

我已经注意到 IntelliJ 工作表功能有一些 "issues"(有一次暗示重构在语法上是错误的),但这是我第一次看到它在 "real compiler"节。

FWIW,这是 IntelliJ 2017.3.2 CE,在 Open JDK 1.8.0 上似乎是 运行(我没有把它放在那里——我正在使用 Java 9 用于 Java 工作),所以我认为它是 IntelliJ 捆绑的 JVM)和 Scala 2.11.6.