Scala: построитель вложенных строковых карт

Я хочу создать класс с именем 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?

Это выполнимо, я могу придумать три возможных решения. - 1. Определите свой собственный ADT для представления вложенных карт, это было бы очень ванильно, но потеряет некоторую безопасность типов (аналогично тому, как можно было бы определить тип данных Json). - 2. Напишите генератор исходного кода sbt для реализации каждой перегрузки (до некоторого произвольного числа, такого как 22), таким образом будет сохранена безопасность типов. - 3. использовать Shapeless (или что-то подобное, например, Magnolia или макросы) для написания универсальной версии, которая вычисляет соответствующий тип во время компиляции.

Luis Miguel Mejía Suárez 10.04.2023 22:33
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
1
59
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Вы хотите вернуться 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))))

Другие вопросы по теме