XSLTがちょっと特殊な言語としていわゆるC++やJavaなどを使う一般(?)のプログラマーから異端視されるのにはそれなりの理由があります.一例が変数です.グローバル変数は一度初期化したらあとで値を代入することはできません.テンプレートのローカル変数も同じです.でもこれはXSLTのポリシーから決められている仕様です.
"XSLT is a language without assignment statements, and although its syntax is very different from these languages, its philosophy is based on the concepts of functional programming."
(XSLT Porgrammer's reference 2nd Editionより抜粋:このあとなぜassigment statement:代入がなぜいけないのか?なぜXSLTで使えないのか?続いています.興味のある方は原書をお読みください.)
例えば今お客様からXMLでオーサリングしたマニュアルのインスタンスからメッセージ部分をテキスト出力して、それを実際のプログラムのメッセージファイルとして使いたいという要望が寄せられています.これをXSLTで実現しようと思ったのですがすっかりハマッテしまいました.
例えば
<mes><p>One of the <b>CPU</b> cards stopped, or is <b>abnormal</b>.</p></mes>
なんていうメッセージがあったとします.テキスト出力だけだったら
<xsl:template match="mes">
<xsl:apply-templates/>
</xsl:template>
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="p">
<xsl:apply-templates/>
</xsl:template>
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="b">
<xsl:apply-templates/>
</xsl:template>
<xsl:apply-templates/>
</xsl:template>
で済んでしまうじゃないかといわれるかもしれません.確かにそのとおりなのですが、ここに「テキストは80桁に収める」という条件が加わると事態は一変します.文字を出力する都度、カラム位置を何らかの変数かパラメータに保持して処理を進めなければなりません.80カラムに到達したら改行を入れます.そこでカラム位置は0に戻ります.
これを正統なXSLTでやろうとすると、xsl:paramにカラム位置を入れて、次々に隣の位置の要素(またはテキストノード)に制御を移して行かねばなりません.
これは口で言うのは簡単ですが作ってみると極めて大変です.例えばテキストノードでは概念的に次のような処理を行わねばなりません.
<xsl:template match="text()">
<!-- テキストをxsl:value-ofで出力
カラム位置を更新する.-->
<!-- このノードの上位でfollowing-sibling::node()
のあるものを探す.見つかったらそのnode()に
カラム位置をパラメータとして、
xsl:apply-templatesする.-->
</xsl:template>
<!-- テキストをxsl:value-ofで出力
カラム位置を更新する.-->
<!-- このノードの上位でfollowing-sibling::node()
のあるものを探す.見つかったらそのnode()に
カラム位置をパラメータとして、
xsl:apply-templatesする.-->
</xsl:template>
ところがこれでも目的は達成されません.カラム位置はずっとxsl:paramで渡し続けるだけではダメで、例えば
<p>XSLT is a language without assignment statements</p>
<p>So what's wrong with assignment statements?</p>
<p>So what's wrong with assignment statements?</p>
とあったら、<p>から<p>へ移る際に改行をいれてカラム位置も1に戻してやらねばなりません.こういう処理は実は大変な負担です.がんばって作れと言われれば作れるでしょうが、たぶんメンテナンスは大変でしょう.正統なXSLTの規則にのっとって作ろうと思うと逆にスタイルシートの構造は奇形になってしまうのです.
そこで考え方を変えます.この条件で必要なのは「更新可能なカラム位置を保持するグローバル変数」です.これさえあれば、上記のようなtail recursionをする必要はまったくありません.最初に書いたテンプレートの雛形で出来ます.
Saxonには独自拡張で代入可能な変数があり、次のように宣言を書きます.
<xsl:variable name="columnCount" as="xs:integer"
saxon:assignable="yes" select="0"
xmlns:saxon="http://saxon.sf.net/"/>
saxon:assignable="yes" select="0"
xmlns:saxon="http://saxon.sf.net/"/>
代入はsaxon:assignを使います.例えばカラムカウントを1個増やす場合は、
<saxon:assign name="columnCount" select="$columnCount + 1"/>
です.改行を出力したときは0に戻します.
<saxon:assign name="columnCount" select="0"/>