web-dev-qa-db-fra.com

Classe de cas à mapper en Scala

Existe-t-il un moyen intéressant de convertir une instance Scala case class, par exemple.

case class MyClass(param1: String, param2: String)
val x = MyClass("hello", "world")

en une cartographie quelconque, par exemple.

getCCParams(x) returns "param1" -> "hello", "param2" -> "world"

Ce qui fonctionne pour n'importe quelle classe de cas, pas seulement ceux prédéfinis. J'ai découvert que vous pouvez extraire le nom de la classe de cas en écrivant une méthode qui interroge la classe de produit sous-jacente, par exemple.

def getCCName(caseobj: Product) = caseobj.productPrefix 
getCCName(x) returns "MyClass"

Je recherche donc une solution similaire mais pour les champs de classe de cas. J'imagine qu'une solution pourrait devoir utiliser la réflexion Java, mais je ne voudrais pas écrire quelque chose qui pourrait tomber en panne dans une future version de Scala si l'implémentation sous-jacente des classes de cas change.

Actuellement, je travaille sur un serveur Scala et je définis le protocole ainsi que tous ses messages et exceptions à l'aide de classes de cas, car elles constituent une construction aussi belle et concise que celle-ci. Mais je dois ensuite les traduire en une carte Java pour envoyer la couche de messagerie pour une implémentation cliente à utiliser. Mon implémentation actuelle ne fait que définir une traduction pour chaque classe de cas séparément, mais il serait bien de trouver une solution généralisée.

68
Will

Cela devrait fonctionner:

def getCCParams(cc: AnyRef) =
  (Map[String, Any]() /: cc.getClass.getDeclaredFields) {(a, f) =>
    f.setAccessible(true)
    a + (f.getName -> f.get(cc))
  }
81
Walter Chang

Parce que les classes de cas étendent Produit , on peut simplement utiliser .productIterator pour obtenir les valeurs de champ:

def getCCParams(cc: Product) = cc.getClass.getDeclaredFields.map( _.getName ) // all field names
                .Zip( cc.productIterator.to ).toMap // zipped with all values

Ou bien:

def getCCParams(cc: Product) = {          
      val values = cc.productIterator
      cc.getClass.getDeclaredFields.map( _.getName -> values.next ).toMap
}

Un des avantages de Product est qu'il n'est pas nécessaire d'appeler setAccessible sur le terrain pour lire sa valeur. Une autre est que productIterator n'utilise pas de réflexion.

Notez que cet exemple fonctionne avec des classes de cas simples qui ne développent pas d'autres classes et ne déclarent pas de champs en dehors du constructeur.

35
Andrejs

Si quelqu'un cherche une version récursive, voici la modification de la solution de @ Andrejs:

def getCCParams(cc: Product): Map[String, Any] = {
  val values = cc.productIterator
  cc.getClass.getDeclaredFields.map {
    _.getName -> (values.next() match {
      case p: Product if p.productArity > 0 => getCCParams(p)
      case x => x
    })
  }.toMap
}

Il étend également les classes de cas imbriquées en cartes à n'importe quel niveau d'imbrication.

12
Piotr Krzemiński

Voici une variante simple si vous ne voulez pas en faire une fonction générique:

case class Person(name:String, age:Int)

def personToMap(person: Person): Map[String, Any] = {
  val fieldNames = person.getClass.getDeclaredFields.map(_.getName)
  val vals = Person.unapply(person).get.productIterator.toSeq
  fieldNames.Zip(vals).toMap
}

scala> println(personToMap(Person("Tom", 50)))
res02: scala.collection.immutable.Map[String,Any] = Map(name -> Tom, age -> 50)
5
ShawnFumo

Vous pourriez utiliser sans forme.

Laisser

case class X(a: Boolean, b: String,c:Int)
case class Y(a: String, b: String)

Définir une représentation générique étiquetée

import shapeless._
import shapeless.ops.product._
import shapeless.syntax.std.product._
object X {
  implicit val lgenX = LabelledGeneric[X]
}
object Y {
  implicit val lgenY = LabelledGeneric[Y]
}

Définir deux classes de types pour fournir les méthodes toMap

