【问题标题】:How to use case class define a extractor?如何用案例类定义提取器?
【发布时间】:2016-06-02 14:40:54
【问题描述】:

我想使用 List 定义的 case class match Seq[Byte],但是发生了编译器错误。

带有编译器错误的用例类

case class :: (head: Byte, tail: Seq[Byte])        
def doMatchWithCaseClass(queue: Seq[Byte]) = {    
  queue match {
    case h :: t => println("Good!") //can't compile
    case _ => println("God!>_<")
  }
}
doMatchWithCaseClass(Seq(1,2,3))

编译器错误:

Error:(93, 14) constructor cannot be instantiated to expected type;
 found   : ::
 required: Seq[Byte]
      case h :: t => println("Good!") //can't compile
             ^

使用@isaias-b 邮政编码更新

  final case class :::: (override val head: Int, override val tail: Seq[Int]) extends Seq[Int] {
    override def length: Int = tail.length + 1
    override def iterator: Iterator[Int] = (head :: tail.toList).toIterator
    override def apply(idx: Int) = {
      1.toByte // just for simple
    }
  }

匹配码:

  def doMatchWithCaseClass(queue: Seq[Int]) = {
    queue match {
      case h :::: t => println("case class - Good! ^_^")
      case x =>
        println(s"case class - God >_<! $x")
    }
  }

测试代码:

  doMatchWithCaseClass(Seq(1,2,3))

控制台结果:

> case class - God >_<! List(1, 2, 3)

上面的代码没有任何编译错误,但这不是我的预期结果。
希望有人能指出错误。谢谢

