Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Collecting data from nested case classes using Generic

Is it possible to provide a generic function which would traverse an arbitrary case class hierarchy and collect information from selected fields? In the following snippet, such fields are encoded as Thing[T].

The snippet works fine for most scenarios. The only problem is when Thing wraps a type class (e.g. List[String]) and such field is nested deeper in the hierarchy; when it is on the top level, it works fine.

import shapeless.HList._
import shapeless._
import shapeless.ops.hlist.LeftFolder

case class Thing[T](t: T) {
  def info: String = ???
}

trait Collector[T] extends (T => Seq[String])

object Collector extends LowPriority {
  implicit def forThing[T]: Collector[Thing[T]] = new Collector[Thing[T]] {
    override def apply(thing: Thing[T]): Seq[String] = thing.info :: Nil
  }
}

trait LowPriority {
  object Fn extends Poly2 {
    implicit def caseField[T](implicit c: Collector[T]) =
      at[Seq[String], T]((acc, t) => acc ++ c(t))
  }

  implicit def forT[T, L <: HList](implicit g: Generic.Aux[T, L],
                                   f: LeftFolder.Aux[L, Seq[String], Fn.type, Seq[String]]): Collector[T] =
    new Collector[T] {
      override def apply(t: T): Seq[String] = g.to(t).foldLeft[Seq[String]](Nil)(Fn)
    }
}

object Test extends App {
  case class L1(a: L2)
  case class L2(b: Thing[List[String]])

  implicitly[Collector[L2]] // works fine
  implicitly[Collector[L1]] // won't compile
}
like image 532
Marcel Mojzis Avatar asked Jul 31 '26 10:07

Marcel Mojzis


1 Answers

I'm afraid that this is impossible. HList appears to be constructed compile time from statically known things. So, when you wrap your types, for whatever reason, it seems that the HList is unable to infer the proper implicits.

Here is a simple example built from shapeless's flatten example.

object Test extends App {
  import shapeless._
  import ops.tuple.FlatMapper
  import syntax.std.tuple._

  trait LowPriorityFlatten extends Poly1 {
    implicit def default[T] = at[T](Tuple1(_))
  }
  object flatten extends LowPriorityFlatten {
    implicit def caseTuple[P <: Product](implicit fm: FlatMapper[P, flatten.type]) =
      at[P](_.flatMap(flatten))
  }

  case class AT[T](a: T, b: T)
  case class A2T[T](a: AT[T], b: AT[T])
  case class A2(a: AT[Int], b: AT[Int])

  println(flatten(A2T(AT(1, 2), AT(3, 4))))
  println(flatten(A2(AT(1, 2), AT(3, 4))))
}

You would think that this should print out the same thing for A2T and A2, however it does not. It actually prints out:

(1,2,3,4)
(AT(1,2),AT(3,4))

So, I do not think you can use Shapeless to do what you want.

However! You can still walk your case class hierarchy looking for Things (just not with shapeless). Check this out!

object Test extends App {
  case class Thing[T](t: T) {
    def info: String = toString
  }

  def collect[T](t: T): Iterator[String] = t match {
    case t: Thing[_] => Iterator(t.info)
    case p: Product => p.productIterator.flatMap(collect)
    case _ => Iterator()
  }

  case class L0(a: L1)
  case class L1(a: L2)
  case class L2(a: Thing[List[String]])
  case class MT(a: L2, b: L2, c: Thing[Int])

  println("Case #1")
  collect(L0(L1(L2(Thing(List("a", "b", "c")))))).foreach(println)

  println("Case #2")
  collect(MT(L2(Thing(List("a", "c"))), L2(Thing(List("b"))), Thing(25))).foreach(println)
}

This has output:

Case #1
Thing(List(a, b, c))
Case #2
Thing(List(a, c))
Thing(List(b))
Thing(25)
like image 125
Nate Avatar answered Aug 04 '26 12:08

Nate



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!