Подтвердить что ты не робот

Scala: преобразовать карту в класс case

Скажем, у меня есть этот примерный класс case

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

И у меня есть карта

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

Мне нужно преобразовать эту карту в мой класс case в нескольких местах кода, примерно так:

myMap.asInstanceOf[Test]

Каким будет самый простой способ сделать это? Могу ли я каким-то образом использовать для этого неявное?

4b9b3361

Ответ 1

Два способа сделать это элегантно. Во-первых, используйте unapply, второй - использовать неявный класс (2.10+) с классом типа для преобразования для вас.

1) Unapply - это самый простой и самый прямой способ написать такое преобразование. Он не делает никаких "магии" и может быть легко обнаружен при использовании IDE. Помните, что подобные вещи могут загромождать ваш сопутствующий объект и заставлять ваш код прорастать зависимости в местах, которые вам могут не понадобиться:

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

Что можно использовать следующим образом:

val MyClass(myInstance) = myMap

будьте осторожны, так как это вызовет исключение, если оно не полностью соответствует.

2) Выполнение неявного класса с классом типа создает для вас больше шаблонов, а также позволяет расширить пространство для того же шаблона, который применим к другим классам классов:

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
}

и в качестве примера вы бы сделали что-то вроде этого:

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

который затем можно использовать так же, как вы описали выше:

val myInstance = myMap.convert[MyObject]

выброс исключения, если преобразование не может быть выполнено. Используя этот шаблон, преобразование между Map[String, String] в любой объект потребует просто еще одного неявного (и подразумеваемого для видимости в области).

Ответ 2

Вот альтернативный метод без шаблонов, который использует отражение Scala (Scala 2.10 и выше) и не требует отдельного скомпилированного модуля:

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)
    }
  }
}

Ответ 3

Джонатан Чоу реализует макрос Scala (разработанный для Scala 2.11), который обобщает это поведение и устраняет шаблон.

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)
      }
    """ }
  }
}

Ответ 4

Мне не нравится этот код, но я предполагаю, что это возможно, если вы можете получить значения карты в кортеж, а затем использовать конструктор tupled для вашего класса case. Это будет выглядеть примерно так:

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)

Вы должны быть осторожны, чтобы убедиться, что список значений составляет ровно 3 элемента и что они являются правильными типами. Если нет, вы в конечном итоге получаете вместо него None. Как я уже сказал, не очень, но это показывает, что это возможно.

Ответ 6

Это хорошо работает для меня, если вы используете Джексона для Scala:

def from[T](map: Map[String, Any])(implicit m: Manifest[T]): T = {
  val mapper = new ObjectMapper() with ScalaObjectMapper
  mapper.convertValue(map)
}

Ссылка из: Преобразовать карту <String, String> в POJO