Как проанализировать общие поля класса дела с помощью анализатора опций scala?

У меня кейс-класс включает около 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 option parser => scopt

2 ответа

Я только заметил, что вы не хотели библиотек, подобных бесформенным. Если вас это утешит, это библиотека, которая в конечном итоге заменит макросы отражения Scala, так что она настолько близка, насколько вы получите чистую Scala, не изобретая велосипед.

Я думаю, что у меня есть кое-что, что может помочь с этим. Это своего рода тяжелое решение, но я думаю, что оно сделает то, что вы просите.

При этом используется фантастическая библиотека scalameta ( http://www.scalameta.org/) для создания статической аннотации. Вы аннотируете свой класс case, а затем этот встроенный макрос сгенерирует соответствующий анализатор scopt для ваших аргументов командной строки.

Вашему build.sbt понадобится плагин macro paradise, а также библиотека 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)

Это тогда во время компиляции расширит ваш класс case, чтобы включить определения 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/. Я также с удовольствием отвечу на любые вопросы, которые могут у вас возникнуть об этом подходе!

Вот версия, реализованная только с отражением во время выполнения. Хотя он менее элегантен, чем решение на основе макросов, он требует только scala-refle.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)))

Несколько вещей, чтобы принять во внимание:

  • Параметры сохраняются в изменяемой карте при их разборе. Преобразование класса case выполняется на последнем шаге с использованием конструктора класса (copy метод не задействован).
  • Как следствие, начальное значение передается в parse Метод не используется вообще (но это не должно иметь значения, так как все аргументы обязательны).
  • Вы должны настроить код для поддержки различных типов аргументов в соответствии с вашими потребностями (типами значений вашего класса дел). Я только добавил String а также Int (См. Добавить больше типов при необходимости... комментарий).
Другие вопросы по тегам