How to handle the general fields of the case class using the scala parameter parser?

My case class includes about 20 fields, all of which are primitive types.

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

and I have to parse all these fields from the command line (unfortunately). I can, but I really don't want to write this 20 times

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

I can get the field name and record type through reflection, but I don’t know how to pin this information for this call in a for loop

I can associate this with the formless, but I am still not familiar with this, and can this be done without the formless?

==

scala parameter parser => scopt

+5
source share
2 answers

I just noticed that you do not want libraries to be formless. If this is a consolation, then this is a library that will eventually replace scala macros, so you will get about the same as pure scala without reinventing the wheel.

I think I may have something that can help with this. This is kind of a tough decision, but I think he will do what you ask.

To create a static annotation, a fantastic rock scale is used ( http://www.scalameta.org ). You annotate your case class, and this built-in macro will generate the appropriate scopt parser for your command line arguments.

In your build.sbt you will need the macro paradise plugin as well as the scalameta library. You can add them to your project with.

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

After you add these depilations to your assembly, you will have to create a separate project for your macros.

A full SBT project definition will look like

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

If the module itself is called "macros", then create a class, and here is a static annotation.

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

After that, your main module will depend on your macro module. Then you can annotate your case classes like this ...

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

Then at compile time, expand your case class to include scopt definitions. Here's what the generated class looks like on top.

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

This should save a lot of boiler plate, and for those who have more knowledge about built-in macros, feel free to tell me how I could write it better, since I'm not an expert in this.

You can find the appropriate tutorial and documentation on this at http://scalameta.org/tutorial/#Macroannotations I will also be happy to answer any questions you may have with this approach!

+3
source

Here is a version implemented only with runtime display. Although less elegant than a macro-based solution, only scala -reflect.jar is required:

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

code:

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

Usage example:

 case class A(f1: String, f2: Int) println(genericParser[A]("main").parse(args, A("", -1))) 

A few things to consider:

  • Parameters are saved in the map to be modified when they are parsed. The conversion of the case class performed in the last step using the class constructor ( copy , not involved).
  • As a result, the initial value passed in the parse method is not used at all (but it does not matter, since all arguments are required).
  • You must configure the code to support various types of arguments according to your needs (value types of your case class). I added only String and Int (see Adding additional types if necessary ... comment).
+2
source

Source: https://habr.com/ru/post/1270773/


All Articles