XSLT3.0への道(10) ストリーミング関連の関数

XSLT3.0ではストリーミング機能が導入されますがそれに関連する関数が追加されています.今回はこれを紹介したいと思います.これらの関数には、copy-of、snapshot、outermost、innermost、haschildrenがあります.機能を読んでみましたが、ちょっと今までの非ストリーミングモードのXSLTプロセッサを使ってきた感覚ではピンときません.
 
例えば、copy-of()は
 
copy-of() as node()
copy-of($node as node()?) as node()?
 
のように定義されます.copy-of()はcopy-of(.)と同じです.次のような例と説明があります.
 
The function returns a deep copy of the node supplied as the argument $node.(一部省略)The effect is the same as that of the xsl:copy-of instruction with copy-namespaces set to yes and validation set to preserve.
<xsl:stream href="employees.xml">
  <xsl:sequence select="employees/employee/copy-of()
                        [department='Marketing' and location='Dubai']"/>
</xsl:stream>
 
要するにcopy-of関数はは、xsl:copy-ofのdeep-copyと同じであるということです.しかしこれは次のような非ストリームモードの記述とどう違うのでしょうか?
 
<xsl:sequence select="doc('employees.xml')/employees/employee[department='Marketing' and location='Dubai']"/>
 
結果は同じなのではないかと思います.しかしこの例の説明には、このようにかかれています.
 
This example copies from the source document all employees who work in marketing and are based in Dubai. Because there are two accesses using the child axis, it is not possible to do this without buffering each employee in memory, which can be achieved using the copy-of function.
 
つまり、employeeの要素のchildに複数のアクセスがあるからストリームモードで処理する場合、employee要素を取得した時点で、下位のノードについてはバッファリングする必要があるということです.このバッファリングを指示するのがcopy-of関数ということなのです.
 
この話を理解するためには、ストリームモードの場合、XSLTプロセッサが入力ドキュメントをどのように読み取るかを知らなければなりません.非ストリームモードの場合、入力ドキュメントは全体がいったんメモリー上にに読み込まれます.あとはいかようにもXPathでアクセスが可能です.ストリームモードの場合、あるノードの読み込みは一回だけです.ノードは読み込まれた時点でどのように処理されるか(捨てられるか、保存されるか)が決められなければならないでしょう.
 
ストリームモードの入力ドキュメントの読み込みはSax(Simple API for XML)か、StAX(Streaming API for XMLhttp://itpro.nikkeibp.co.jp/article/COLUMN/20080404/298015/ に解説あり)で行われると思って間違いないでしょう.SAXのように、パーサからどんどんノードの読み込みが通知されるか、StAXのようにパーサーをコントロールして一つ一つノードを読み込んで行く感じです.もしそうであるならば、employeeの子要素departmentの値が"Marketing"であり、location要素の値が"Dubai"であることを判定してemployee以下の要素を出力するためには、子要素の「バッファリング」(いったんメモリー上にためておく)が不可欠です.
 
copy-of関数はこのバッファリングをストリームモードのXSLTプロセッサに指示するためにあると考えられます.これは逆にストリームモードで入力ドキュメントを処理する場合、プログラマーもその特性を知ってスタイルシートをプログラミングをしなければならないということになります.これは知っておかねばならない事柄です.
 
それでは残りの関数を紹介します.
 
■ snapshot()
次のように定義されます.
snapshot() as node()
snapshot($node as node()?) as node()?
 
snapshot()はsnapshot(.)と同じになります.snapshot(N)は、Nのancestorとcopy-of(N)のノード列を返します.例はつぎのようなものです.
 
<xsl:stream href="employees.xml">
  <xsl:for-each select="locations/location[@name='Dubai']
                          /employee/snapshot()[department='Marketing']">
    <employee>
      <location code="{../@code}"/>
      <salary value="{salary}"/>
    </employee>
  </xsl:for-each>
</xsl:stream>
 
この例では、employee/copy-of()ではダメなのでしょうか?この場合、<location code="{../@code}"/>でemployeeの親要素のlocationのname属性を参照しているので、copy-of()でなくsnapshot()が必要になるはずです.つまりemployeeのancestor軸は保存しておかねばなりません.もし<location code="{../@code}"/>がなければ、copy-of()でOKなはずです.
 
snapshot()もスタイルシートをパースする時、employeeのdescendantだけでなくancestorのノードを保存するように指示するものと考えられます.
 
■ outermost()
次のように定義されます.
outermost($nodes as node()*) as node()*
outermost()関数は、node()*をパラメータに取り、そのどれも他のノードがancestorにならない集合を返します.
 
例えばoutermost(//table)は、入れ子になっているtableを除いたtable要素の集合を返します.
outermost($node)は、$nodes[not(ancestor::node() intersect $nodes)] と等価です.
outermost()関数は、XPath式 .//section/head がストリーミングモードで使えない問題を回避するための一つの策として提示されています.
 
 
<section nr="1">
  <section nr="1.1">
    <head>section 1.1</head>
  </section>
  <section nr="1.2">
    <head>section 1.2</head>
  </section>
  <head>section 1</head>
</section>
 
のようなデータに対して、<xsl:value-of select="//section/head"/>とやれば、ドキュメントオーダーで"section 1.1 section 1.2 section 1"と出力できるのですが、
 
<xsl:for-each select="//section">
  <xsl:value-of select="head"/>
</xsl:for-each>
 
というようなテンプレートだと、"section 1 section 1.1 section 1.2"と出力しなければなりません.これはストリーミングを実装する立場からすると、単純なドキュメントオーダーのノード処理では実現できないです.なんらかのバッファリングか代替手段が必要です.(しかしこの例だとoutermost(//section)としてしまうと、後者は"section 1"しか出力されないですね.入れ子がなければまったく問題ないはずですが...)
 
■ innermost()
次のように定義されます.
innermost($nodes as node()*) as node()*
innermost関数は、node()*をパラメータに取り、そのどれも他のノードのancestorにならない集合を返します.例えばinnermost(//table)は、内部にtable要素を含まないtable要素の集合を返します.
innermost($nodes)は、$nodes except $nodes/ancestor::node() と等価とされています.
innermost関数はストリーミングモードとは関係しません、outermostと対極にある関数として定義されます.

■ has-children()
読んで字のごとくです.カレントコンテキストにchildがあればtrueを返します.
has-children() as xs:boolean
で定義されます.
用途はパターン(match="~")の記述での制約条件での使用.まだ検討中のようです.
 
さて、ストリーミングに関係する関数がわかりづらいのは、ストリーミングをサポートするXSLTプロセッサをどのように設計するかという問題ときっても切り離せない関係にあるからです.ストリーミングに関する仕様案は、ドラフトの18章に詳述されていますが、どちらかというとXSLTプロセッサを使う立場からはわかりづらい.やはりXSLTプロセッサを実装する立場の方がしみじみと読むべき内容でしょう.(もちろん使う立場の人間が読んでもよいのですがなかなか大変です.)