Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Constructing simple Scala case classes from Strings, strictly without boiler-plate

I seek succinct code to initialize simple Scala case classes from Strings (e.g. a csv line):

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

What's the simplest Creator object to do this? I would learn a lot about Scala from seeing a good solution to this.

(Note that companion objects Person, Book and Country should not to be forced to exist. That would be boiler-plate!)

like image 565
Perfect Tiling Avatar asked Nov 07 '15 17:11

Perfect Tiling


People also ask

How do I create a dynamic case class in Scala?

The steps are: Figure out a name for your class based on convention. Parse your CSV header to know field names and data types. Generate a case class with the above information and write to a file on disk.

For which kind of data should you use a case class in Scala?

A Scala Case Class is like a regular class, except it is good for modeling immutable data. It also serves useful in pattern matching, such a class has a default apply() method which handles object construction. A scala case class also has all vals, which means they are immutable.


2 Answers

I'm going to give a solution that's about as simple as you can get given some reasonable constraints about type safety (no runtime exceptions, no runtime reflection, etc.), using Shapeless for generic derivation:

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

This work exactly as specified (although note that the values are wrapped in Option to represent the fact that the parsing operation can fail):

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 here is a type class that provides evidence that we can parse a string into a given type. We have to provide explicit instances for basic types like String, Int, etc., but we can use Shapeless to generically derive instances for case classes (assuming that we have Creator instances for all of their member types).

like image 60
Travis Brown Avatar answered Sep 23 '22 15:09

Travis Brown


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]
  }
}
like image 20
Maxim Avatar answered Sep 24 '22 15:09

Maxim