web-dev-qa-db-fra.com

Scala: convertir la carte en classe de cas

Disons que j'ai cette classe d'exemple

case class Test(key1: Int, key2: String, key3: String)

Et j'ai une carte

myMap = Map("k1" -> 1, "k2" -> "val2", "k3" -> "val3")

Je dois convertir cette carte dans ma classe de cas à plusieurs endroits du code, quelque chose comme ceci: 

myMap.asInstanceOf[Test]

Quel serait le moyen le plus simple de le faire? Puis-je en quelque sorte utiliser implicite pour cela?

23
Caballero

Deux façons de faire cela avec élégance. La première consiste à utiliser unapply, la seconde à utiliser une classe implicite (2.10+) avec une classe type pour effectuer la conversion à votre place.

1) L'inapplication est le moyen le plus simple et le plus direct d'écrire une telle conversion. Il ne fait aucune "magie" et peut facilement être trouvé si vous utilisez un IDE. Notez que faire ce genre de chose peut encombrer votre objet compagnon et faire en sorte que votre code engendre des dépendances dans des endroits que vous ne souhaitez peut-être pas:

object MyClass{
  def unapply(values: Map[String,String]) = try{
    Some(MyClass(values("key").toInteger, values("next").toFloat))
  } catch{
    case NonFatal(ex) => None
  }
}

Ce qui pourrait être utilisé comme ceci:

val MyClass(myInstance) = myMap

soyez prudent, car cela jetterait une exception s'il n'était pas assorti complètement.

2) Faire une classe implicite avec une classe type crée plus de passe-partout, mais laisse également beaucoup de place pour développer le même motif et l’appliquer à d’autres classes de cas:

implicit class Map2Class(values: Map[String,String]){
  def convert[A](implicit mapper: MapConvert[A]) = mapper conv (values)
}

trait MapConvert[A]{
  def conv(values: Map[String,String]): A
}

et à titre d'exemple, vous feriez quelque chose comme ceci:

object MyObject{
  implicit val new MapConvert[MyObject]{
    def conv(values: Map[String, String]) = MyObject(values("key").toInt, values("foo").toFloat)
  }
}

qui pourrait alors être utilisé exactement comme vous l'avez décrit ci-dessus:

val myInstance = myMap.convert[MyObject]

lancer une exception si aucune conversion ne peut être faite. L'utilisation de ce modèle pour convertir un Map[String, String] en n'importe quel objet nécessiterait simplement un autre implicite (implicite dans la portée.)

24
wheaties

Voici une méthode alternative non passe-partout qui utilise la réflexion de Scala (Scala 2.10 et plus) et ne nécessite pas de module compilé séparément:

import org.specs2.mutable.Specification
import scala.reflect._
import scala.reflect.runtime.universe._

case class Test(t: String, ot: Option[String])

package object ccFromMap {
  def fromMap[T: TypeTag: ClassTag](m: Map[String,_]) = {
    val rm = runtimeMirror(classTag[T].runtimeClass.getClassLoader)
    val classTest = typeOf[T].typeSymbol.asClass
    val classMirror = rm.reflectClass(classTest)
    val constructor = typeOf[T].decl(termNames.CONSTRUCTOR).asMethod
    val constructorMirror = classMirror.reflectConstructor(constructor)

    val constructorArgs = constructor.paramLists.flatten.map( (param: Symbol) => {
      val paramName = param.name.toString
      if(param.typeSignature <:< typeOf[Option[Any]])
        m.get(paramName)
      else
        m.get(paramName).getOrElse(throw new IllegalArgumentException("Map is missing required parameter named " + paramName))
    })

    constructorMirror(constructorArgs:_*).asInstanceOf[T]
  }
}

class CaseClassFromMapSpec extends Specification {
  "case class" should {
    "be constructable from a Map" in {
      import ccFromMap._
      fromMap[Test](Map("t" -> "test", "ot" -> "test2")) === Test("test", Some("test2"))
      fromMap[Test](Map("t" -> "test")) === Test("test", None)
    }
  }
}
14
jkschneider

Jonathan Chow implémente une macro Scala (conçue pour Scala 2.11) qui généralise ce comportement et élimine le passe-partout.

http://blog.echo.sh/post/65955606729/exploring-scala-macros-map-to-case-class-conversion

import scala.reflect.macros.Context

trait Mappable[T] {
  def toMap(t: T): Map[String, Any]
  def fromMap(map: Map[String, Any]): T
}

object Mappable {
  implicit def materializeMappable[T]: Mappable[T] = macro materializeMappableImpl[T]

  def materializeMappableImpl[T: c.WeakTypeTag](c: Context): c.Expr[Mappable[T]] = {
    import c.universe._
    val tpe = weakTypeOf[T]
    val companion = tpe.typeSymbol.companionSymbol

    val fields = tpe.declarations.collectFirst {
      case m: MethodSymbol if m.isPrimaryConstructor ⇒ m
    }.get.paramss.head

    val (toMapParams, fromMapParams) = fields.map { field ⇒
      val name = field.name
      val decoded = name.decoded
      val returnType = tpe.declaration(name).typeSignature

      (q"$decoded → t.$name", q"map($decoded).asInstanceOf[$returnType]")
    }.unzip

    c.Expr[Mappable[T]] { q"""
      new Mappable[$tpe] {
        def toMap(t: $tpe): Map[String, Any] = Map(..$toMapParams)
        def fromMap(map: Map[String, Any]): $tpe = $companion(..$fromMapParams)
      }
    """ }
  }
}
5
jkschneider

Je n'aime pas ce code, mais je suppose que cela est possible si vous pouvez obtenir les valeurs de carte dans un tuple, puis utiliser le constructeur tupled pour votre classe de cas. Cela ressemblerait à ceci:

val myMap = Map("k1" -> 1, "k2" -> "val2", "k3" -> "val3")    
val params = Some(myMap.map(_._2).toList).flatMap{
  case List(a:Int,b:String,c:String) => Some((a,b,c))
  case other => None
}    
val myCaseClass = params.map(Test.tupled(_))
println(myCaseClass)

Vous devez faire attention à vous assurer que la liste de valeurs contient exactement 3 éléments et qu'ils correspondent aux types corrects. Sinon, vous vous retrouvez avec un non. Comme je l'ai dit, pas génial, mais cela montre que c'est possible.

2
cmbaxter
commons.mapper.Mappers.mapToBean[CaseClassBean](map)

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

0
Kai Han