【问题标题】:how to parse generic case class fields using scala option parser?如何使用 scala 选项解析器解析通用案例类字段?
【发布时间】:2018-01-19 16:55:42
【问题描述】:

我有一个案例类,包括大约 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 循环中的此调用中

我可以将它与 shapeless 联系起来,但我仍然不熟悉,如果没有 shapeless 可以做到这一点吗?

==

scala 选项解析器 => scopt

【问题讨论】:

  • 我想“scala option parser”是指scopt ?
  • 如果你想这样做,你会发现自己正在重塑无形。你正在寻找类似LabelledGeneric的东西。
  • @Alec,重新实现轮子是完全可以接受的,只要我只需要写一小段代码。
  • @zinking 问题是你需要在某个地方使用宏——你需要某种方法来提取字段、它们的名称和它们的类型。这需要大量非类型安全的反射或带有一些无形样式类型级编程的宏。
  • 在这种情况下我可以忍受非类型安全的反射。

标签: scala generics shapeless


【解决方案1】:

我刚刚注意到您不需要像 shapeless 这样的库。如果有什么安慰的话,这是一个最终将取代 scala 反射宏的库,因此它与您在不重新发明轮子的情况下获得的纯 scala 一样接近。

我想我可能有一些可能对此有所帮助的东西。这是一种繁重的解决方案,但我认为它会满足您的要求。

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

您的 build.sbt 将需要宏天堂插件以及 scalameta 库。您可以将这些添加到您的项目中。

addCompilerPlugin("org.scalameta" % "paradise" % paradise cross CrossVersion.full)
libraryDependencies ++= Seq(
    "org.scalameta" %% "scalameta" % meta % Provided,
)

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

完整的 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 找到相应的教程和文档,我也很乐意回答您可能对这种方法提出的任何问题!

【讨论】:

  • 谢谢,我不想要 shapeless 的原因是它对编译器插件的要求,因为我在 scala 2.10 上。抛开这些,我认为只有一些通用和复杂的函数声明才能解决它,为什么它必须那么复杂?
  • 我明白了。我假设您使用的是 scala 2.10?如果您不是无形的,则不需要任何编译器插件即可运行。我不认为这是一个特别难的问题,只是这个问题需要您在编译时生成新代码,因此需要宏。
  • 您实际上是如何得出这需要代码生成的结论的?我认为这可以在运行时实现。
  • @zinking,我不认为声明 c.copy(f1 = x)) 可以在没有某种形式的代码生成的情况下一概而论。 f1 被命名为在编译时必须知道的参数。案例类根本无法让您按索引/名称更新字段。一种解决方法是使用Map 而不是案例类。
  • 并不是明确需要代码生成,我相信你也可以通过反射来做到这一点。但是 Scala 反射你必须做很多容易出错的非类型安全的事情才能让它工作。这违背了 scopt 类型安全和案例类的目的。如果您在编译时有案例类,那么没有理由在运行时这样做,您会选择更慢、更容易出错、非类型安全的方法,除非您有一些非常严格或非标准的要求这不是最好的方法。
【解决方案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(请参阅Add more types if necessary...评论)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-06-25
    • 2018-10-23
    • 1970-01-01
    相关资源
    最近更新 更多