1

我正在修补一些泛型、懒惰和隐式,并碰上了我很确定只与我的泛型类型的边界有关的墙(但我可能错了......)我试图构建一个 Stream-Like事物:

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 中的方差背景下给出的示例,这里唯一的关键区别似乎是我的 cons / #:: 操作的“静态”性质(我相信这是必不可少,因为我认为我不能拥有“懒惰的这个”(从概念上讲,至少对我来说这似乎是不可能的!

我错过了什么?

4

2 回答 2

1

我有几点。首先,声称

一件事是我在 cons 和 #:: 行为中使用的界限,每个 MyStream 都会退化为 MyStream[Any]。

实际上是不正确的。您可以在此现场演示中亲自观看。请注意如何ssGood轻松地将其分配给类型化ssGood2而无需强制转换,并且您无法使用ssBad显式类型化 as来执行此操作MyStream[Any]。这里的重点是 Scala 编译器在这种情况下得到了非常正确的类型。我怀疑您的实际意思是 Intellij IDEA 推断出错误的类型并进行了一些不好的突出显示等。不幸的是,出于技术原因,IDEA 使用自己的编译器而不是标准编译器,并且有时在代码复杂时会出错。有时您实际上必须编译代码以查看它是否正确。

关于天真的泛型的第二个说法对我来说也不正确。

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

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

类型保持稳定,但我不能使用 cons / #:: 将任何内容附加到 MyStream.empty ...

当我使用以下代码时(可在线获取

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但不必添加到. 所以用下面的定义#::MyStreamOpsconsMyStream

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编译没有错误(并且即使没有完全因为工作ss2也使用编译)。consU+T

于 2018-01-01T02:01:26.507 回答
0

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

在此处输入图像描述

我已经注意到 IntelliJ 工作表功能存在一些“问题”(有时建议进行语法错误的重构),但这是我第一次在“真正的编译器”部分看到它失败。

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

于 2018-01-01T16:46:15.783 回答