LoginSignup
11
12

More than 5 years have passed since last update.

case classからXMLを生成するScalaのマクロを作る

Last updated at Posted at 2016-01-12

はじめに

Play Frameworkにはcase classからJSONへ変換するためのインスタンスを自動で作るマクロがある。一方でなんらかの型をXMLへ変換する機能はない。そこでこの記事では、まずPlayがどのようにcase classをJSONへ変換しているのかを軽く解説してそれのXML版を作り、最後にScalaのマクロを用いてcase classからXMLを生成するためのインスタンスを自動生成する。

Playが型AをJSONへ変換するとき

Playが型AをJSONへ変換する際にはWrites[A]という型クラスのインスタンスを要求する。このWritesは次のような型クラスになっている。

https://github.com/playframework/playframework/blob/master/framework/src/play-json/src/main/scala/play/api/libs/json/Writes.scala

trait Writes[-A] {
  /**
   * Convert the object into a JsValue
   */
  def writes(o: A): JsValue

  /**
   * Transforms the resulting [[JsValue]] using transformer function
   */
  def transform(transformer: JsValue => JsValue): Writes[A] = Writes[A] { a => transformer(this.writes(a)) }

  /**
   * Transforms resulting [[JsValue]] using Writes[JsValue]
   */
  def transform(transformer: Writes[JsValue]): Writes[A] = Writes[A] { a => transformer.writes(this.writes(a)) }
}

transformwritesから作られているので、ひとまずここでは無視すると、型クラスWrites[A]のインスタンスは次のようなメソッドを持つ。

def writes(o: A): JsValue

このwritesは引数として型Aの値oを取り、JSONを表す型JsValueを返しているので、writesは型AからJSONへ変換する関数であるといえる。例えば、Playでよく使うJson.toJsonという関数は次のようになる。

https://github.com/playframework/playframework/blob/master/framework/src/play-json/src/main/scala/play/api/libs/json/Json.scala#L118

def toJson[T](o: T)(implicit tjs: Writes[T]): JsValue = tjs.writes(o)

このように、implicitパラメーターでWrites[T]のインスタンスを受け取って、それを用いてJSONへの変換を行う。
これをそっくりXMLへ移植すればよい。

AをXMLへ変換する型クラスXmlWrites[A]

JSONへ変換する際に用いた型クラスWrites[A]とほとんど同じXmlWrites[A]を次のように定義する。ScalaにはXMLを表す型があらかじめ用意されているので、それをそのまま用いればよい。

XmlWrites.scala
trait XmlWrites[-A] {
  def writes(o: A): scala.xml.NodeSeq
}

そして、Json.toJsonのような関数を定義する。

Xml.scala
object Xml {
  def toXml[W](o: W)(implicit X: XmlWrites[W]): scala.xml.NodeSeq = X.writes(o)
}

これは次のように使うことができる。

case class Test(a: String, b: Int)

implicit val testWrites: XmlWrites[Test] = new Writes[Test] {
  def writes(o: Test): NodeSeq =
    <a>
      {o.a}
    </a>
    <b>
      {o.b.toString}
    </b>
}

Xml.toXml(Test("hoge", 123))

やや冗長だが、これで一応動作はする。

マクロを用いたインスタンスの自動生成

PlayのWritesにはマクロを使ってcase classのWritesインスタンスを自動生成することができる。例えば先ほどのTestWritesは次のように書ける。

case class Test(a: String, b: Int)

implicit val testJsonWrites: Writes[Test] = Json.writes[Test]

Json.toJson(Test("hoge", 123))

このように一行でインスタンスを生成できる。Playの実装を調べるとScalaのマクロを使ってインスタンスを生成していたので、こちらの記事を参考にしつつ、case classのインスタンスを自動生成するマクロを次のように実装する。

XmlWrites.scala
object Xml {
  def toXml[W](o: W)(implicit X: XmlWrites[W]): scala.xml.NodeSeq = X.writes(o)

