Construyendo clases de cajas Scala simples a partir de cuerdas, estrictamente sin placa de caldera
constructor boilerplate (2)
Busco un código sucinto para inicializar clases de casos de Scala simples desde cadenas (por ejemplo, una línea csv):
case class Person(name: String, age: Double)
case class Book(title: String, author: String, year: Int)
case class Country(name: String, population: Int, area: Double)
val amy = Creator.create[Person]("Amy,54.2")
val fred = Creator.create[Person]("Fred,23")
val hamlet = Creator.create[Book]("Hamlet,Shakespeare,1600")
val finland = Creator.create[Country]("Finland,4500000,338424")
¿Cuál es el objeto Creator
más simple para hacer esto? Aprendería mucho sobre Scala si viera una buena solución para esto.
(Tenga en cuenta que los objetos complementarios Person
, Book
y Country
no deben ser obligados a existir. ¡Eso sería una placa de calderas!)
Daré una solución que es tan simple como se le pueden dar algunas restricciones razonables sobre la seguridad de los tipos (sin excepciones de tiempo de ejecución, sin reflejo en el tiempo de ejecución, etc.), usando Shapeless para la derivación genérica:
import scala.util.Try
import shapeless._
trait Creator[A] { def apply(s: String): Option[A] }
object Creator {
def create[A](s: String)(implicit c: Creator[A]): Option[A] = c(s)
def instance[A](parse: String => Option[A]): Creator[A] = new Creator[A] {
def apply(s: String): Option[A] = parse(s)
}
implicit val stringCreate: Creator[String] = instance(Some(_))
implicit val intCreate: Creator[Int] = instance(s => Try(s.toInt).toOption)
implicit val doubleCreate: Creator[Double] =
instance(s => Try(s.toDouble).toOption)
implicit val hnilCreator: Creator[HNil] =
instance(s => if (s.isEmpty) Some(HNil) else None)
private[this] val NextCell = "^([^,]+)(?:,(.+))?$".r
implicit def hconsCreate[H: Creator, T <: HList: Creator]: Creator[H :: T] =
instance {
case NextCell(cell, rest) => for {
h <- create[H](cell)
t <- create[T](Option(rest).getOrElse(""))
} yield h :: t
case _ => None
}
implicit def caseClassCreate[C, R <: HList](implicit
gen: Generic.Aux[C, R],
rc: Creator[R]
): Creator[C] = instance(s => rc(s).map(gen.from))
}
Esto funciona exactamente como se especifica (aunque tenga en cuenta que los valores están ajustados en Option
para representar el hecho de que la operación de análisis puede fallar):
scala> case class Person(name: String, age: Double)
defined class Person
scala> case class Book(title: String, author: String, year: Int)
defined class Book
scala> case class Country(name: String, population: Int, area: Double)
defined class Country
scala> val amy = Creator.create[Person]("Amy,54.2")
amy: Option[Person] = Some(Person(Amy,54.2))
scala> val fred = Creator.create[Person]("Fred,23")
fred: Option[Person] = Some(Person(Fred,23.0))
scala> val hamlet = Creator.create[Book]("Hamlet,Shakespeare,1600")
hamlet: Option[Book] = Some(Book(Hamlet,Shakespeare,1600))
scala> val finland = Creator.create[Country]("Finland,4500000,338424")
finland: Option[Country] = Some(Country(Finland,4500000,338424.0))
Creator
aquí es una clase de tipo que proporciona evidencia de que podemos analizar una cadena en un tipo dado. Tenemos que proporcionar instancias explícitas para tipos básicos como String
, Int
, etc., pero podemos usar Shapeless para derivar genéricamente instancias para clases de casos (asumiendo que tenemos instancias de Creator
para todos sus tipos de miembros).
object Creator {
def create[T: ClassTag](params: String): T = {
val ctor = implicitly[ClassTag[T]].runtimeClass.getConstructors.head
val types = ctor.getParameterTypes
val paramsArray = params.split(",").map(_.trim)
val paramsWithTypes = paramsArray zip types
val parameters = paramsWithTypes.map {
case (param, clas) =>
clas.getName match {
case "int" => param.toInt.asInstanceOf[Object] // needed only for AnyVal types
case "double" => param.toDouble.asInstanceOf[Object] // needed only for AnyVal types
case _ =>
val paramConstructor = clas.getConstructor(param.getClass)
paramConstructor.newInstance(param).asInstanceOf[Object]
}
}
val r = ctor.newInstance(parameters: _*)
r.asInstanceOf[T]
}
}