Я хочу создать класс с именем NestedStrMap, где у него есть подпись как таковая:
final class NestedStrMap[A](list: List[A], first: A => String, rest: (A => String)*)
Я хочу написать внутри нее функцию asMap
, где я могу взять first
и rest
для построения вложенной карты. Однако я не могу понять, как определить возвращаемый тип этой функции.
def asMap = {
rest.toList.foldLeft(list.groupBy(first)) { (acc, i) =>
acc.view.mapValues(l => l.groupBy(i)).toMap // fails because the return type doesn't match
}
}
Вот пример того, как я хотел бы его использовать:
case class TestResult(name: String, testType: String, score: Int)
val testList = List(
TestResult("A", "math", 75),
TestResult("B", "math", 80),
TestResult("B", "bio", 90),
TestResult("C", "history", 50)
)
val nestedMap = NestedStrMap(testList, _.name, _.testType)
val someMap: Map[String, Map[String, List[TestResult]] = nestedMap.asMap
println(someMap)
/*
Map(
"A" -> Map("math" -> List(TestResult("A", "math", 75)))
"B" -> Map(
"math" -> List(TestResult("B", "math", 80)),
"bio" -> List(TestResult("B", "bio", 90))
),
"C" -> Map("history" -> List(TestResult("C", "history", 50)))
)
*/
Это выполнимо в scala?
Вы хотите вернуться Map[String, Map[String, ... Map[String, List[A]]]]
. Тип должен быть известен во время компиляции. Поэтому длина rest: (A => String)*
должна быть известна во время компиляции. Вы можете ввести класс шрифта, используя Shapeless Size
// 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))))
В Scala 2.13 вместо import scala.collection.Seq
(т.е. если вы хотите, чтобы Seq
ссылался на scala.Seq
или scala.collection.immutable.Seq
, что является стандартом для Scala 2.13, а не на scala.collection.Seq
), вы можете определить
implicit def immutableSeqAdditiveCollection[T]:
shapeless.AdditiveCollection[collection.immutable.Seq[T]] = null
(Не уверен, почему это неявное не определено, я думаю, что должно.)
Кошки, полученные автоматически с помощью Seq
Если вы не хотите указывать N
вручную, вы можете определить макрос
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))))
Это не будет работать с
val sels = Seq[TestResult => String](_.name, _.testType)
val nestedMap = NestedStrMap(testList)(sels: _*)
потому что sels
— это значение времени выполнения.
В качестве альтернативы Shapeless вы можете применять макросы с самого начала (с foldRight
/foldLeft
, как вы хотели)
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))))
Это выполнимо, я могу придумать три возможных решения. - 1. Определите свой собственный ADT для представления вложенных карт, это было бы очень ванильно, но потеряет некоторую безопасность типов (аналогично тому, как можно было бы определить тип данных
Json
). - 2. Напишите генератор исходного кода sbt для реализации каждой перегрузки (до некоторого произвольного числа, такого как22
), таким образом будет сохранена безопасность типов. - 3. использовать Shapeless (или что-то подобное, например, Magnolia или макросы) для написания универсальной версии, которая вычисляет соответствующий тип во время компиляции.