Argumen introspeksi diteruskan ke makro Scala

Saya ingin memprogram makro Scala yang mengambil instance kelas kasus sebagai argumen. Semua objek yang bisa diteruskan ke makro harus mengimplementasikan sifat penanda tertentu.

Cuplikan berikut menunjukkan sifat penanda dan dua contoh kelas kasus yang menerapkannya:

trait Domain
case class Country( id: String, name: String ) extends Domain
case class Town( id: String, longitude: Double, latitude: Double ) extends Domain

Sekarang, saya ingin menulis kode berikut menggunakan makro untuk menghindari beratnya refleksi runtime dan ketidakamanan threadnya:

object Test extends App {

  // instantiate example domain object
  val myCountry = Country( "CH", "Switzerland" )

  // this is a macro call
  logDomain( myCountry )
} 

Makro logDomain diimplementasikan dalam proyek berbeda dan terlihat mirip dengan:

object Macros {
  def logDomain( domain: Domain ): Unit = macro logDomainMacroImpl

  def logDomainMacroImpl( c: Context )( domain: c.Expr[Domain] ): c.Expr[Unit] = {
    // Here I would like to introspect the argument object but do not know how?
    // I would like to generate code that prints out all val's with their values
  }
}

Tujuan makro adalah untuk menghasilkan kode yang - saat runtime - menampilkan semua nilai (id dan name) dari objek tertentu dan mencetaknya seperti yang ditunjukkan berikutnya:

id (String) : CH
name (String) : Switzerland

Untuk mencapai hal ini, saya harus secara dinamis memeriksa argumen tipe yang diteruskan dan menentukan anggotanya (vals). Kemudian saya harus membuat AST yang mewakili kode yang membuat keluaran log. Makro harus berfungsi terlepas dari objek spesifik apa yang mengimplementasikan sifat penanda "Domain" yang diteruskan ke makro.

Pada titik ini saya tersesat. Saya akan sangat menghargai jika seseorang dapat memberi saya titik awal atau mengarahkan saya ke beberapa dokumentasi? Saya relatif baru mengenal Scala dan belum menemukan solusi di dokumen Scala API atau panduan Makro.


person MontChanais    schedule 14.01.2013    source sumber


Jawaban (2)


Membuat daftar pengakses kelas kasus adalah operasi yang umum ketika Anda bekerja dengan makro sehingga saya cenderung menggunakan metode seperti ini:

def accessors[A: u.WeakTypeTag](u: scala.reflect.api.Universe) = {
  import u._

  u.weakTypeOf[A].declarations.collect {
    case acc: MethodSymbol if acc.isCaseAccessor => acc
  }.toList
}

Ini akan memberi kita semua simbol metode pengakses kelas kasus untuk A, jika ada. Perhatikan bahwa saya menggunakan API refleksi umum di sini—belum perlu menjadikannya khusus makro.

Kita dapat menyelesaikan metode ini dengan beberapa hal praktis lainnya:

trait ReflectionUtils {
  import scala.reflect.api.Universe

  def accessors[A: u.WeakTypeTag](u: Universe) = {
    import u._

    u.weakTypeOf[A].declarations.collect {
      case acc: MethodSymbol if acc.isCaseAccessor => acc
    }.toList
  }

  def printfTree(u: Universe)(format: String, trees: u.Tree*) = {
    import u._

    Apply(
      Select(reify(Predef).tree, "printf"),
      Literal(Constant(format)) :: trees.toList
    )
  }
}

Dan sekarang kita dapat menulis kode makro sebenarnya dengan cukup ringkas:

trait Domain

object Macros extends ReflectionUtils {
  import scala.language.experimental.macros
  import scala.reflect.macros.Context

  def log[D <: Domain](domain: D): Unit = macro log_impl[D]
  def log_impl[D <: Domain: c.WeakTypeTag](c: Context)(domain: c.Expr[D]) = {
    import c.universe._

    if (!weakTypeOf[D].typeSymbol.asClass.isCaseClass) c.abort(
      c.enclosingPosition,
      "Need something typed as a case class!"
    ) else c.Expr(
      Block(
        accessors[D](c.universe).map(acc =>
          printfTree(c.universe)(
            "%s (%s) : %%s\n".format(
              acc.name.decoded,
              acc.typeSignature.typeSymbol.name.decoded
            ),
            Select(domain.tree.duplicate, acc.name)
          )
        ),
        c.literalUnit.tree
      )
    )
  }
}

