不思議なScala Macroの旅(三)-実際の応用

15044 ワード

前編では,macroを用いてLogを書き換えるdebug/info法を例示し,macroの基本文法,基本使用法,およびASTなどのmacroの背後にある概念を大まかに紹介した.では、本編では、scala-sqlプロジェクトにおける著者の実際の応用と結びつけて、macroが何に使えるかを示します.どう使いますか.
scala-sqlの概要
scala-sqlは軽量級のJDBCライブラリであり、scalaでリレーショナル・データベースにアクセスする簡単なAPIを提供し、その位置はscala開発者向けであり、spring-jdbc、iBatisを置き換えることができるデータ・アクセス・ライブラリを提供する.spring-jdbc、iBatisなどのライブラリに比べて、scala-sqlには独自の特徴があります.
  • scala言語向け.したがってjavaまたは他のプログラミング言語を選択するとscala-sqlは基本的に意味がありません.spring-jdbc,iBatisなどは明らかにこの制限を受けていない.
  • の概念は簡単です.scala-sqlはjavaです.sql.Connection, javax.sql.DataSourceなどのオブジェクトは,executeUpdate,rows,foreachなどの方法を拡張したが,その意味はjdbcの概念と完全に一致している.jdbcに詳しいプログラマー、scala文法に詳しいプログラマーは、scala-sqlには基本的に新しい概念を学ぶ必要はありません.
  • 強タイプ.scala-sqlは現在2.0バージョンで、Jdbcの複雑なsetParameter操作の代わりにsql「sql-statement」の補間構文を使用しており、強いタイプと拡張性があります.
  • 強タイプ:URL、FILEなどのオブジェクトを入力しようとすると、scalaコンパイラがエラーを検出し、無効なパラメータタイプを提示します.
  • 拡張可能:JDBCとは異なり、固定されたいくつかのタイプしか使用できません.scala-sqlはBound Context:JdbcValueAccessorで拡張されます.つまり、対応する拡張を定義すれば、URL、FILEもJDBCに伝達されるパラメータとして使用できます.(もちろん、JdbcValueAccessorで正しく処理する必要がある).
  • 関数サポート.scala-sqlはResultSetからObjectへのマッピングをサポートし、1.0バージョンではJavaBeanにマッピングされ、2.0ではCase Classにマッピングされます.Case Classを選択した理由も、関数プログラミングをよりよくサポートするためです.(Case Classをサポートすることは関数式プログラミングと何の関係がありますか?関数式プログラミングの真髄は副作用のない値変換であり、immutableこそ関数式プログラミングの真の愛である)
  • コンパイル中のSQL構文チェック.この特性は開発をより便利にすることができ、SQL構文エラーがある場合や、スペルを間違えたフィールド名、テーブル名などがある場合、コンパイル時期にエラーを発見することができ、エラーをより速く暴露し、テストサイクルを短縮することができます.

  • 上記のいくつかの特性では,ResultSetからCase Classへのマッピング,およびコンパイル時期のSQL構文チェックはscala macroに依存して行われる.
    Macroを使用してResultSetからCase Classへのマッピングをサポート
    scala-sql 1.0のバージョンでは、次のコードを使用できます.
    class User {  
     var name: String = _
     var age: Int = _
     var address: String = _ }
    val name = "Q%"
    val users: List[User] = dataSource.rows[User](sql"select * from users where name like $name")

    scala-sql 1.0バージョンでは、このマッピングは反射によって完了します.mutableを用いたデータ構造という欠点(FPに友好的ではない)を除いて,反射によりマッピングが完了し,−性能損失という欠点がある.(cacheを使用して反射情報を格納することで、注目する必要のないレベルにパフォーマンス損失を低減できます)-タイプチェック.userにマッピングに適さないフィールドタイプがある場合は、実行中にのみ理解できます.
    scala-sql 2.0バージョンではCase Class(JavaBeanへのサポートを破棄)をサポートすることが決定し、Case Class数immutableの場合、実行時にオブジェクトの構築を反射的に動的に行うこともできません.この問題を解決するには、Case Classごとにコンパイル時にResultSetからCase Classへのマッピングを静的に生成するしかありません.
      case class User(name:String, age:Int, address:String) trait ResultSetMapper[T] { 
    def from(rs: ResultSet): T }

    あるCase Class、例えばUserにとって、手動でコードを書くと、このコードは次のように形成されるResultSetMappper[User]の実装が必要です.
      /**      
       * the base class used in automate generated ResultSetMapper.      
       */ abstract class CaseClassResultSetMapper[T] extends ResultSetMapper[T] { case class Field[T: JdbcValueAccessor](name: String, default: Option[T] = None) {
           def apply(rs: ResultSetEx): T = {
             if ( rs hasColumn name ){ rs.get[T](name) }
             else { default match {
                 case Some(m) => m
                 case None => throw new RuntimeException(s"The ResultSet have no field $name but it is required") } } } } }
       object UserMapper extends CaseClassResultSetMapper[User] {
         override def from(rs: ResultSet): User = {
           val NAME = Field[String]("name")
           val AGE = Field[Int]("age")
           val CLASSROOM = Field[Int]("classRoom")
           User(NAME(rs), AGE(rs), CLASSROOM(rs)) } }

    しかし、Case Classごとに対応するMapperを定義する必要がある場合、このことはそれほど美しくありません.プログラマーは挑戦的なタスクをするのが好きで、Copy-Pasteに近いタスクを書くのが苦手か、いやになります.もちろん、大量のCopy-Pasteコードは実際には工事の災難を構成し、ある点を修正する必要がある場合、これはまるで災難です.したがって,プログラミングの実践分野では,「コードを繰り返すことが最大の品質問題である」という説があり,非常に理にかなっている.
    Macroは実際にはこのようなCopy-Pasteコードを効果的に解消する利器であり、macroがあれば定義されたCase Classに基づいて、必要な上のコードを自動的に生成することができ、同時に、コードを生成すると同時に、いくつかのタイプの検査を行い、コンパイル時にエラーを発見することができ、例えば、あるフィールドがResultSetからマッピングできない場合、コンパイル時にエラーを報告できます.
    実際、これも正式にmacroの応用シーンです.
      
      object ResultSetMapper {
    implicit def material[T]: ResultSetMapper[T] = macro Macros.generateCaseClassResultSetMapper[T] }
    object Macros { def generateCaseClassResultSetMapper[T: c.WeakTypeTag](c: scala.reflect.macros.whitebox.Context): c.Tree = {
    import c.universe._ val t: c.WeakTypeTag[T] = implicitly[c.WeakTypeTag[T]] // T TypeTag assert( t.tpe.typeSymbol.asClass.isCaseClass, s"only support CaseClass, but ${t.tpe.typeSymbol.fullName} is not" ) val companion = t.tpe.typeSymbol.asClass.companion // T , , Case Class // Case Class , Case Class val constructor: c.universe.MethodSymbol = t.tpe.typeSymbol.asClass.primaryConstructor.asMethod var index = 0 val args: List[(c.Tree, c.Tree)] = constructor.paramLists(0).map { (p: c.universe.Symbol) => val term: c.universe.TermSymbol = p.asTerm index += 1 // search "apply$default$X" val name = term.name.toString
    val newTerm = TermName(name.toString) val tree = if(term.isParamWithDefault) { // val defMethod: c.universe.Symbol = companion.asModule.typeSignature.member(TermName("$lessinit$greater$default$" + index)) q"""val $newTerm = Field[${term.typeSignature}]($name, Some($companion.$defMethod) )""" }
    else q"""val $newTerm = Field[${term.typeSignature}]($name) """ (q"""${newTerm}(rs)""", tree) } q"""        
            import wangzx.scala_commons.sql._        
            import java.sql.ResultSet          
            new CaseClassResultSetMapper[$t] {          
              ..${args.map(_._2)}  //            
              override def from(arg: ResultSet): $t = {            
                val rs = new ResultSetEx(arg)            
                new $t( ..${args.map(_._1) } )          
              }        
            }      
         """ } }

    Quasiquote(http://docs.scala-lang.org/overviews/quasiquotes/intro.html)、AST(ASTを構築することと、ASTから情報を抽出することを含む)をより容易に処理することができ、このコードは比較的簡潔である.いくつかの関連APIは、読者がScala Macros、Scala Reflection、Scala Quasiquotesなどのドキュメントを通じてさらに理解することができます.
    new $t( ..${args.map(_._1) } )    “..”    :
    scala> val ab = List(q"a", q"b")
    scala> val fab = q"f(..$ab)"
    fab: universe.Tree = f(a, b)


    Scala - Quasiquote  
    Splicing

    では、scala-sqlはどのようにしてタイプの検査を実現したのでしょうか.実際には、上記のコード自体はCase Classのフィールドのタイプをチェックするのではなく、生成されたコード:val NAME = Field[String]("name")によってタイプチェックが完了し、フィールドのタイプ、例えばURLであればval NAME = Field[URL]("name")はコンパイルチェックを通過できません.Field[T: JdbcValueAccessor]の構成では、すべてのデータ型がbound context: JdbcValueAccessor、すなわちimplicit val anyName: JdbcValueAccessor[T]の暗黙的な値を同時に備えなければならないため、この値によってTからSQL(parameter、ResultSetを含む)へのアクセス操作を処理する.
    もちろん、macroでは、Tをチェックし、一致しない場合は、より友好的なコンパイルエラーを表示することもできます.たとえば、 name URL, です.これもできます.後続のシナリオで、それを例示します.
    私たちのこのケースでは、Macroを使用してCase ClassのResultSet Mapperを生成し、Case ClassにJSON、XMLマッピングを提供する場合は、反射方式(GSON、fastjsonなど)などよりもmacroがより効率的なコードを生成し、より良いタイプのセキュリティサポートを提供することもできます.
    Macroを使用してSQL構文をコンパイル中にチェック
     implicit class SQLStringContext(sc: StringContext) {    
    def sql(args: JdbcValue[_]*) = SQLWithArgs(sc.parts.mkString("?"), args)    
    def SQL(args: JdbcValue[_]*): SQLWithArgs = macro Macros.parseSQL } object Macros {
    def parseSQL(c: reflect.macros.blackbox.Context)(args: c.Tree*): c.Tree = { import c.universe._ // SQL"..." sql , sql val q"""$a(scala.StringContext.apply(..$literals))""" = c.prefix.tree val it: c.Tree = c.enclosingClass // @db(name="dbname") db , // , sql 。 val db: String = it.symbol.annotations.flatMap { x => x.tree match {
    case q"""new $t($name)""" if t.symbol.asClass.fullName == classOf[db].getName => val Literal(Constant(str: String)) = name
    Some(str)
    case _ => None } } match {
    case Seq(name) => name
    case _ => "default" } val x: List[Tree] = literals // val stmt = x.map { case Literal(Constant(value: String)) => value }.mkString("?") try { // 。 SqlChecker.checkSqlGrammar(db, stmt, args.length) }
    catch {
    case ex: Throwable => // , 。 c.error(c.enclosingPosition, s"SQL grammar erorr ${ex.getMessage}") } q"""wangzx.scala_commons.sql.SQLWithArgs($stmt, Seq(..$args))""" } }

    この例では、前の例に比べて、次のような特性が追加されています.
  • は、現在の定義クラスの@db寸法情報を取得するなど、現在のコードの詳細情報を抽出する.
  • 現在のコードの文字列定数を抽出し、これらの文字列をさらにチェックします.この例では、コンパイル時にローカルのデータベースに接続することで、現在のSQL文をシミュレーションして実行し、構文をチェックしたり、テーブル名やフィールド名などの識別子のスペルが正しいかどうかをチェックしたりすることができます.

  • 同様に、macroを作成し、多くの文字列をさらにチェックすることができます.例:
  • 正規表現を構文チェックします.
  • は日付フォーマットの文法検査を行うが、これらは従来のコードでは、実行期間中に行われており、コンパイラに繰り上げることで、コードをより安全にするだけでなく、より敏捷な考え方にも合致する、Let it fail fast.

  • 参照先:
    不思議なScala Macroの旅(二)-一例
    変換元:
    不思議なScala Macroの旅(3)