อาร์กิวเมนต์วิปัสสนาส่งผ่านไปยังมาโคร Scala

ฉันต้องการเขียนโปรแกรมแมโคร Scala ที่ใช้อินสแตนซ์ของคลาสเคสเป็นอาร์กิวเมนต์ ออบเจ็กต์ทั้งหมดที่สามารถส่งผ่านไปยังมาโครจะต้องใช้ลักษณะเครื่องหมายเฉพาะ

ตัวอย่างต่อไปนี้แสดงคุณลักษณะของเครื่องหมายและคลาสกรณีตัวอย่างสองคลาสที่นำไปใช้:

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

ตอนนี้ ฉันต้องการเขียนโค้ดต่อไปนี้โดยใช้มาโครเพื่อหลีกเลี่ยงความหนักหน่วงของการสะท้อนรันไทม์และความไม่ปลอดภัยของเธรด:

object Test extends App {

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

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

มาโคร logDomain ถูกนำไปใช้ในโครงการอื่นและมีลักษณะคล้ายกับ:

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

วัตถุประสงค์ของมาโครควรเพื่อสร้างโค้ดที่ - ณ รันไทม์ - ส่งออกค่าทั้งหมด (id และ name) ของออบเจ็กต์ที่กำหนดและพิมพ์ออกมาดังที่แสดงถัดไป:

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

เพื่อให้บรรลุเป้าหมายนี้ ฉันจะต้องตรวจสอบอาร์กิวเมนต์ประเภทที่ส่งแบบไดนามิกและกำหนดสมาชิกของมัน (vals) จากนั้นฉันจะต้องสร้าง AST แทนโค้ดที่สร้างเอาต์พุตบันทึก แมโครควรทำงานได้ไม่ว่าวัตถุเฉพาะที่ใช้ลักษณะเครื่องหมาย "โดเมน" จะถูกส่งผ่านไปยังมาโครหรือไม่

ณ จุดนี้ฉันหลงทาง ฉันจะขอบคุณถ้ามีคนให้จุดเริ่มต้นหรือชี้ให้ฉันดูเอกสารบางอย่าง ฉันค่อนข้างใหม่กับ Scala และไม่พบวิธีแก้ปัญหาในเอกสาร Scala API หรือคู่มือ Macro


person MontChanais    schedule 14.01.2013    source แหล่งที่มา


คำตอบ (2)


การแสดงรายการ accessors ของคลาสเคสเป็นการดำเนินการทั่วไปเมื่อคุณทำงานกับมาโคร ซึ่งฉันมักจะคงวิธีการไว้ดังนี้:

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
}

นี่จะให้สัญลักษณ์เมธอดตัวเข้าถึงคลาสเคสทั้งหมดสำหรับ A ถ้ามี โปรดทราบว่าฉันใช้ API การสะท้อนทั่วไปที่นี่ ยังไม่จำเป็นต้องสร้างมาโครนี้โดยเฉพาะ

เราสามารถรวมวิธีนี้เข้ากับสิ่งอำนวยความสะดวกอื่น ๆ ได้:

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

และตอนนี้เราสามารถเขียนโค้ดแมโครจริงได้ค่อนข้างกระชับ:

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

โปรดทราบว่าเรายังคงจำเป็นต้องติดตามประเภทคลาสเคสเฉพาะที่เรากำลังเผชิญอยู่ แต่การอนุมานประเภทจะดูแลเรื่องนั้นที่ไซต์การโทร—เราไม่จำเป็นต้องระบุพารามิเตอร์ประเภทอย่างชัดเจน

ตอนนี้เราสามารถเปิด REPL แล้ววางคำจำกัดความคลาสเคสของคุณ จากนั้นเขียนดังต่อไปนี้:

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

ตามที่ต้องการ

person Travis Brown    schedule 14.01.2013
comment
คุณทุบตีฉัน 5 นาที! :) - person Eugene Burmako; 15.01.2013
comment
ขอบคุณมากสำหรับคำตอบโดยละเอียด! ตัวอย่างของคุณทำในสิ่งที่ฉันกำลังมองหาอย่างแน่นอน ฉันจะลองดูคืนนี้ ข้อดีของโซลูชันของคุณคือการใช้พารามิเตอร์ type และ WeakTypeTag ซึ่งทำให้โค้ดเป็นแบบทั่วไปโดยสมบูรณ์ ควรใช้ได้กับคลาสเคสใดๆ ที่ใช้โดเมน - person MontChanais; 15.01.2013

จากสิ่งที่ฉันเห็น คุณต้องแก้ไขปัญหาสองประการ: 1) รับข้อมูลที่จำเป็นจากอาร์กิวเมนต์แมโคร 2) สร้างแผนผังที่แสดงถึงโค้ดที่คุณต้องการ

ใน Scala 2.10 สิ่งเหล่านี้เสร็จสิ้นด้วย Reflection API ติดตาม มีบทช่วยสอนเกี่ยวกับ Scala 2.10 หรือไม่ การสะท้อน API หรือยัง เพื่อดูว่ามีเอกสารอะไรบ้าง

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
ขอบคุณมากสำหรับคำตอบโดยละเอียดนี้ ฉันชอบ Scala มากขึ้นเรื่อยๆ และการรองรับ Reflection และ Macro ใหม่ก็ยอดเยี่ยมมาก ฉันจะเล่นกับมันให้มากขึ้น และพยายามสร้างโค้ดที่สร้างอินสแตนซ์ให้กับอ็อบเจ็กต์อื่นโดยใช้ค่าที่แยกออกมาของ case vals - person MontChanais; 15.01.2013