从字符串构造简单的Scala case类,严格避免样板代码。

29
我希望为简单的Scala case类从字符串(例如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")

有哪个最简单的Creator对象可以做到这一点呢?从看到一个好的解决方案中我将学到很多Scala。

(请注意,伴生对象PersonBookCountry不应该被强制存在。那样就是样板代码!)

2个回答

46

我将提供一个解决方案,考虑到类型安全的合理限制(无运行时异常,无运行时反射等),使用Shapeless进行通用推导,这是尽可能简单的方案:

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

这个工作完全按照规定的方式进行(请注意,值被包装在 Option 中表示解析操作可能会失败):

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是一个类型类,提供了我们可以将字符串解析为给定类型的证明。 我们必须为基本类型(如StringInt等)提供显式实例,但是我们可以使用Shapeless为case类派生通用实例(假设我们有所有成员类型的Creator实例)。


2
这很酷,Travis!你能解释一下这个符号 Creator[H :: T] 吗?我该如何阅读这个类型? - marios
3
这句话的意思是:“HList的头部是类型H,尾部是类型T。”其中,T可以是另一个类型层面的cons(H2 :: T2),或者是HNil,表示该列表中没有更多的值了。请注意,翻译时不可改变原文的含义,需要让内容更加通俗易懂。 - Nicolas Rinaudo
3
是的,它本质上是一个元组,但在元数方面有一些额外的抽象。我会在回到电脑前添加一些详细信息。 - Travis Brown
3
我刚才在这篇博客文章中对这个答案进行了更详细的解释(可能有点过头了)。链接在这里:https://meta.plasm.us/posts/2015/11/08/type-classes-and-generic-derivation/。 - Travis Brown
1
那是一篇非常棒的文章。我强烈推荐任何对解决方案细节感到困惑的人阅读这篇博客文章。感谢@TravisBrown! - marios
显示剩余2条评论

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

1
我真的很喜欢这个简单易用,没有包依赖的特点。当然,由于反射机制,它会比较慢 --- 但是对于当前的目的,我并不在意这一点。(我假设基于shapeless的答案可以在编译时完成魔法,而无需使用反射...?!) - Perfect Tiling
3
这种方法的主要问题在于它很脆弱,一旦出现问题,就会在运行时崩溃——在大多数情况下,性能代价不太可能显著。而且,在 Shapeless 解决方案中没有运行时反射(虽然它确实使用了反射——请参见我在这里的回答进行讨论)。 - Travis Brown

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接