Generically rewriting Scala case classes

2019-06-25 04:23发布

问题:

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.

回答1:

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.



回答2:

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.



回答3:

I don't think you'll really find a better way than just using unapply/apply through pattern matching:

someValue match {
  case FindCaseClass(a, b, c) => ReplaceCaseClass(a, b, c)
  // . . .
}

You have to write out the rules to associate FindCaseClass with ReplaceCaseClass somehow, and although you might be able to do it a little more succinctly by somehow just using the names, this has the added benefit of also checking the number and types of the case class fields at compile time to make sure everything matches just right.

There is probably some way to do this automatically using the fact that all case classes extend Product, but the fact that productElement(n) returns Any might make it a bit of a pain—I think that's where reflection would have to come in. Here's a little something to get you started:

case class From(i: Int, s: String, xs: Seq[Nothing])
case class To(i: Int, s: String, xs: Seq[Nothing])

val iter = From(5,"x",Nil).productIterator
val f = To.curried
iter.foldLeft(f: Any) { _.asInstanceOf[Any => Any](_) }
// res0: Any = To(5,x,List())

But really, I think you're better off with the pattern-matching version.

Edit: Here is a version with the relavent code refactored into a method:

case class From(i: Int, s: String, xs: Seq[Nothing])
case class To(i: Int, s: String, xs: Seq[Nothing])

type Curryable = { def curried: _ => _ }

def recase(from: Product, to: Curryable) = {
  val iter = from.productIterator
  val f = to.curried
  iter.foldLeft(f: Any) { _.asInstanceOf[Any => Any](_) }
}

recase(From(5,"x",Nil), To)
// res0: Any = To(5,x,List())