有界变体泛型变为Any?

时间:2017-12-31 23:11:02

标签: scala generics variance type-bounds

我正在修补一些仿制品,懒惰和暗示,并且撞墙,我非常确定只与我的泛型类型有关(但我可能错了......)我试图建立一个像流一样的东西:

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和#:: behavior中使用了界限,每个MyStream都退化为MyStream [Any]。

但是,如果我选择天真的仿制药:

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

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

我认为我非常密切地关注了Martin Odersky在List中的方差背景下给出的一个例子,这里唯一的关键区别似乎是"静态"我的缺点/#::操作的本质(我相信是必不可少的,因为我不认为我可以有一个"懒惰这个"(从概念上讲,这似乎是不可能的)至少我!

我错过了什么?

2 个答案:

答案 0 :(得分:1)

我有几点意见。首先,声称

  

有一点是,我在cons和#:: behavior中使用的界限,每个MyStream都退化为MyStream [Any]。

实际上不是真的。您可以在live demo处自己查看。请注意ssGood如何轻松分配给已键入的ssGood2,而无需进行投射,而ssBad明确键入MyStream[Any]则无法执行此操作。这里的要点是Scala编译器在这种情况下获得的类型非常正确。我 怀疑 你真正的意思是Intellij IDEA推断错误类型并做一些不好的突出显示等。由于技术原因,IDEA使用自己的编译器而不是标准编译器当代码复杂时,有时会出错。有时您实际上必须编译代码以查看它是否正确。

关于天真仿制品的第二个主张对我来说也不正确。

  

但是,如果我选择天真的仿制药:

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

  

类型保持稳定,但我不能使用cons /#::将任何内容附加到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编译没有错误(ss2使用cons编译即使没有U,因为+T正常工作)。

答案 1 :(得分:0)

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

enter image description here

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

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