How to reproduce case class behaviour with apply/unapply methods?

后端 未结 2 1971
失恋的感觉
失恋的感觉 2021-01-21 19:02

I tried to replace case class with mundane class and companion object and suddenly get type error.

Code that compiles fine (synthetic example):

trait Ele         


        
相关标签:
2条回答
  • 2021-01-21 19:23

    This answer ended up being longer than I expected. If you just want the guts of what is happening with type inference, skip to the end. Otherwise, you get led through the steps of getting to the answer.

    The problem is in the case, but not the one in case class

    In this case, as much as I hate to admit it, case classes really are magic. In particular, they get special treatment at the type checker level (I think we can agree that your code would work if it got past that phase - you might even be able to throw enough casts at it to make that work).

    The problem is, surprisingly enough, not in the class Chain itself, but in the places it is used, specifically in the pattern matching part. For example, consider the case class

    case class Clazz(field: Int)
    

    Then, you expect the following to be equivalent:

    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")
    

    But, Scala wants to be more clever and optimize this. In particular, this unapply method pretty can pretty much never fail (let's ignore null for now) and is probably used a lot, so Scala wants to avoid it altogether and just extract the fields as it usually would get any member of an object. As my compiler professor is fond of saying, "compilers are the art of cheating without getting caught".

    Yet here there is a difference in the type-checker. The problem is in

    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)
    }
    

    If you compile with -Xprint:typer you'll see what the type checker sees. The case class version has

    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)
    }
    

    While the regular class has

    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)
    }
    

    So the type checker actually gets a different (special) case construct.

    So what does the match get translated to?

    Just for fun, we can check what happens at the next phase -Xprint:patmat which expands out patterns (although here the fact that these are no longer really valid Scala programs really becomes painful). First, the case class has

    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
      }
    }
    

    Although a lot of stuff is confusing here, notice that we never use the unapply method! For the non-case class version, I'll use the working code from 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
      }
    }
    

    And here, sure enough, the unapply method makes an appearance.

    It isn't actually cheating (for the Pros)

    Of course, Scala doesn't actually cheat - this behavior is all in the specification. In particular, we see that constructor patterns from which case classes benefit are kind of special, since, amongst other things, they are irrefutable (related to what I was saying above about Scala not wanting to use the unapply method since it "knows" it is just extracting the fields).

    The part that really interests us though is 8.3.2 Type parameter inference for constructor patterns. The difference between the regular class and the case class is that Chain pattern is a "constructor pattern" when Chain is a case class, and just a regular pattern otherwise. The constructor pattern

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

    ends up getting typed as though it were

    other match {
      case _: Chain[a1,a2,a3] => ...
    }
    

    Then, based on the fact that other: Elem[C,A] from the argument types and the fact that Chain[a1,a2,a3] extends Elem[a1,a3], we get that a1 is C, a3 is A and a2 can by anything, so is Any. Hence why the types in the output of -Xprint:typer for the case class has an Chain[C,Any,A] in it. This does type check.

    However, constructor patterns are specific to case classes, so no - there is no way to imitate the case class behavior here.

    A constructor pattern is of the form c(p1,…,pn) where n≥0. It consists of a stable identifier c, followed by element patterns p1,…,pn. The constructor c is a simple or qualified name which denotes a case class.

    0 讨论(0)
  • 2021-01-21 19:27

    Firstly other is Elem[C, A], but after you had tried to match it as Chain(head, tail) it actually matched to Chain[C, some inner B, A](head: Elem[C, inner B], tail: Elem[inner B, A]). After that you create Chain[C, inner B <: Any, A](head: Elem[C, inner B], (tail :: this): Elem[inner B, B])

    But result type must be Elem[C, B], or Chain[C, Any, B]. So compiler trying to cast inner B to Any. But beacause inner B is invariant - you must have exactly Any.

    This is actually better rewrite as follows:

    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)
    }
    

    After this error message becoming much more informative and it is obviously how to repair this.

    However I don't know why that works for case classes. Sorry.

    Working example is:

    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)
    }
    

    EDITED:

    Eventually I found that:

    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)
    

    Obvious that it is compiler feature. So I think no way there to reproduce the full case class behavior.

    0 讨论(0)
提交回复
热议问题