如何使用apply / unapply方法重现案例类行为?

时间:2016-08-17 18:42:36

标签: scala case-class

我尝试用普通类和伴侣对象替换case类,然后突然出现类型错误。

编译精细的代码(合成示例):

trait Elem[A,B] {
  def ::[C](other : Elem[C,A]) : Elem[C,B] = other match {
    case Chain(head, tail) => Chain(head, tail :: this)
    case simple => Chain(simple, this)
  }
}
class Simple[A,B] extends Elem[A,B]
final case class Chain[A,B,C](head : Elem[A,B], tail : Elem[B,C]) extends Elem[A,C]

使用以下命令更改最后一个定义:

final class Chain[A,B,C](val head : Elem[A,B], val tail : Elem[B,C]) extends Elem[A,C]
object Chain {
  def unapply[A,B,C](src : Chain[A,B,C]) : Option[(Elem[A,B], Elem[B,C])] =
    Some( (src.head, src.tail) )
  def apply[A,B,C](head : Elem[A,B], tail : Elem[B,C]) : Chain[A,B,C] =
    new Chain(head, tail)
}

但是看似等效的代码会让编译器发出错误:

CaseMystery.scala:17: error: type mismatch;
 found   : test.casemystery.Fail.Elem[A,B] where type B, type A >: C <: C
 required: test.casemystery.Fail.Elem[A,Any] where type A >: C <: C
Note: B <: Any, but trait Elem is invariant in type B.
You may wish to define B as +B instead. (SLS 4.5)
      case Chain(head, tail) => Chain(head, tail :: this)
                                  ^
CaseMystery.scala:17: error: type mismatch;
 found   : test.casemystery.Fail.Elem[B(in method ::),B(in trait Elem)] where type B(in method ::)
 required: test.casemystery.Fail.Elem[Any,B(in trait Elem)]
Note: B <: Any, but trait Elem is invariant in type A.
You may wish to define A as +A instead. (SLS 4.5)
      case Chain(head, tail) => Chain(head, tail :: this)
                                             ^
two errors found

使用case语句隐式创建的方法和为世俗类明确编写的方法有什么区别?

2 个答案:

答案 0 :(得分:3)

这个答案最终比我预期的要长。如果您只想了解类型推断所发生的事情,请跳到最后。否则,您将获得解决问题的步骤。

问题出在case,但不是case class

中的问题

在这种情况下,尽管我不愿承认,案例类真的很神奇。特别是,他们在类型检查器级别获得特殊处理(我认为我们可以同意,如果它已经过了那个阶段你的代码就可以工作了 - 你甚至可以投入足够的强制转换来使其工作)。

问题是,令人惊讶的是,不是在类Chain本身,而是在它使用的地方,特别是在模式匹配部分。例如,考虑案例类

case class Clazz(field: Int)

然后,您希望以下内容相同:

Clazz(3) match { case Clazz(i) => i }
// vs
val v = Clazz.unapply(Clazz(3))
if (v.isDefined) v.get else throw new Exception("No match")

但是,Scala想要更聪明并优化它。特别是,这个unapply方法几乎可以永远不会失败(现在让我们忽略null)并且可能会使用很多,因此Scala希望完全避免它并且只是提取字段,因为它通常会获取对象的任何成员。正如我的编译教授喜欢说的那样,“编译器是欺骗的艺术而不会被抓住”。

然而,类型检查器存在差异。问题在于

def ::[Z, X](other : Elem[Z, X]) : Elem[Z, Y] = other match {
  case Chain(head, tail) => Chain(head, tail :: this)
  case simple => Chain(simple, this)
}

如果使用-Xprint:typer进行编译,您将看到类型检查器看到的内容。案例类版本有

def ::[C](other: Elem[C,A]): Elem[C,B] = other match {
  case (head: Elem[C,Any], tail: Elem[Any,A])Chain[C,Any,A]((head @ _), (tail @ _)) => Chain.apply[C, Any, B](head, {
    <synthetic> <artifact> val x$1: Elem[Any,A] = tail;
    this.::[Any](x$1)
  })
  case (simple @ _) => Chain.apply[C, A, B](simple, this)
}

普通班有

def ::[C](other: Elem[C,A]): Elem[C,B] = other match {
  case Chain.unapply[A, B, C](<unapply-selector>) <unapply> ((head @ _), (tail @ _)) => Chain.apply[A, Any, B](<head: error>, {
    <synthetic> <artifact> val x$1: Elem[_, _ >: A <: A] = tail;
    this.::[B](x$1)
  })
  case (simple @ _) => Chain.apply[C, A, B](simple, this)
}

因此类型检查器实际上获得了一个不同的(特殊)案例构造。

那么match被翻译成什么?

只是为了好玩,我们可以检查下一阶段-Xprint:patmat会发生什么情况,这会扩展模式(尽管这些事实上这些不再是真正有效的Scala程序真的很痛苦)。首先,案例类有

def ::[C](other: Elem[C,A]): Elem[C,B] = {
  case <synthetic> val x1: Elem[C,A] = other;
  case5(){
    if (x1.isInstanceOf[Chain[C,Any,A]])
      {
        <synthetic> val x2: Chain[C,Any,A] = (x1.asInstanceOf[Chain[C,Any,A]]: Chain[C,Any,A]);
        {
          val head: Elem[C,Any] = x2.head;
          val tail: Elem[Any,A] = x2.tail;
          matchEnd4(Chain.apply[C, Any, B](head, {
            <synthetic> <artifact> val x$1: Elem[Any,A] = tail;
            this.::[Any](x$1)
          }))
        }
      }
    else
      case6()
  };
  case6(){
    matchEnd4(Chain.apply[C, A, B](x1, this))
  };
  matchEnd4(x: Elem[C,B]){
    x
  }
}

