如何有效地遍历递归的案例类树

时间:2018-01-26 08:57:43

标签: scala generics macros tree-traversal scala-macros

我想创建一个宏,它生成一个类似于访问者模式的案例类实例树的递归遍历。对于其类型派生自多种基类型之一的所有字段,生成的代码应该递归。

我想到了一些可以使用的代码:

trait A
trait B
trait C

case object D extends A { }
case object E extends C { }
case class F (a: A, b: B, c: Int) extends A
case class G (d: C, e: String) extends B
val instanceOfA = F (D, G (E, "Foo"), 1)

// expected type   extra types that should be included in traversal
//       vv        vv
traverse[A,        B, C] (handlerForA, handlerForB, handlerForC) (instanceOfA)

遍历的类型将类似于以下内容(我知道这不是有效的语法,但无法提出更好的):

def traverse[T, U...] (handleT : T => Unit, handleU... : U... => Unit) : T => Unit = macro traverseImpl

我知道Scala不能支持可变参数泛型,但是我不明白为什么不应该用宏来实现这样的东西。但我还没有找到一种允许将多种类型作为参数传递给def宏的方法。

2 个答案:

答案 0 :(得分:1)

经过一番调查后,我发现你的问题比第一眼看上去要复杂一些。我起草了一个解决方案,而不是宏我使用基于Monocle的DSL:

import monocle.Lens
import monocle.macros.GenLens

case class Nested[A, B](aToB: Lens[A, B], handleB: B => Unit) {

  def from[C](cToA: Lens[C, A]): Nested[C, B] = Nested(cToA composeLens aToB, handleB)

  def nest(nested: Nested[B, _]*): Seq[Nested[A, _]] = this +: nested.map(_.from(aToB))
}

def traverse[A](handleA: A => Unit)(configure: Nested[A, A] => Seq[Nested[A, _]]): A => Unit =
  value => configure(Nested(Lens.id[A], handleA)).foreach { case Nested(lens, handler) =>
    handler(lens.get(value))
  }

然而,一旦我开始测试它,我就发现了一个问题:

val t = traverse[F](handlerForA)(_.nest(
  Nested(GenLens[F](_.a), handlerForA),
  Nested(GenLens[F](_.b), handlerForB),
  // how to put handlerForC?
))

t(instanceOfA)

我可以使用handlerForC ...因为您的原始类型不仅仅是产品类型。 A可能有不同的实现,B和C.也可以。

那么,我们是否可以尝试生成一些更复杂的解决方案,其中考虑了副产品?好吧,如果您的确切示例不是 - 编译器只能在您的类/特征被密封且所有(直接)实现必须在同一文件中实现的情况下派生已知的直接子类。

但是,让我们说你ABC被封了。在这种情况下,我会以某种方式尝试使用Prism,以便在可能的情况下进入内部(或使用模式匹配或任何其他等效解决方案)。但这使DSL变得复杂 - 我想为什么决定首先看一下宏。

所以,让我们重新思考这个问题。

您必须编写的代码:

  • 考虑从产品类型中提取值
  • 考虑联合产品类型的分支
  • 为每个(联合产品)类型
  • 使用提供的处理程序
  • 最小化写入的代码量

这个要求听起来很难实现。现在我想如果你为所有特征创建了一个共同的父母,那么使用recursive schemes可以更快地实现你的目标,"解除了#34;将所有类添加到Id,然后编写一个遍历函数和一个应用处理程序的函数。

当然所有这些都可以使用def宏以这种方式实现,但事情就是以现在的方式要求我会非常难以确保它不会做坏事因为我们对输出有非常严格的要求(对于每个处理的案例,即使它们是嵌套的,也会找到它们),而对输入的要求非常宽松(最小上限是Any / AnyRef,层次结构不是密封)。只要您不想改变任何假设或要求,我不确定运行时反射是否更容易实现您的目标。

答案 1 :(得分:0)

我找到了另一种完全不同的解决方案,它有自己的缺点。我添加了两个特征TraversableTraverseHandler

trait Traversable[+T] {
    def traverse (handler: TraverseHandler) : T
}
trait TraverseHandler {
    def handleA (a: A) : A
    def handleB (b: B) : B
    def handleC (c: C) : C
}

ABC扩展Traversable[A] ...并且他们的traverse方法已在所有案例类中实施。我正在使用注释宏(想要避免它们,但它们接缝是最简单的解决方案)来实现以下代码扩展:

@traversable [TraverseHandler]
case class F (@expand a: A, @expand b: B, c: Int) extends A

case class F (a: A, b: B, c: Int) extends A {
    def traverse (handler: TraverseHandler) : F = {
        val newA = handler.handleA(a)
        val newB = handler.handleB(b)
        if ((newA ne a) || (newB ne b))
            copy (a = newA, b = newB)
        else
            this
    }
}

这避免了对密封特征的需要,并允许不同的处理程序,同时不会给类本身增加太多开销。

当然,如果有更容易的东西或者不需要对类本身进行更改的东西,那就太好了。它还要求处理程序进行递归(另一方面,它可以删除单个元素而无需处理它们或允许自定义的预处理/后处理)。

也许更好的解决方案可能是在伴侣对象中创建一个方法,为镜头提供特定于类的构造函数。但我仍然担心这种结构的递归性质可能会使单片眼镜的使用复杂化。如何创造一个依赖于镜头的镜头,镜头又取决于原始镜头?换句话说:镜头是否有固定点组合器?