2017-08-11 72 views
5

我有一個案例類包括大約20個領域,所有這些都是原始類型。我不得不從命令行解析這些字段(不幸的是)。 我可以,但我真的不想寫這20倍如何使用scala選項解析器解析泛型case類字段?

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

我能獲得通過反射的字段名稱和申請的類型,但我不知道如何堅持這些信息到該呼叫內for循環

我可以連接這個沒有形狀,但我仍然不熟悉這一點,這可以做到沒有無形?

==

斯卡拉選項解析器=> scopt

+0

我想通過「scala選項解析器」,你的意思是[scopt](https://github.com/scopt/scopt)? – thibr

+0

如果你想這樣做,你會發現自己重塑無形。您正在尋找類似'LabelledGeneric'的東西。 – Alec

+0

@Alec,只要我只需要寫一小段代碼,重新實現輪子就完全可以接受。 – zinking

回答

3

我只注意到你想要不一樣的庫無形。如果有任何安慰的話,這是一個將最終取代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找到相應的教程和相關文檔,我也很樂意回答您對此方法可能存在的任何問題!

+0

謝謝,我不想要無形的原因是它需要編譯插件,因爲我在scala 2.10上。把它放在一邊,我認爲只有一些通用和複雜的函數聲明才能解決它,爲什麼它必須是複雜的? – zinking

+0

我明白了。我假設你使用scala 2.10?如果你不是無形的,不需要任何編譯器插件來運行。我不認爲這是一個特別困難的問題,只是這個問題需要你在編譯時生成新的代碼,因此也就是宏。 –

+0

你是怎麼得出這樣的結論:這需要代碼生成?我認爲這可以在運行時實現。 – zinking

2

以下是僅使用運行時反射實現的版本。雖然它比宏觀基礎的解決方案那麼優雅,它只需要斯卡拉-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))) 

幾件事考慮:

  • 參數當它們被解析時,s被存儲在一個可變的Map中。不涉及使用類構造函數(copy方法)在最後一步中執行的案例類轉換。
  • 因此,在parse方法中傳遞的初始值根本沒有使用(但由於所有參數都是必需的,所以不應該出現問題)。
  • 您必須根據您的需要(您的案例類別值的類型)調整代碼以支持不同類型的參數。我只添加了StringInt(請參閱如果需要添加更多類型...評論)。