【问题标题】:Bounded variant generic becomes Any?有界变体泛型变为 Any?
【发布时间】:2018-06-11 06:54:39
【问题描述】:

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

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 / #:: 操作的“静态”性质(其中我相信是必不可少的,因为我认为我不能拥有“懒惰的这个”(从概念上讲,至少对我来说这似乎是不可能的!

我错过了什么?

【问题讨论】:

    标签: scala generics variance type-bounds


    【解决方案1】:

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

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

    实际上是不正确的。您可以在live demo 亲自查看。请注意ssGood 是如何轻松分配给类型化的ssGood2 而不需要强制转换的,并且您不能使用明确类型化为MyStream[Any]ssBad 来执行此操作。这里的重点是 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]。要解决这个问题,您需要在MyStreamOps 中将另一个泛型类型U >: T 添加到#::,但不必添加到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 有效)。

    【讨论】:

    • 关于第 1 点,令人着迷。我不确定 SO 中的正确礼仪是什么,但我会添加一个“答案”来解决这个问题。在第 2 点上,我确实说错了——我一直在与两个问题作斗争,而第二个问题却在我的脑海中迷失了方向。打破的不是空的使用,但是当我尝试制作“子”类的流,然后在它前面添加一个“父”类时它失败了。有人会期望,因为 +T 部分处理“子流可分配给父流”,但它是 U >: T 边界事物,并允许 #:: 返回在这种情况下所需的父类型流.
    • @TobyEggitt,对不起,我还是不明白你的问题 #2。您能否提供一个未在此处或您的问题中编译的代码示例?在我的回答中的更新中查看更多详细信息。
    • 好吧,我不能在这里格式化,但是考虑一下这个用法: val p = new Parent("p") val c1 = new Child1("c1") val ps = p #:: MyStream.empty val pc1 = c1 #:: MyStream.empty val px = p #:: pc1 // This !
    • 如果这太神秘了,LMK 和我会想办法把更完整的代码放在那里。该示例将父级“添加”到 Stream[Child] 仅适用于使用 #:: [U >: T] ... 约束
    • @TobyEggitt, 1) 是的,cmets 中的代码看起来不太好。这就是为什么我建议在您需要大量代码时编辑问题(恕我直言,您当前的示例足够小,每行使用 ` (反引号)的单独代码部分可能可以替代格式如 val p = new Parent("p") val c1 = new Child1("c1") )。 2) 我不确定您的示例与我在更新中描述的示例有何不同(添加StringStringBuilder),原因是在这种情况下需要[U >: T]。如果您仍然不清楚,我不确定到底是什么。
    【解决方案2】:

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

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

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

    【讨论】:

      猜你喜欢
      • 2022-01-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-07-14
      • 2012-02-14
      • 2020-11-11
      • 1970-01-01
      • 2020-08-20
      相关资源
      最近更新 更多