虽然很多内容让人感到困惑,但请注意我们从不使用unapply方法!对于非案例类版本,我将使用user1303559的工作代码:

def ::[Z, XX >: X](other: Elem[Z,XX]): Elem[Z,Y] = {
  case <synthetic> val x1: Elem[Z,XX] = other;
  case6(){
    if (x1.isInstanceOf[Chain[A,B,C]])
      {
        <synthetic> val x2: Chain[A,B,C] = (x1.asInstanceOf[Chain[A,B,C]]: Chain[A,B,C]);
        {
          <synthetic> val o8: Option[(Elem[A,B], Elem[B,C])] = Chain.unapply[A, B, C](x2);
          if (o8.isEmpty.unary_!)
            {
              val head: Elem[Z,Any] = o8.get._1;
              val tail: Elem[Any,XX] = o8.get._2;
              matchEnd5(Chain.apply[Z, Any, Y](head, {
                <synthetic> <artifact> val x$1: Elem[Any,XX] = tail;
                this.::[Any, XX](x$1)
              }))
            }
          else
            case7()
        }
      }
    else
      case7()
  };
  case7(){
    matchEnd5(Chain.apply[Z, XX, Y](x1, this))
  };
  matchEnd5(x: Elem[Z,Y]){
    x
  }
}

在这里,确实,unapply方法出现了。

实际上并没有作弊(对于专业人员)

当然,Scala实际上并没有作弊 - 这种行为都在规范中。特别是,我们看到constructor patterns从哪个案例类中获益是特别的,因为除其他外,它们是irrefutable(与我上面提到的关于Scala不想使用{ {1}}方法,因为它“知道”它只是提取字段。)

真正感兴趣的部分是8.3.2 Type parameter inference for constructor patterns。常规类和案例类之间的区别在于unapply模式是Chain是案例类时的“构造函数模式”,否则只是常规模式。构造函数模式

Chain

最终会被输入,好像它是

other match {
  case Chain(head, tail) => Chain(head, tail :: this)
  case simple => Chain(simple, this)
}

然后,基于参数类型other match { case _: Chain[a1,a2,a3] => ... } other: Elem[C,A]这一事实,我们得到Chain[a1,a2,a3] extends Elem[a1,a3]a1Ca3A可以是任何内容,a2也是如此。因此,为什么案例类的Any输出中的类型中包含-Xprint:typer。这会进行类型检查。

但是,构造函数模式特定于case类,所以没有 - 这里没有办法模仿case类行为。

  

构造函数模式的格式为Chain[C,Any,A],其中c(p1,…,pn)。它   由一个稳定的标识符n≥0组成,后跟元素模式   c。构造函数p1,…,pn是一个简单或限定的名称   表示c

答案 1 :(得分:0)

首先otherElem[C, A],但在您尝试将其与Chain(head, tail)匹配后,它实际上与Chain[C, some inner B, A](head: Elem[C, inner B], tail: Elem[inner B, A])匹配。之后,您创建Chain[C, inner B <: Any, A](head: Elem[C, inner B], (tail :: this): Elem[inner B, B])

但结果类型必须为Elem[C, B]Chain[C, Any, B]。所以编译器试图将inner B转换为Any。但是,由于inner B 不变,您必须完全Any

这实际上是更好的重写如下:

trait Elem[X, Y] {
  def ::[Z, X](other : Elem[Z, X]) : Elem[Z, Y] = other match {
    case Chain(head, tail) => Chain(head, tail :: this)
    case simple => Chain(simple, this)
  }
}

final class Chain[A, B, C](val head : Elem[A, B], val tail : Elem[B, C]) extends Elem[A, C]

object Chain {
  def unapply[A,B,C](src : Chain[A,B,C]) : Option[(Elem[A,B], Elem[B,C])] =
    Some( (src.head, src.tail) )
  def apply[A,B,C](head : Elem[A,B], tail : Elem[B,C]) : Chain[A,B,C] =
    new Chain(head, tail)
}

在此错误消息变得更具信息性之后,显然如何修复此问题。

但是,我不知道为什么它适用于案例类。遗憾。

工作示例是:

trait Elem[+X, +Y] {
  def ::[Z, XX >: X](other : Elem[Z, XX]) : Elem[Z, Y] = other match {
    case Chain(head, tail) => Chain(head, tail :: this)
    case simple => Chain(simple, this)
  }
}

final class Chain[A, B, C](val head : Elem[A, B], val tail : Elem[B, C]) extends Elem[A, C]

object Chain {
  def unapply[A,B,C](src : Chain[A,B,C]) : Option[(Elem[A,B], Elem[B,C])] =
    Some( (src.head, src.tail) )
  def apply[A,B,C](head : Elem[A,B], tail : Elem[B,C]) : Chain[A,B,C] =
    new Chain(head, tail)
}

<强>编辑:

最终我发现:

case class A[T](a: T)
List(A(1), A("a")).collect { case A(x) => A(x) }
// res0: List[A[_ >: String with Int]] = List(A(1), A(a))

class B[T](val b: T)
object B {
  def unapply[T](b: B[T]): Option[T] = Option(b.b)
}
List(new B(1), new B("b")).collect { case B(x) => new B(x) }
// res1: List[B[Any]] = List(B@1ee4afee, B@22eaba0c)

很明显它是编译器功能。所以我认为没有办法重现完整的案例类行为。