  def xmlWrites[A]: XmlWrites[A] = macro XmlMacroImpl.impl[A]
}
XmlMacroImpl.scala
// macroでcase classのXmlWritesインスタンスを自動導出する
// http://matsu-chara.hatenablog.com/entry/2015/06/21/110000
object XmlMacroImpl {
  def impl[A: c.WeakTypeTag](c: blackbox.Context): c.Expr[XmlWrites[A]] ={
    import c.universe._

    // case classのクラス名
    val caseClassSym: c.universe.Symbol = c.weakTypeOf[A].typeSymbol
    if (!caseClassSym.isClass || !caseClassSym.asClass.isCaseClass) c.abort(c.enclosingPosition, s"$caseClassSym is not a case class")

    // 各フィールドのシンボル
    val syms: List[TermSymbol] = caseClassSym.typeSignature.decls.toList.collect { case x: TermSymbol if x.isVal && x.isCaseAccessor => x }

    val xmlTreeList: List[Tree] = syms.map { e =>
      val name = e.name.toString.trim
      val elemTree = q"_root_.scala.xml.Elem(null, $name, _root_.scala.xml.Null, _root_.scala.xml.TopScope, false, implicitly[XmlWrites[${e.typeSignature}]].writes(o.${TermName(name)}): _*)"
      q"$$removeEmptyNode($elemTree)"
    }

    val xmlTree: Tree = xmlTreeList.tail.foldLeft(xmlTreeList.head)((x, y) =>
      q"$x ++ $y"
    )

    val finalTree: Tree =
      q"""
        def $$removeEmptyNode(node: _root_.scala.xml.Elem): _root_.scala.xml.NodeSeq = node match {
          case _root_.scala.xml.Elem(_, _, _, _) => _root_.scala.xml.NodeSeq.Empty
          case _ => node
        }

        new XmlWrites[$caseClassSym] {
          def writes(o: $caseClassSym): scala.xml.NodeSeq = $xmlTree
        }
      """

    c.Expr[XmlWrites[A]](finalTree)
  }
}

case classかどうかを判定して、case classならばフィールドと型情報を取得し、それを使ってコードを組み立てていく。また、$$removeEmptyNode関数は<a>NodeSeq.Empty</a>のようなXMLノードが発生した際に、その要素を消去する関数である。

あとは、よく使いそうなインスタンスをXmlWritesのコンパニオンオブジェクトに用意しておく。

XmlWrites.scala
object XmlWrites {
  implicit val stringWrites: XmlWrites[String] = new XmlWrites[String] {
    def writes(o: String): NodeSeq = Text(o)
  }

  implicit val intWrites: XmlWrites[Int] = new XmlWrites[Int] {
    def writes(o: Int): NodeSeq = Text(o.toString)
  }

  implicit val floatWrites: XmlWrites[Float] = new XmlWrites[Float] {
    def writes(o: Float): NodeSeq = Text(o.toString)
  }

  implicit val doubleWrites: XmlWrites[Double] = new XmlWrites[Double] {
    def writes(o: Double): NodeSeq = Text(o.toString)
  }

  implicit val booleanWrites: XmlWrites[Boolean] = new XmlWrites[Boolean] {
    def writes(o: Boolean): NodeSeq = Text(o.toString)
  }

  implicit def listWrites[A](implicit W: XmlWrites[A]): XmlWrites[List[A]] = new XmlWrites[List[A]] {
    def writes(o: List[A]): NodeSeq = NodeSeq.fromSeq(o.flatMap(W.writes(_).toSeq))
  }

  implicit def optionWrites[A](implicit W: XmlWrites[A]): XmlWrites[Option[A]] = new XmlWrites[Option[A]] {
    def writes(o: Option[A]): NodeSeq = o match {
      case Some(a) => W.writes(a)
      case None => NodeSeq.Empty
    }
  }
}

これらを使えば、先ほどのcase classTestXmlWrites[Test]は次のように生成できる。

case class Test(a: String, b: Int)

implicit val testJsonWrites: XmlWrites[Test] = Xml.xmlWrites[Test]

Xml.toXml(Test("hoge", 123))

まとめ

この記事ではcase classからXMLへ変換する型クラスを用いて、XMLを生成し、かつインスタンスをマクロで生成するということを行った。はじめてのマクロにはいろいろ苦労したが、実用的なものが少ない行数で実装できてよかった。

11
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
12