【问题标题】:How to efficiently traverse recursive case-class trees如何有效地遍历递归案例类树
【发布时间】:2018-01-26 08:57:43
【问题描述】:

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

我想到了一些可以类似这样使用的代码:

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 宏。

【问题讨论】:

  • 你真的不需要像光学这样的东西吗:julien-truffaut.github.io/Monocle ?
  • 我的意思是:将每个类的处理程序与每个嵌套级别的镜头提取器组合起来,您应该获得相同的结果,尽管语法略有不同。
  • 感谢您的提示。 Monocle 看起来很有趣,它甚至可以支持创建已编辑的实例。我想知道它是否可以提供足够的灵活性和性能来在我相当复杂的场景中工作而无需太多开销。
  • 一个问题:你想分支吗(例如,对于 F,你想同时遍历 A 和 B)吗?我正在考虑为体育起草一个实施方案,所以根据情况我会采取不同的方法。
  • 是的,我会一次遍历它们,因为我可以进行包含多种类型的递归,例如case class H (a: A) extends B; object I extends B; F (H (F (F (D, I, 0), I, 1)), I, 2),我希望能够一次更改所有F.cs。

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


【解决方案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 更快地实现目标,将所有类“提升”到Id,然后编写一个遍历函数和一个应用处理程序的函数。

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

【讨论】:

  • 感谢您努力寻找解决方案。递归方案看起来可以提供我需要的东西。不幸的是,我没有时间完全理解它们。这就是为什么我现在使用带有注释宏的更多 OO 方法。
【解决方案2】:

我发现了另一种完全不同的解决方案,它有自己的缺点。我添加了两个特征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
    }
}

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

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

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

【讨论】:

    猜你喜欢
    • 2018-07-12
    • 2014-03-11
    • 1970-01-01
    • 2020-04-25
    • 1970-01-01
    • 1970-01-01
    • 2017-05-11
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多