29

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

Perfect Tiling
  • 661
  • 4
  • 13

2 Answers2

46

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

Travis Brown
  • 138,631
  • 12
  • 375
  • 680
  • 2
    This is pretty cool Travis! Can you explain this notation a bit `Creator[H :: T]`? How do I read the type here? – marios Nov 07 '15 at 18:42
  • 3
    This reads "HList whose head is of type H and whose tail is of type T". T is either another type level cons (H2 :: T2) or HNil to signify that there are no further values in that list. – Nicolas Rinaudo Nov 07 '15 at 18:46
  • Thanks Nicolas. HList is a shapeless construct? – marios Nov 07 '15 at 18:50
  • 3
    @marios Yep, it's essentially a tuple with some extra abstractions over arity. I'll add some detail when I'm back at a computer. – Travis Brown Nov 07 '15 at 19:00
  • 3
    @marios I've just expanded on this answer (probably way too much) in [this blog post](https://meta.plasm.us/posts/2015/11/08/type-classes-and-generic-derivation/). – Travis Brown Nov 08 '15 at 21:39
  • 1
    That was an amazing read. I strongly recommend anyone puzzled with the details of the solution to read the blog post. Thanks @TravisBrown! – marios Nov 09 '15 at 02:54
  • Thank you, Travis. Your extended blog post was extremely instructive. Very nicely done. I have a question about that: what do the notations `T <: HList: Parser` and `Parser[H :: T]` mean? – Perfect Tiling Nov 10 '15 at 04:21
2
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]
  }
}
Maxim
  • 7,268
  • 1
  • 32
  • 44
  • 1
    I really like the fact that this is simple, with no package dependencies. Of course it will be slow, due to reflection --- but I don't care about that for current purposes. (I'm assuming that the shapeless-based answer does the magic at compile time, without reflection...?!) – Perfect Tiling Nov 08 '15 at 00:13
  • 3
    @PerfectTiling The primary problem with this approach is that it's fragile and when it breaks, it breaks at runtime—the performance cost is unlikely to be significant in most cases. And right, there's no runtime reflection in the Shapeless solution (although it does use reflection—see my answer [here](http://stackoverflow.com/a/33580411/334519) for some discussion). – Travis Brown Nov 08 '15 at 15:34