我有一个案例类,包括大约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
答案 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
方法中传递的初始值根本没有使用(但是因为所有参数都是必需的,所以无关紧要。)String
和Int
(如果需要,请参阅添加更多类型... 评论)。