如何使用scala选项解析器解析泛型案例类字段?

时间:2017-08-11 08:43:25

标签: scala generics shapeless

我有一个案例类,包括大约20个字段,所有字段都是原始类型。

case class A( f1: String, f2: Int .....)

我必须从命令行解析这些字段(不幸的是)。 我可以,但我真的不想写这20次

opt[String]("f1") required() valueName "<f1>" action { (x, c) =>
    c.copy(f1 = x)
  } text "f1 is required"
//...repeat 20 times

我可以通过反射获取字段名称和字段类型,但我不知道如何在for循环中将这些信息粘贴到此调用中

我可以将它与无形的连接起来,但我仍然不熟悉它,这可以在没有形状的情况下完成吗?

==

scala选项解析器=&gt; scopt

2 个答案:

答案 0 :(得分:3)

我刚注意到你想要没有像无形的图书馆。如果它是任何安慰,那么这个库最终将取代scala反射宏,所以它就像你将获得的纯scala一样接近而不重新发明轮子。

我想我可能会对此有所帮助。这是一种沉重的解决方案,但我认为它会做你要求的。

这使用了奇妙的scalameta(http://www.scalameta.org)库来创建静态注释。您将对您的case类进行注释,然后这个内联宏将为您的命令行args生成适当的scopt解析器。

你的build.sbt将需要宏天堂插件以及scalameta库。您可以使用。

将这些添加到项目中
addCompilerPlugin("org.scalameta" % "paradise" % paradise cross CrossVersion.full)
libraryDependencies ++= Seq(
    "org.scalameta" %% "scalameta" % meta % Provided,
)

将这些deps添加到构建中后,您必须为您的宏创建一个单独的项目。

完整的SBT项目定义看起来像

lazy val macros = project
  .in(file("macros"))
  .settings(
    addCompilerPlugin("org.scalameta" % "paradise" % paradise cross CrossVersion.full),
    libraryDependencies ++= Seq(
      "org.scalameta" %% "scalameta" % "1.8.0" % Provided,
    )
   )

如果模块本身被命名为“宏”,那么创建一个类,这里​​是静态注释。

import scala.annotation.{StaticAnnotation, compileTimeOnly}
import scala.meta._

@compileTimeOnly("@Opts not expanded")
class Opts extends StaticAnnotation {
  inline def apply(defn: Any): Any = meta {
    defn match {
      case q"..$mods class $tname[..$tparams] ..$ctorMods (...$paramss) extends $template" =>
        val opttpe = Type.Name(tname.value)
        val optName = Lit.String(tname.value)
        val opts = paramss.flatten.map {
          case param"..${_} $name: ${tpeopt: Option[Type]} = $expropt" =>
            val tpe = Type.Name(tpeopt.get.toString())
            val litName = Lit.String(name.toString())
            val errMsg = Lit.String(s"${litName.value} is required.")
            val tname = Term.Name(name.toString())
            val targ = Term.Arg.Named(tname, q"x")
            q"""
                opt[$tpe]($litName)
                  .required()
                  .action((x, c) => c.copy($targ))
                  .text($errMsg)
            """
        }
        val stats = template.stats.getOrElse(Nil) :+ q"def options: OptionParser[$opttpe] = new OptionParser[$opttpe]($optName){ ..$opts }"
        q"""..$mods class $tname[..$tparams] ..$ctorMods (...$paramss) {
            import scopt._
            ..$stats
        }"""
    }
  }
}

之后,您将使主模块依赖于您的宏模块。然后你就可以注释你的案例类......

@Opts
case class Options(name: String, job: String, age: Int, netWorth: Double, job_title: String)

这将在编译时扩展您的案例类以包含scopt定义。以下是生成的类从上面看起来的样子。

case class Options(name: String, job: String, age: Int, netWorth: Double, job_title: String) {
  import scopt._

  def options: OptionParser[Options] = new OptionParser[Options]("Options") {
    opt[String]("name").required().action((x, c) => c.copy(name = x)).text("name is required.")
    opt[String]("job").required().action((x, c) => c.copy(job = x)).text("job is required.")
    opt[Int]("age").required().action((x, c) => c.copy(age = x)).text("age is required.")
    opt[Double]("netWorth").required().action((x, c) => c.copy(netWorth = x)).text("netWorth is required.")
    opt[String]("job_title").required().action((x, c) => c.copy(job_title = x)).text("job_title is required.")
  }
}

这应该可以节省大量的锅炉板,对于任何对内联宏有更多了解的人,请随时告诉我如何更好地写这个,因为我不是这方面的专家。

您可以在http://scalameta.org/tutorial/#Macroannotations找到相应的相关教程和相关文档。我也很乐意回答您对此方法的任何疑问!

答案 1 :(得分:2)

这是仅使用运行时反射实现的版本。虽然它不如基于宏的解决方案优雅,但它只需要scala-reflect.jar:

libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value

代码:

import scala.collection.mutable
import scala.reflect.runtime.universe._

def genericParser[T: TypeTag](programName: String): OptionParser[T] = new OptionParser[T](programName) {
  val StringTpe: Type = typeOf[String]

  val fields: List[MethodSymbol] = typeOf[T].decls.sorted.collect {
    case m: MethodSymbol if m.isCaseAccessor ⇒ m
  }

  val values = mutable.Map.empty[TermName, Any]

  /**
    * Returns an instance of a [[scopt.Read]] corresponding to the provided type
    */
  def typeToRead(tpe: Type): Read[Any] = (tpe match {
    case definitions.IntTpe ⇒ implicitly[Read[Int]]
    case StringTpe          ⇒ implicitly[Read[String]]
      // Add more types if necessary...
  }) map identity[Any]

  for (f ← fields) {
    // kind of dynamic implicit resolution
    implicit val read: Read[Any] = typeToRead(f.returnType)
    opt[Any](f.name.toString) required() valueName s"<${f.name}>" foreach { value ⇒
      values(f.name) = value
    } text s"${f.name} is required"
  }

  override def parse(args: Seq[String], init: T): Option[T] = {
    super.parse(args, init) map { _ ⇒
      val classMirror = typeTag[T].mirror.reflectClass(typeOf[T].typeSymbol.asClass)
      val constructor = typeOf[T].decl(termNames.CONSTRUCTOR).asMethod
      val constructorMirror = classMirror.reflectConstructor(constructor)
      val constructorArgs = constructor.paramLists.flatten.map(symbol ⇒ values(symbol.asTerm.name))

      constructorMirror(constructorArgs: _*).asInstanceOf[T]
    }
  }
}

使用示例:

case class A(f1: String, f2: Int)

println(genericParser[A]("main").parse(args, A("", -1)))

需要考虑的一些事项:

  • 解析时,参数存储在可变映射中。使用类构造函数(copy方法在最后一步中执行的案例类转换不参与)。
  • 因此,parse方法中传递的初始值根本没有使用(但是因为所有参数都是必需的,所以无关紧要。)
  • 您必须调整代码以支持不同类型的参数,根据您的需要(您的案例类值的类型)。我只添加了StringInt(如果需要,请参阅添加更多类型... 评论)。