混在内容,再び

混在内容のサポートを前に始めましたが,おさらいをしておくと,<xs:complexType mixed="true"> の時,XHTML のように一つの要素内にテキストノードと子要素が並列して混在することができようになります.実装した後で,生成される case class が DRY じゃないのが結構気になっていました.

例えば,

<xs:element name="mixedTest">
  <xs:complexType mixed="true">
    <xs:choice maxOccurs="unbounded">
      <xs:element name="billTo" type="Address"/>
      <xs:any namespace="##other" processContents="lax" />
    </xs:choice>
  </xs:complexType>
</xs:element>

case class Element3(arg1: Seq[rt.DataRecord[Any]], mixed: Seq[rt.DataRecord[Any]]) 生成していましたが,最初のパラメータの arg1 は子要素を収容し,次の mixed は子要素とテキスト・ノードの両方を収容します.case class が XML に往復して帰るためにはテキスト・ノードと子要素の両方を順番通りに mixed に保存する必要があります.しかし,パーシングは子要素のみに行われていたため,arg1のみに Address オブジェクトは格納され,mixed にはパース前の scala.xml.Elem オブジェクトが入っています.お行儀が悪いですね.
以前のパーシングがあまり洗練されたものではなかったのが,一つの理由になっています.

いよいよマトモなパーサを使ったパーシングを始めたので,この問題を再検討することにしました.順番を保存する必要性は変わらないので mixed は今後も必要になります.しかし,もしパーシングがしっかりすれば arg1 は無くすことができるのではないでしょうか.そのために複合型が混在であるときはテキスト・ノードも文法の一部とみなすようにパーシングを変更しました.上記の複合型より生成されたコードです:

case class MixedTest(mixed: Seq[rt.DataRecord[Any]])
 
object MixedTest extends rt.ElemNameParser[MixedTest] {
  val targetNamespace: Option[String] = Some("http://www.example.com/mixed")
  def isMixed: Boolean = true
 
  def parser(node: scala.xml.Node): Parser[MixedTest] =
    optTextRecord ~ rep((((((rt.ElemName(targetNamespace, "billTo")) ^^ 
      (x => rt.DataRecord(x.namespace, Some(x.name), Address.fromXML(x.node)))) ~ 
    optTextRecord) ^^ 
      { case p1 ~ p2 => Seq.concat(Seq(p1),
      p2.toList) })) | 
    (((any ^^ (x => rt.DataRecord(x.namespace, Some(x.name), x.node))) ~ 
      optTextRecord) ^^ 
      { case p1 ~ p2 => Seq.concat(Seq(p1), p2.toList) })) ~ optTextRecord ^^
        { case p1 ~ 
      p2 ~ 
      p3 => MixedTest(Seq.concat(p1.toList,
      p2.flatten,
      p3.toList)) }
 
  def toXML(__obj: MixedTest, __namespace: Option[String], __elementLabel: Option[String], __scope: scala.xml.NamespaceBinding): scala.xml.NodeSeq = {
    var attribute: scala.xml.MetaData  = scala.xml.Null
 
    scala.xml.Elem(rt.Helper.getPrefix(__namespace, __scope).orNull,
      __elementLabel getOrElse { error("missing element label.") },
      attribute, __scope,
      __obj.mixed.flatMap(x => rt.DataRecord.toXML(x, x.namespace, x.key, __scope).toSeq): _*)
  }
}

住所オブジェクトもちゃんとパースされて,しかも重複せずに保存されています.一見問題が解決したかに思われましたが,ラウンド・トリップにおいて新たな問題が発生しました.scala.xml.Elem を保持しないために,DataRecord.toXML が XML を生成できなくなってしまったのです.mixedIntString のような組み込み型 ,scala.xml.Elem のような XMLノード,そして Address のようなユーザ定義型を格納できるように rt.DataRecord[Any] と宣言されています.組み込み型や XMLノードのための XML生成の方法は事前に出荷できますが,ユーザ定義型もサポートする必要があります.型クラスで実装してみることにしました:

trait XMLWriter[A] {
  implicit val ev = this
  def toXML(__obj: A, __namespace: Option[String], __elementLabel: Option[String],
      __scope: NamespaceBinding): NodeSeq
}

