Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scala Tree Match Case

Tags:

macros

tree

scala

I'm fairly new to Scala and very new to writing macros and looking for a little help/advise. I have the following code...

trait ValidationRule
case class Required() extends ValidationRule
case class HasLength(l: Int) extends ValidationRule
case class Person(name: String)

myMacro[Person] { p => p.name.is(Required(), HasLength(255)) }

Obviously there is some missing code here but this is just pseudo to get the question out.

So given a Tree representing p => p.name.is(Required(), HasLength(255)) I'm trying to write a match/case to select out all expressions representing a ValidationRule. Something like:

case TypeApply(Select(_, ....

Can anyone suggest the best match case to be able to extract a List of Trees that represent each "all" ValidationRules from within the "is" method?

like image 283
user2270883 Avatar asked Oct 31 '22 06:10

user2270883


1 Answers

You should definitely look into Quasiquotes.

Quasiquotes are used to do two things: to build Trees, and to pattern match Trees. They allow you to express the tree that you want to work with in terms of the equivalent Scala code. You let the quasiquote library deal with how Scala code maps to a Tree graph, and that's a good thing!

You can play around with them in the REPL, though the results might be slightly different in the macro universe:

scala> import scala.reflect.runtime.universe._
scala> showRaw(cq"p => p.name.is(Required(), HasLength(255))")

res0: String = CaseDef(
  Bind(
    TermName("p"), 
    Ident(termNames.WILDCARD)), 
  EmptyTree, 
  Apply(
    Select(
      Select(
        Ident(TermName("p")), 
        TermName("name")), 
      TermName("is")), 
    List(
      Apply(
        Ident(TermName("Required")), 
        List()), 
      Apply(
        Ident(TermName("HasLength")), 
        List(Literal(Constant(255)))))))

The other thing you can do with Quasiquotes is to actually use them to pattern match.

scala> val fromTree = cq"p => p.name.is(Required(), HasLength(255))"
scala> val cq"p => p.name.is($x, $y)" = fromTree


x: reflect.runtime.universe.Tree = Required()
y: reflect.runtime.universe.Tree = HasLength(255)

Now, you have to be careful, because that pattern ONLY matches if the user named their pattern variable p.

scala> val fromTree = cq"x => x.name.is(Required(), HasLength(255))"
scala> val cq"p => p.name.is($x, $y)" = fromTree

scala.MatchError: case (x @ _) => x.name.is(Required(), HasLength(255)) (of class scala.reflect.internal.Trees$CaseDef)
  ... 33 elided

Instead, you'll want to be a bit more generic:

scala> val cq"${p1:TermName} => ${p2:TermName}.name.is($x, $y)" = fromTree
p1: reflect.runtime.universe.TermName = x
p2: reflect.runtime.universe.TermName = x
x: reflect.runtime.universe.Tree = Required()
y: reflect.runtime.universe.Tree = HasLength(255)

scala> p1 == p2
res2: Boolean = true

And, of course, if you were doing this as part of a pattern match, you could do:

case cq"${p1:TermName} => ${p2:TermName}.name.is($x, $y)" if p1 == p2 => 
  ???

Keep in mind that Macros are a deep, dark hole. If you're just getting started, expect to spend a lot of time getting your macro code correct. After that, expect to spend a lot of time dealing with edge cases.

like image 105
Daniel Yankowsky Avatar answered Nov 18 '22 11:11

Daniel Yankowsky