3
votes

I want to use the power of Scala's pattern matching within a set of `condition-action' rules. These rules are not known in advance, but rather are generated at runtime according to some complex critera. The algorithmic generation mechanism can be considered as a completely separate and is not part of this question, which is concerned with how to express this via Scala reflection/quasiquotes.

Concretely, I'm looking to generate case definitions (of the general form case v0@x(v1,_,v2): X => f(v1,v2)) at runtime.

It is presumably possible to do this via toolBox.parse(str) for some string that is generated at runtime. However, if possible it would seem desirable to incorporate a greater degree of type-safety than this:

More specifically, I want the case defs to match against a sealed case class hierarchy of Terms (Term,Var(name: Char),Lit(value:Int),Group(a: Term,b: Term,c: Term)).

For example, the generated case def would in general, return some function of none, some or all of v0,v1,v2:

  t match {
    case v0@Group(v1@_,v2@Var('a')) => Group(v2,v0,Group(v1,Var('z'),Lit(17))) // etc
  }

I'm attempting to follow through on the description of quasiquotes for case defs given here, but the syntax is rather mind-bending (and eclipse with Scala 2.11 refuses to show me the types), so below is as far as I've got. My specific questions are embedded in the code:

def dynamicMatch(condition: SomeType, action: SomeType, tb: ToolBox)
(t: Term): Option[Term] = {

  // Q1. What type should condition and action be for maximum
  // typesafety in the calling code? Symbols? Quasiquotes? 
  // Would they best be combined into a single actual CaseDef?

  // This is obviously a hardcoded placeholder expression, in general:
  // Q2. How to bind in t, condition and action?
  val q"$expr match { case ..$cases }" =
    q"foo match { case _ : Term => Some(expr) case _ => None }"

  val cq"$pat1 => $body1" :: cq"$pat2 => $body2" :: Nil = cases

  // Q3. how should this be invoked to return the desired result?
  ???
}
2
What are you trying to achieve with this? This all happens at compile time, not run time.Michael Zajac
Could you explain why you need this? I wonder if runtime reflection over the object might be better than using macros to generate match statements? I think you need to make this question much more accessible if you hope for an answer here.Rich
@m-z It's possible to generate CaseDefs at runtime, though right? Presumably one could simply use toolBox.parse("case X => Y") for some string that could be generated at runtime, part of what I'm asking is what's the strongest typing that can be used in place of String.NietzscheanAI
@m-z: quasiquotes aren't necessarily a compile-time mechanism, though are they? For example, there's the "Just In Time Compilation", example at docs.scala-lang.org/overviews/quasiquotes/usecases.html –NietzscheanAI

2 Answers

1
votes

This really doesn't feel like it should be done using macros. As other answers have noted macros should be used for compile-time safety. There is no tangible benefit to doing this that a general definition, written below, does not provide.

case class GenericClass(`type`: String, args: List[Any])

val _actions = Map("1" -> () => println("hello"), "2" -> () => println("goodbye"))

def dynamic(gen: GenericClass) match {
   case GenericClass(n, _) => _actions.get(n).map(_.apply())
}

You can of course create case classes at runtime, however this is not the same as creating CaseDef which is simply an AST tree. You would basically have to go through the steps for the class/methods to be available outside of your code.

  1. Create the AST tree
  2. Get the scala compiler to compile it into java
  3. Load the java class using your own classloader or some sort of reflection to load the byte code.

Not only that but everywhere you use these new generated classes would have to use reflection to instantiate and call the methods unless you generated those at the same time as generating the types.

As you can probably tell this is difficult. Java and hence scala are both compiled languages unlike python or javascript which are interpreted. Loading classes into the runtime is not standard or recommended.

EDIT - after clarification

It's worth clarifying that the question is more to do with how to create a partial function safely rather than dynamic generation of code as first seemed.

First let's take the scenario, you essentially wish to have the behavior in the below statement for n different classes which you do not know at runtime (presumably as part of the output of some algorithm),

case v0@x(v1,_,v2): X => f(v1,v2))

before we continue it's worth discussing what this actually compiles to. for the block of code,

val f: PartialFunction[String, Int] = {
    case "foo" => 1
}

in particular, scala essentially converts case statements of this form into a PartialFunction, that is a function for values defined only for certain values of the input. In the case that the point is not defined it will return an Option of the return type. The key method on this type is isDefined.

This indeed works when widening the type to Any and matching on the class,

val f: PartialFunction[Any, Int] = {
    case _: String => 1
    case _: Int => 2
}

How does this relate to your question? Well, the other interesting method for a PartialFunction is the orElse method. What it does is to check whether a partial function is defined for a particular point and in the case that it is not will attempt to evaluate the second PartialFunction.

 case class Foo(s: String)
 case class Bar(i: Int)

 val f1: PartialFunction[Any, String] = {
   case Foo(s) => s
 }

 val f2: PartialFunction[Any, String] = {
   case Bar(i) => i.toString
 }

 //our composite!
 val g = f1 orElse f2

In the example above g will only evaluate if the input is either of Foo or Bar. In the event it is neither it will return a None safely and the behavior of the function is changed at runtime.

1
votes

There's a shapeless example that builds a function invocation with the purpose of getting the toolbox to select a typeclass based on runtime types.

You also want to build the logic dynamically, but you're having difficulty with quasiquoting.

This does something:

// Some classes
sealed trait Term
case class Lit(value: Int) extends Term
case class Group(a: Term, b: Term) extends Term

// Build a function that hooks up patterns to other expressions
  def stagedTermFunction(t: Term): Option[Term] = {
    // add lits
    val tt = tq"_root_.shapeless.examples.Term"
    val lit = q"_root_.shapeless.examples.Lit"
    val grp = q"_root_.shapeless.examples.Group"
    val pat = pq"$grp($lit(x), $lit(y))"
    val add = cq"$pat => Some($lit(x + y))"
    val default = cq"_ => None"
    val cases = add :: default :: Nil
    val res = q"{ case ..$cases } : (($tt) => Option[$tt])"
    val f = evalTree[Term => Option[Term]](res)
    f(t)
  }

Then this doesn't blow up:

  val t3: Term = Group(Lit(17), Lit(42))
  val Some(Lit(59)) = stagedTermFunction(t3)
  assert(stagedTermFunction(Lit(0)) == None)

If you wanted to manipulate symbolOf[Group] you might have to convert sym.fullName to the Select tree built by q"name"; I think there's a utility method somewhere.