全てのコンパニオン・オブジェクトはすでに toXML を実装しているので,XMLWriter[A] を継承するだけです.一度 DataRecord[Any] に格納してしまって Any になってしまったオブジェクトから XMLWriter[A] を抜き出す方法が分からなかったので,XMLWriter[A]DataRecord[Any] に保存することにしました.この方法の問題は DataRecord[A] における値の型の A によって決まりきった値のパラメータが余計に増えることです.String の場合は何らかの __StringXMLWriter になるし,Address の場合は常に Address となります.また,余計なパラメータはパターン・マッチの邪魔にもなります.これをどのように回避できるでしょうか.

まず,object DataRecord下に def dataRecord という名前でコンストラクタ・ヘルパを定義します.これは最初の三つのパラメータは明示的に取った後,XMLWriter[A] を context-bound 文法によって暗黙に取ります:

def dataRecord[A:XMLWriter](namespace: Option[String], key: Option[String], value: A): DataRecord[A] =
  DataRecord(namespace, key, value, implicitly[XMLWriter[A]])

この時点で,scalaxb で使われる組み込み型と XML ノードに関して暗黙の値を提供する必要があります:

object XMLWriter {
  implicit object __NodeXMLWriter extends XMLWriter[Node] {
    def toXML(__obj: Node, __namespace: Option[String], __elementLabel: Option[String],
      __scope: NamespaceBinding): NodeSeq = __obj
  }
 
  implicit object __StringXMLWriter extends XMLWriter[String] {
    def toXML(__obj: String, __namespace: Option[String], __elementLabel: Option[String],
        __scope: scala.xml.NamespaceBinding): scala.xml.NodeSeq =
      Helper.stringToXML(__obj, __namespace, __elementLabel, __scope) 
  }
...
}

Scala の規格で興味深いのは暗黙のパラメータを探す場所です.Programming in Scala p.440-441:

さらに,一つの例外を覗いて,暗黙の型変換は一つの識別子としてスコープに入っていなければいけません.

「単一識別子」ルールに一つだけ例外があります.コンパイラは暗黙の値の定義を元の型および期待されている対象の型のコンパニオン・オブジェクトの中も探します.

XMLWriter[A] の中に implicit val ev = this の定義を入れておいたのに気付いたでしょうか:

trait XMLWriter[A] {
  implicit val ev = this
  def toXML(__obj: A, __namespace: Option[String], __elementLabel: Option[String],
      __scope: NamespaceBinding): NodeSeq
}

AddressXMLWriter[Address] を継承するため,Address オブジェクトも暗黙の値となります.

この新しい def dataRecordDataRecord の生成の問題は解決しますが,パターン・マッチングに四つのパラメータがある問題が残っています.

パターン・マッチングはただ単に def unapply を適用しただけの話なので,古い DataRecord との互換性を保つために DataRecord を元からの三つの値だけの trait として再定義することができます.object DataRecord下に unapply を以下のように定義します:

def unapply[A](record: DataRecord[A]): Option[(Option[String], Option[String], A)] =
  Some(record.namespace, record.key, record.value)

パターン・マッチングを模倣することができたので,どうせならオブジェクト生成も模倣してしまいましょう.def dataRecord の代わりに def apply と定義することで DataRecord のコンストラクタを擬態することができます.XMLWriter[A] を含めた実際の値の保持は private な case class を object DataRecord 内に隠し持ちます:

object DataRecord {
  private case class DataWriter[+A](
    namespace: Option[String],
    key: Option[String],
    value: A,
    writer: XMLWriter[_]) extends DataRecord[A]
 
  def apply[A:XMLWriter](namespace: Option[String], key: Option[String], value: A): DataRecord[A] =
    DataWriter(namespace, key, value, implicitly[XMLWriter[A]])
 
  def apply[A:XMLWriter](value: A): DataRecord[A] =
    apply(None, None, value)
 
  def unapply[A](record: DataRecord[A]): Option[(Option[String], Option[String], A)] =
    Some(record.namespace, record.key, record.value)
 
  def toXML[A](__obj: DataRecord[A], __namespace: Option[String], __elementLabel: Option[String],
      __scope: scala.xml.NamespaceBinding): scala.xml.NodeSeq = __obj match {
    case w: DataWriter[_] => w.writer.asInstanceOf[XMLWriter[A]].toXML(__obj.value, __namespace, __elementLabel, __scope)
    case _ => error("unknown DataRecord.")
  }
}

これで後方互換性があり,かつ型独自の XMLアウトプットができる DataRecord ができました.