3

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
}

1 Answer 1

1

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)
Sign up to request clarification or add additional context in comments.

2 Comments

Thank you, Nate. I'm afraid this will stop working when you add another level to the hierarchy (case class L0(a: L1))
Updated my answer. Appears impossible with shapeless; just do it without!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.