<choice> の限定

XML ドキュメントを利用しやすいネイティブなオブジェクトに変換(データ・バインディング)する,そしてそのオブジェクトを XML ドキュメントに逆変換(ラウンド・トリップ)できる能力を保持するという二つの要請から rt.DataRecord は 生まれました.しかし,この二つの要件は時として統一的な行動を取りません.例えば,逆変換のことだけを考えてしまうと,XML を書き出すために scala.xml.Node を保持すれば解決してしまいますが,データ・バインディングという立場から見ると便利なものではありません.

scalaxb の当初の目標は XML Schema の表現空間をカバーして,かつラウンド・トリップを実装することでした.この目標はほぼ達成されたので,最近の更新では逆変換性は保持しつつ,生成されたコードの利便性を高めることを目指してきました.ラウンド・トリップ側に傾き過ぎたものの一つに <choice> のコード生成があります.まずはこれまでの <choice> のコード生成の歴史を振り返り,最後に新しい案を提起したいと思います.

第一の試み

初めに <choice> を実装したときは choice を含む複合型が選択肢を参照できるように choice 内の全ての選択肢を表す trait を生成しようと試み,このようなものになるのではと想像しました:

case class Order(arg1: OrderOption, items: Seq[Item])
trait OrderOption
case class Address(name: String, street: String, city: String) extends OrderOption
case class InternalAddress(building: String, room: Int) extends OrderOption

この方法にはいくつかの問題があります.choice のスキーマが以下のようなものだと仮定します:

<xs:choice>
  <xs:element name="groundShipping" type="Address" />
  <xs:element name="twoDayShipping" type="Address" />
  <xs:element name="oneDayShipping" type="Address" />
  <xs:element name="internalShipping" type="InternalAddress" />
</xs:choice>

問題は arg1 を見ても,Address を囲んでいた要素の要素名が分からないということです.この例からも分かるように,XML ドキュメント内の要素はその複合型を表すデータ構造に比べ要素名という情報を一つ多く持ちます.これを解決するのに,少なくとも二つの方法があります.

第二の試み

まずは各々の要素に対して子クラスを生成するという方法があります:

case class GroundShipping(name: String, street: String, city: String) extends OrderOption
case class TwoDayShipping(name: String, street: String, city: String) extends OrderOption
case class OneDayShipping(name: String, street: String, city: String) extends OrderOption
case class InternalShipping(building: String, room: Int) extends OrderOption

Scala は case class を継承して別の case class を作ることを許さないため,データ構造を繰り返す必要があります.また,XML の名前空間の仕組みとも相まってあまり嬉しくない結果となりました.XML においては子要素は別のトップレベル要素の子要素と同じ名前を使うことが許させています.当然データ構造が食い違う可能性も想定されます.この名前衝突を避けるためには親クラスの名前を前置するなど工夫が必要でしょう.

第二の試みの第二の方法

次に,要素名を別に保存するという方法があります.これが rt.DataRecord の骨子です.コードは以下のようになります:

case class Order(arg1: rt.DataRecord[OrderOption], items: Seq[Item])
trait OrderOption
case class Address(name: String, street: String, city: String) extends OrderOption
case class InternalAddress(building: String, room: Int) extends OrderOption

arg1 は以下のようなものを保持します

rt.DataRecord(None, Some("twoDayShipping"), Address("foo", "1537 Paper Street", "Wilmington"))

別の問題に先に直面したため,上のような発展は実はありませんでした.次の choice のスキーマを見てください:

<xs:choice>
  <xs:element name="groundShipping" type="Address" />
  <xs:element name="twoDayShipping" type="Address" />
  <xs:element name="oneDayShipping" type="Address" />
  <xs:element name="internalShipping" type="xs:string" />
</xs:choice>

そうです.もし,選択肢の一つが xs:string のような単純型だった場合はどうすればいいでしょう?

第三の試み

単純型と <any> に直面して <choice> に関しては型の安全性を諦めることにしました.全ての choice は要素名とどんな値も保持できる rt.DataRecord[Any] を使って表すことができます.

case class Order(arg1: rt.DataRecord[Any], items: Seq[Item])
trait OrderOption
case class Address(name: String, street: String, city: String) extends OrderOption

これによってラウンド・トリップ問題は解決しましたが,型安全性が無くなったことが気持ちの良いことではありませんでした.ここで <choice> の限定という考えが発生します.最良の解決策は与えられた <choice> に関して選択肢の最小公スーパークラスを計算することです.勿論,言うは易く行なうは難しです.scalaxb というコンテクストにおいては,本当の型ではなく机上の Scala コードを扱っており,また,評価が必要な型そのものがまだ存在すらしていないという状況なのです.XML Schema は複合型を他の複合型から派生することができるので,生成された型の間においてさえ出自の心配をしなくてはいけません.例えば,USAddressUKAddress という choice があったとすると,最小公スーパークラスは trait Addressable となります.

第四の試み

場合によって rt.DataRecord をより狭いものに限定できる特殊ケースがあることにも気づきました.第一は選択肢の全てが同じ型である場合です.

<xs:choice>
  <xs:element name="groundShipping" type="xs:string" />
  <xs:element name="internalShipping" type="xs:string" />
</xs:choice>

選択肢の両方ともが String であれば,選択肢のどちらであっても String であると言うことができます.

case class Order(arg1: rt.DataRecord[String], items: Seq[Item])

スーパークラスを推論できるもう一つのケースは選択肢が名前空間内の複合型,シーケンス,もしくは choice のみで構成されている場合で,これらは全て選択肢の trait を継承します.

<xs:choice>
  <xs:element name="groundShipping" type="Address" />
  <xs:element name="twoDayShipping" type="Address" />
  <xs:element name="oneDayShipping" type="Address" />
  <xs:element name="internalShipping" type="InternalAddress" />
</xs:choice>

上の <choice> は以下のコードを生成します:

case class Order(arg1: rt.DataRecord[OrderOption], items: Seq[Item])
trait OrderOption
case class Address(name: String, street: String, city: String) extends OrderOption
case class InternalAddress(building: String, room: Int) extends OrderOption

これは全ての choice を rt.DataRecord[Any] に広げる前の同じものです.この場合は選択肢の全てが名前空間内の複合型で OrderOption を継承するので,選択肢の出自を調べること無く OrderOption に限定できます.仮に AddressInternalAddress が共に trait Addressable のようなものを継承していれば,それを選べれば最も限定された型となりますが,Any よりはいいでしょう.