After some investigation is appears to me that your problem is slightly more complicated than it seems on the first sight. I drafted a solution where instead of macros I used DSL based on Monocle:
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))
}
However, as soon as I started testing it I figured out an issue:
val t = traverse[F](handlerForA)(_.nest(
Nested(GenLens[F](_.a), handlerForA),
Nested(GenLens[F](_.b), handlerForB),
// how to put handlerForC?
))
t(instanceOfA)
I was usable to use handlerForC... because your original types were not product types-only. A might have different implementations, B and C as well.
So, could we try to generate some more complex solution where coproducts are taken into account? Well, with your exact example not - compiler is only able to derive known direct subclasses if your class/trait is sealed and all (direct) implementations have to be implemented in the same file.
But let's say you made A
, B
and C
sealed. In such case I would somehow try to use Prism, in order to step inside only when possible (or use pattern matching or any other equivalent solution). But that complicates the DSL - the reason I guess why do decided to look at the macros in the first place.
So, lets rethink the issue anew.
Code that you would have to write would:
- take into account extracting values from products types
- take into account branching on coproduct types
- make use of provided handlers for each (coproduct) type
- minimize the amount of code written
This requirements sounds difficult to achieve. Right now I am thinking if you could achieve your goal faster by using recursive schemes if you created a common parent for all traits, "lifted" all your classes to Id
and then writing one traversing function and one which applies handlers.
Of course all of that could be implemented one way or the other using def macros, but the thing is will requirements in the way they are now I would be extremely difficult to make sure that it won't do something bad, as we have pretty strict requirements for the output (for each handled cases find them even if they are nested) while very relaxed requirements on the input (least upper bound is Any
/AnyRef
, hierarchies are not sealed). I am not sure if runtime reflection wouldn't be easier way to achieve your goals as long as you don't want to change any assumption or requirements.