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

后端 未结 2 717
名媛妹妹
名媛妹妹 2020-12-12 15:37

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(tit         


        
相关标签:
2条回答
  • 2020-12-12 16:04
    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]
      }
    }
    
    0 讨论(0)
  • 2020-12-12 16:12

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

    0 讨论(0)
提交回复
热议问题