You want to return Map[String, Map[String, ... Map[String, List[A]]]]. The type must be known at compile time. So the length of rest: (A => String)* must be known at compile time. You can introduce a type class using Shapeless Sized
// libraryDependencies += "com.chuusai" %% "shapeless" % "2.3.10"
import shapeless.nat.{_0, _2}
import shapeless.{Nat, Sized, Succ}
import scala.collection.Seq // Scala 2.13
// type class
trait AsMap[A, N <: Nat] {
type Out
def apply(list: List[A], selectors: Sized[Seq[A => String], N]): Out
}
object AsMap {
type Aux[A, N <: Nat, Out0] = AsMap[A, N] {type Out = Out0}
def instance[A, N <: Nat, Out0](f: (List[A], Sized[Seq[A => String], N]) => Out0): Aux[A, N, Out0] = new AsMap[A, N] {
type Out = Out0
override def apply(list: List[A], selectors: Sized[Seq[A => String], N]): Out = f(list, selectors)
}
implicit def zero[A]: Aux[A, _0, List[A]] = instance((l, _) => l)
implicit def succ[A, N <: Nat](implicit
asMap: AsMap[A, N]
): Aux[A, Succ[N], Map[String, asMap.Out]] =
instance((l, sels) => l.groupBy(sels.head).view.mapValues(asMap(_, sels.tail)).toMap)
}
final class NestedStrMap[A, N <: Nat](list: List[A], selectors: (A => String)*){
def asMap(implicit asMap: AsMap[A, N]): asMap.Out =
asMap(list, Sized.wrap[Seq[A => String], N](selectors))
}
object NestedStrMap {
def apply[N <: Nat] = new PartiallyApplied[N]
class PartiallyApplied[N <: Nat] {
def apply[A](list: List[A])(selectors: (A => String)*) = new NestedStrMap[A, N](list, selectors: _*)
}
}
case class TestResult(name: String, testType: String, score: Int)
val testList: List[TestResult] = List(
TestResult("A", "math", 75),
TestResult("B", "math", 80),
TestResult("B", "bio", 90),
TestResult("C", "history", 50)
)
val nestedMap = NestedStrMap[_2](testList)(_.name, _.testType)
val someMap = nestedMap.asMap
someMap: Map[String, Map[String, List[TestResult]]]
//Map(A -> Map(math -> List(TestResult(A,math,75))), B -> Map(bio -> List(TestResult(B,bio,90)), math -> List(TestResult(B,math,80))), C -> Map(history -> List(TestResult(C,history,50))))
In Scala 2.13, instead of import scala.collection.Seq (i.e. if you want Seq to refer to scala.Seq aka scala.collection.immutable.Seq, which is standard for Scala 2.13, rather than to scala.collection.Seq) then you can define
implicit def immutableSeqAdditiveCollection[T]:
shapeless.AdditiveCollection[collection.immutable.Seq[T]] = null
(Not sure why this implicit isn't defined, I guess it should.)
Cats auto derived with Seq
If you don't want to specify N manually, you can define a macro
import scala.language.experimental.macros
import scala.reflect.macros.whitebox // libraryDependencies += scalaOrganization.value % "scala-reflect" % scalaVersion.value
object NestedStrMap {
def apply[A](list: List[A])(selectors: (A => String)*): NestedStrMap[A, _ <: Nat] = macro applyImpl[A]
def applyImpl[A: c.WeakTypeTag](c: whitebox.Context)(list: c.Tree)(selectors: c.Tree*): c.Tree = {
import c.universe._
val A = weakTypeOf[A]
val len = selectors.length
q"new NestedStrMap[$A, _root_.shapeless.nat.${TypeName(s"_$len")}]($list, ..$selectors)"
}
}
// in a different subproject
val nestedMap = NestedStrMap(testList)(_.name, _.testType)
val someMap = nestedMap.asMap
someMap: Map[String, Map[String, List[TestResult]]]
//Map(A -> Map(math -> List(TestResult(A,math,75))), B -> Map(bio -> List(TestResult(B,bio,90)), math -> List(TestResult(B,math,80))), C -> Map(history -> List(TestResult(C,history,50))))
This will not work with
val sels = Seq[TestResult => String](_.name, _.testType)
val nestedMap = NestedStrMap(testList)(sels: _*)
because sels is a runtime value.
Alternatively to Shapeless, you can apply macros from the very beginning (with foldRight/foldLeft as you wanted)
import scala.language.experimental.macros
import scala.reflect.macros.whitebox
final class NestedStrMap[A](list: List[A])(selectors: (A => String)*) {
def asMap: Any = macro NestedStrMapMacro.asMapImpl[A]
}
object NestedStrMapMacro {
def asMapImpl[A: c.WeakTypeTag](c: whitebox.Context): c.Tree = {
import c.universe._
val A = weakTypeOf[A]
val ListA = weakTypeOf[List[A]]
c.prefix.tree match {
case q"new NestedStrMap[..$_]($list)(..$selectors)" =>
val func = selectors.foldRight(q"_root_.scala.Predef.identity[$ListA]")((sel, acc) =>
q"(_: $ListA).groupBy($sel).view.mapValues($acc).toMap"
)
q"$func.apply($list)"
}
}
}
// in a different subproject
val someMap = new NestedStrMap(testList)(_.name, _.testType).asMap
someMap: Map[String, Map[String, List[TestResult]]]
//Map(A -> Map(math -> List(TestResult(A,math,75))), B -> Map(bio -> List(TestResult(B,bio,90)), math -> List(TestResult(B,math,80))), C -> Map(history -> List(TestResult(C,history,50))))