scalapattern-matchingcase-classscala-macrosscala-quasiquotes

Unapply/pattern matching with scala macros and quasiquotes for knownDirectSubclasses


I'm trying to create a match statement using macros, that matches all subclasses of a given type. But I have trouble with extracting the field values of the case classes. For example:

sealed abstract class Foobar
case class Foo(x:Int,f:Foobar) extends Foobar
case class Bar(s:String, f:Foobar) extends Foobar

Now I want to create code that looks like this, when Foobar is given:

 e1 match {
   case Foo(args) => args.toString
   case Bar(args) => args.toString
 }

Thats what I got so far:

  def eqImpl[A: c.WeakTypeTag](c: Context)(e1: c.Expr[A], e2: c.Expr[A]): c.Expr[Boolean] = {
    import c.universe._

    val tpe = c.weakTypeOf[A].typeSymbol.asClass
    tpe.typeSignature // SI-7046
    val subclasses = tpe.knownDirectSubclasses

    val cases = 
      subclasses.map{ clazz =>
      cq"x: $clazz => x " 
    }
    println(cases)
    reify(true)
  }

This code would match Foo and Bar, but I can not extract the fields, which I need on the right hand side.


Solution

  • So I got it mostly working, here is an example:

    def eqImpl[A: c.WeakTypeTag](c: Context)(e1: c.Expr[A], e2: c.Expr[A]): c.Expr[Boolean] = {
        import c.universe._
        val tpe = c.weakTypeOf[A].typeSymbol.asClass
        tpe.typeSignature // SI-7046 workaround 
    
        val subclasses = tpe.knownDirectSubclasses
    
        val cases = 
          subclasses.map{ case clazz : ClassSymbol =>
            require (clazz.isCaseClass)
            val name = clazz.companionSymbol.name
            val fields = clazz.typeSignature.declarations.collect {
            case m: MethodSymbol if m.isCaseAccessor => m.name}
            //pattern for the fields of the left and right side side
            val lFields = fields.map{ m => pq"""${m+"L":TermName}"""} 
            val rFields = fields.map{ m => pq"""${m+"R":TermName}"""} side
            //right hand side of the case statment 
            val eqFields = 
              fields.map{ m => q"""${m+"R":TermName} == ${m+"L":TermName}"""}.reduce[Tree]{ 
                case (acc,n) => q"$acc && $n"}
    
            cq"($name(..$lFields),$name(..$rFields)) => $eqFields  " 
        }
        val matchStmt = q"""Tuple2[$tpe,$tpe]($e1,$e2) match {
              case ..$cases
              case _ => false }"""
        c.Expr[Boolean](matchStmt)
      }
    }
    

    This code creates a match statement, that matches a tuple. If both sides of the tuple are instances of the same case class, the fields are compared. True is returned, if all fields are equal. I know that is not a particularly realistic example, but I hope it helps. For the example from the question this would generate:

    Tuple2[Foobar,Foobar](e1,e2) match {
         case (Foo(xL,fL),Foo(xR,fR) => xL == xR && fL == fR
         case (Bar(sL,fL),Bar(sR,fR) => sL == sR && fL == fR
         case _                      => false
     }