object ToMapImplicits {

  implicit class ToMapOps[A <: Product](val a: A)
    extends AnyVal {
    def mkMapAny(implicit toMap: ToMap.Aux[A, Symbol, Any]): Map[String, Any] =
      a.toMap[Symbol, Any]
        .map { case (k: Symbol, v) => k.name -> v }
  }

  implicit class ToMapOps2[A <: Product](val a: A)
    extends AnyVal {
    def mkMapString(implicit toMap: ToMap.Aux[A, Symbol, Any]): Map[String, String] =
      a.toMap[Symbol, Any]
        .map { case (k: Symbol, v) => k.name -> v.toString }
  }
}

Ensuite, vous pouvez l'utiliser comme ça.

object Run  extends App {
  import ToMapImplicits._
  val x: X = X(true, "bike",26)
  val y: Y = Y("first", "second")
  val anyMapX: Map[String, Any] = x.mkMapAny
  val anyMapY: Map[String, Any] = y.mkMapAny
  println("anyMapX = " + anyMapX)
  println("anyMapY = " + anyMapY)

  val stringMapX: Map[String, String] = x.mkMapString
  val stringMapY: Map[String, String] = y.mkMapString
  println("anyMapX = " + anyMapX)
  println("anyMapY = " + anyMapY)
}

qui imprime 

anyMapX = Carte (c -> 26, b -> vélo, a -> vrai)

anyMapY = Map (b -> seconde, a -> première)

stringMapX = Carte (c -> 26, b -> vélo, a -> vrai)

stringMapY = Map (b -> seconde, a -> première)

Pour les classes de cas imbriquées, (donc les cartes imbriquées) Check une autre réponse

4
Harry Laou

Solution avec ProductCompletion du paquet interprète:

import tools.nsc.interpreter.ProductCompletion

def getCCParams(cc: Product) = {
  val pc = new ProductCompletion(cc)
  pc.caseNames.Zip(pc.caseFields).toMap
}
4
Stefan Endrullis

Si vous utilisez des Json4, vous pouvez effectuer les opérations suivantes:

import org.json4s.{Extraction, _}

case class MyClass(param1: String, param2: String)
val x = MyClass("hello", "world")

Extraction.decompose(x)(DefaultFormats).values.asInstanceOf[Map[String,String]]
3
Barak BN

Je ne sais pas pour Nice ... mais cela semble fonctionner, du moins pour cet exemple très très basique. Cela nécessite probablement un peu de travail mais pourrait être suffisant pour vous aider à démarrer? Fondamentalement, il filtre toutes les méthodes "connues" d'une classe de cas (ou de toute autre classe: /)

object CaseMappingTest {
  case class MyCase(a: String, b: Int)

  def caseClassToMap(obj: AnyRef) = {
    val c = obj.getClass
    val predefined = List("$tag", "productArity", "productPrefix", "hashCode",
                          "toString")
    val casemethods = c.getMethods.toList.filter{
      n =>
        (n.getParameterTypes.size == 0) &&
        (n.getDeclaringClass == c) &&
        (! predefined.exists(_ == n.getName))

    }
    val values = casemethods.map(_.invoke(obj, null))
    casemethods.map(_.getName).Zip(values).foldLeft(Map[String, Any]())(_+_)
  }

  def main(args: Array[String]) {
    println(caseClassToMap(MyCase("foo", 1)))
    // prints: Map(a -> foo, b -> 1)
  }
}
2
André Laszlo
commons.mapper.Mappers.Mappers.beanToMap(caseClassBean)

Détails: https://github.com/hank-whu/common4s

2
Kai Han

À partir de Scala 2.13, case classes (en tant qu'implémentations de Product ), une méthode productElementNames est renvoyée qui renvoie un itérateur sur le nom de leur champ.

En compressant les noms de champs avec les valeurs de champs obtenues avec productIterator , nous pouvons obtenir de manière générique la Map associée:

// case class MyClass(param1: String, param2: String)
// val x = MyClass("hello", "world")
(x.productElementNames Zip x.productIterator).toMap
// Map[String,Any] = Map("param1" -> "hello", "param2" -> "world")
1
Xavier Guihot