Perhatikan bahwa kita masih perlu melacak tipe kelas kasus tertentu yang sedang kita tangani, namun inferensi tipe akan menangani hal tersebut di lokasi panggilan—kita tidak perlu menentukan parameter tipe secara eksplisit.

Sekarang kita dapat membuka REPL, menempelkan definisi kelas kasus Anda, dan kemudian menulis yang berikut:

scala> Macros.log(Town("Washington, D.C.", 38.89, 77.03))
id (String) : Washington, D.C.
longitude (Double) : 38.89
latitude (Double) : 77.03

Or:

scala> Macros.log(Country("CH", "Switzerland"))
id (String) : CH
name (String) : Switzerland

Seperti yang diinginkan.

person Travis Brown    schedule 14.01.2013
comment
Anda mengalahkan saya dalam 5 menit! :) - person Eugene Burmako; 15.01.2013
comment
Terima kasih banyak atas jawaban detailnya! Contoh Anda melakukan persis apa yang saya cari. Saya akan mencobanya malam ini. Yang menyenangkan dari solusi Anda adalah penggunaan parameter type dan WeakTypeTag yang membuat kode sepenuhnya generik. Ini harus berfungsi untuk semua kasus kelas yang mengimplementasikan Domain. - person MontChanais; 15.01.2013

Dari apa yang saya lihat, Anda perlu menyelesaikan dua masalah: 1) mendapatkan informasi yang diperlukan dari argumen makro, 2) menghasilkan pohon yang mewakili kode yang Anda perlukan.

Di Scala 2.10 hal ini dilakukan dengan API refleksi. Ikuti Apakah ada tutorial tentang Scala 2.10 API refleksi belum? untuk melihat dokumentasi apa yang tersedia untuk itu.

import scala.reflect.macros.Context
import language.experimental.macros

trait Domain
case class Country(id: String, name: String) extends Domain
case class Town(id: String, longitude: Double, latitude: Double) extends Domain

object Macros {
  def logDomain(domain: Domain): Unit = macro logDomainMacroImpl

  def logDomainMacroImpl(c: Context)(domain: c.Expr[Domain]): c.Expr[Unit] = {
    import c.universe._

    // problem 1: getting the list of all declared vals and their types
    //   * declarations return declared, but not inherited members
    //   * collect filters out non-methods
    //   * isCaseAccessor only leaves accessors of case class vals
    //   * typeSignature is how you get types of members
    //     (for generic members you might need to use typeSignatureIn)
    val vals = typeOf[Country].declarations.toList.collect{ case sym if sym.isMethod => sym.asMethod }.filter(_.isCaseAccessor)
    val types = vals map (_.typeSignature)

    // problem 2: generating the code which would print:
    // id (String) : CH
    // name (String) : Switzerland
    //
    // usually reify is of limited usefulness
    // (see https://stackoverflow.com/questions/13795490/how-to-use-type-calculated-in-scala-macro-in-a-reify-clause)
    // but here it's perfectly suitable
    // a subtle detail: `domain` will be possibly used multiple times
    // therefore we need to duplicate it
    val stmts = vals.map(v => c.universe.reify(println(
      c.literal(v.name.toString).splice +
      "(" + c.literal(v.returnType.toString).splice + ")" +
      " : " + c.Expr[Any](Select(domain.tree.duplicate, v)).splice)).tree)

    c.Expr[Unit](Block(stmts, Literal(Constant(()))))
  }
}
person Eugene Burmako    schedule 14.01.2013
comment
Terima kasih banyak atas jawaban terperinci ini. Saya semakin menyukai Scala dan dukungan Refleksi dan Makro yang baru sangat bagus. Saya akan bermain-main lebih banyak dengannya dan mencoba membuat beberapa kode yang membuat instance objek berbeda menggunakan nilai yang diekstraksi dari case vals. - person MontChanais; 15.01.2013