Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generically rewriting Scala case classes

Is it possible to generically replace arguments in a case class? More specifically, say I wanted a substitute function that received a "find" case class and a "replace" case class (like the left and right sides of a grammar rule) as well as a target case class, and the function would return a new case class with arguments of the find case class replaced with the replace case class? The function could also simply take a case class (Product?) and a function to be applied to all arguments/products of the case class.

Obviously, given a specific case class, I could use unapply and apply -- but what's the best/easiest/etc way to generically (given any case class) write this sort of function?

I'm wondering if there is a good solution using Scala 2.10 reflection features or Iso.hlist from shapeless.

For example, what I really want to be able to do is, given classes like the following...

class Op[T]
case class From(x:Op[Int]) extends Op[Int]
case class To(x:Op[Int]) extends Op[Int]

case class Target(a:Op[Int], b:Op[Int]) extends ...
// and lots of other similar case classes

... have a function that can take an arbitrary case class and return a copy of it with any elements of type From replaced with instances of type To.

like image 906
Josh Marcus Avatar asked Dec 11 '22 20:12

Josh Marcus


2 Answers

If you'll pardon the plug, I think you'll find that the rewriting component of our Kiama language processing library is perfect for this kind of purpose. It provides a very powerful form of strategic programming.

Here is a complete solution that rewrites To's to From's in a tree made from case class instances.

import org.kiama.rewriting.Rewriter

class Op[T]
case class Leaf (i : Int) extends Op[Int]
case class From (x : Op[Int]) extends Op[Int]
case class To (x : Op[Int]) extends Op[Int]

case class Target1 (a : Op[Int], b : Op[Int]) extends Op[Int]
case class Target2 (c : Op[Int]) extends Op[Int]

object Main extends Rewriter {

    def main (args : Array[String]) {
        val replaceFromsWithTos =
            everywhere {
                rule {
                    case From (x) => To (x)
                }
            }

        val t1 = Target1 (From (Leaf (1)), To (Leaf (2)))
        val t2 = Target2 (Target1 (From (Leaf (3)), Target2 (From (Leaf (4)))))

        println (rewrite (replaceFromsWithTos) (t1))
        println (rewrite (replaceFromsWithTos) (t2))
    }

}

The output is

Target1(To(Leaf(1)),To(Leaf(2)))
Target2(Target1(To(Leaf(3)),Target2(To(Leaf(4)))))

The idea of the replaceFromsWithTos value is that the rule construct lifts a partial function to be able to operate on any kind of value. In this case the partial function is only defined at From nodes, replacing them with To nodes. The everywhere combinator says "apply my argument to all nodes in the tree, leaving unchanged places where the argument does not apply.

Much more can be done than this kind of simple rewrite. See the main Kiama rewriting documentation for the gory detail, including links to some more examples.

like image 111
inkytonik Avatar answered Jan 02 '23 19:01

inkytonik


I experimented a bit with shapeless and was able to come up with the following, relatively generic way of converting one case class into another:

import shapeless._ /* shapeless 1.2.3-SNAPSHOT */

case class From(s: String, i: Int)
case class To(s: String, i: Int)

implicit def fromIso = Iso.hlist(From.apply _, From.unapply _)
implicit def toIso = Iso.hlist(To.apply _, To.unapply _)

implicit def convert[A, B, L <: HList]
                   (a: A)
                   (implicit srcIso: Iso[A, L],
                             dstIso: Iso[B, L])
                   : B =
  dstIso.from(srcIso.to(a))

val f1 = From("Hi", 7)
val t1 = convert(f1)(fromIso, toIso)

println("f1 = " + f1) // From("Hi", 7)
println("t1 = " + t1) // To("Hi", 7)

However, I was not able to get the implicits right. Ideally,

val t1: To = f1

would be sufficient, or maybe

val t1 = convert(f1)

Another nice improvement would be to get rid of the need of having to explicitly declare iso-implicits (fromIso, toIso) for each case class.

like image 29
Malte Schwerhoff Avatar answered Jan 02 '23 20:01

Malte Schwerhoff