【问题讨论】:

    标签: scala pattern-matching extractor


    【解决方案1】:

    我不能 100% 确定这是否是所需的解决方案,但这似乎可行:

    case class ::(override val head:Int, override val tail: Seq[Int]) 
      extends Seq[Int] { 
        def iterator = ??? 
        def apply(idx: Int): Int = ???
        def length: Int = ??? 
    }
    

    正如编译器错误告诉您的那样,它找到了 :: 类型的实例,但需要 Seq[Byte]。我提供了一个带有Int 的示例,它现在扩展了Seq[Int],这让我可以更进一步。

    scala> case class ::(head:Int, tail: Seq[Int]) extends Seq[Int]
    <console>:10: error: overriding method head in trait IterableLike of type => Int;
     value head needs `override' modifier
           case class ::(head:Int, tail: Seq[Int]) extends Seq[Int]
                         ^
    

    所以我添加了override 关键字,在另一个错误之后,我也添加了val 关键字。然后还有 3 个给定的抽象方法需要定义,我提供了在控制台上打印的存根。这样就可以执行以下操作:

    scala> Seq(1,2,3) match { case ::(a, as) => "list"; case _ => "empty!" }
    res5: String = empty!
    

    希望在为 3 种必需方法提供正确实现时,这种情况会消失。但是,编译器不再抱怨了……

    更新 1

    @badcook 指出...

    您的 :::: 案例类不起作用,因为子类型可以说是“错误的方式”;虽然您的 :::: 是 Seq,但队列不是 :::: 并且后者在模式匹配中很重要。

    他是对的,当通常试图支持组合而不是继承时,扩展总是感觉很奇怪。现在是here is a comparison from Martin Odersky's online book about scala,它指出了使用case classes 实现的提取器与使用单独的un/applyun/applySeq 方法之间的区别。

    尽管它们非常有用,但案例类有一个缺点:它们暴露了数据的具体表示。

    用他的话说,将提取器与表示分离,利用表示独立性。这就引出了一个问题:这是因为技术限制的硬选择,还是软选择?大多数时候,这不是一个艰难的选择:

    那么对于模式匹配,您应该更喜欢这两种方法中的哪一种?这取决于。如果您为封闭的应用程序编写代码,案例类通常更可取,因为它们在简洁、速度和静态检查方面具有优势。如果您决定稍后更改类层次结构,则需要重构应用程序,但这通常不是问题。另一方面,如果您需要向未知客户端公开类型,则提取器可能更可取,因为它们保持表示独立性。

    幸运的是,您无需立即做出决定。您总是可以从案例类开始,然后,如果需要,可以更改为提取器。因为提取器上的模式和案例类上的模式在 Scala 中看起来完全一样,所以客户端中的模式匹配将继续有效。

    但是,现在有一部分描述了该指南存在例外情况!

    本章讨论的电子邮件地址就是这样一个例子。在这种情况下,提取器是唯一可能的选择。

    更新 2

    我也不是 100% 确定,如果这也适用于当前场景,如果您面临遇到此限制的情况。 @badcook 似乎知道这一点,因为他指出的另一件事是:

    根据您对其他回复的 cmets,您似乎已经了解提取器对象,并且实际上只是想看看 case 类是否可以节省您为预先存在的类型键入自己的 unapply 和 unapplySeq 方法的工作。

    很遗憾,答案是否定的。你必须使用 unapply 和/或 unapplySeq (它们还不错:))。

    我们后来澄清了这一点,所以我现在想自己也理解它。我将通过背诵自己来总结谈话的摘录。为此假设有以下case class Y(x: X)

    case class Y 中提供的 def unapply(y:Y):X 方法强烈绑定到它的输入和输出类型。将其他内容放入其中的唯一方法是使用继承。那就麻烦了。

    在我目前的理解中,这个问题被称为表示依赖

    【讨论】:

    • 我已经认识到 :: 应该是用 Seq 扩展的,所以很抱歉,代码得到了一个“上帝”而不是“好”......,希望有人来这里。
    • 太棒了!谢谢你整理这样的资源。正如你所说,我必须使用提取器来匹配Seq类型。最后,“但是,为什么”是什么意思?
    • 我已将新信息添加到帖子中,并删除了“为什么”。我想知道为什么我们必须在这里选择提取器而不是案例类。
    • 哈...,其实你提到的问题。我只是准备如下:如果你想提取一个抽象类或最终类,case类是做不到的。因为case类只提取它扩展的类,另外,它不能扩展的final类和抽象类是无用的提取。 Seq 是我们尝试过的抽象类,而 String 是最终类。您提到的论文示例,电子邮件地址,尝试将地址字符串提取到元组。所以,在这两种情况下,我们都必须使用对象伴侣unapply方法来提取数据。
    • 什么是表示依赖?感觉很抽象
    【解决方案2】:

    您可以使用稍微不同的提取器直接匹配Seq

    def doMatch(queue: Seq[Byte]) = {
      queue match {
        case h +: t => println(s"Head: $h and tail: $t")
        // You could also do case Nil =>...
        case t      => println(s"Bad: $t")
      }
    }
    
    doMatch(List(1, 2, 3)) // Head: 1 and tail: List(2, 3)
    doMatch(List())        // Bad: List()
    

    编辑:

    看来您不只是想在Seqs 上进行匹配,而是在考虑更通用的模式匹配。

    根据您对其他回复的 cmets,您似乎已经了解提取器对象,并且真的只是想看看 case class 是否可以节省您输入自己的 unapplyunapplySeq 方法的工作量预先存在的类型。

    很遗憾,答案是否定的。你必须使用unapply 和/或unapplySeq(它们还不错:))。

    您的:::: 案例类不起作用,因为子类型可以说是“错误的方式”;虽然您的::::Seq,但queue 不是::::,后者在模式匹配中很重要。

    模式匹配器看到您的第一个case,并尝试寻找相关的unapply 方法。不幸的是,:::: 生成的unapply 方法有签名:::: =&gt; Option[(Int, Seq[Int])],而queue 不是:::: 所以匹配失败。

    另一方面

    object JustWriteUnapplyAndLifeWillBeGood {
      def unapply(xs: Seq[Int]): Option[(Int, Seq[Int])] =
        if (xs.isEmpty) None else Some(xs.head, xs.tail)
    }
    

    工作得很好。

    Seq(1, 2, 3) match {
      case JustWriteUnapplyAndLifeWillBeGood(head, tail) => 
        println(s"head: $head, tail: $tail")
    } // head: 1, tail: List(2, 3)
    
    // Look Ma, no prefix!
    Seq(1, 2, 3) match {
      case head JustWriteUnapplyAndLifeWillBeGood tail =>
        println(s"head: $head, tail: $tail")
    } // head: 1, tail: List(2, 3)
    

    override 生成的unapply 方法:::: 具有正确的签名。

    【讨论】:

    • 你能解释一下,为什么在这种情况下需要使用applyunapply?确切的限制是什么?另请参阅我的答案以获取一些可靠的资源。
    • 案例类Y本质上是一堆预定义方法的语法糖,这些方法都有Y类型的参数和/或输出。对于将Y 作为参数的任何方法(例如unapply),这意味着为了使该方法可以在X 类型上调用,X 必须是Y 的子类型(即逆变)。如果X 已经存在并且不存在extend Y,则Scala 无法在不修改X 的定义的情况下追溯这样做。在这种情况下,用户无法修改 Seq,因此任何案例类 Y 都不能拥有正确的 unapply 方法。
    • 有点道理。 case class Y 中提供的 def unapply(y:Y):X 方法强烈绑定到它的输入和输出类型。将其他内容放入其中的唯一方法是使用继承。这会导致麻烦。
    • 哦,我想你指出了Seq 是抽象类但Seq(1,2,3)List 所以case 类永远不能匹配Seq,不是吗?
    • 我想说的另一件事是模式匹配不支持覆盖。但我认为如果它是coveriant的话也不错。至少Seq可以将它的所有子类与我的案例类匹配。:)
    猜你喜欢
    • 1970-01-01
    • 2011-10-23
    • 1970-01-01
    • 2021-01-27
    • 1970-01-01
    • 1970-01-01
    • 2018-09-27
    • 1970-01-01
    • 2019-06-21
    相关资源
    最近更新 更多