XSLTのfor-eachはループしない(だから便利)

2021/05/07に公開

xsl:for-eachはループする?

XSLTのxsl:for-eachは「ループ」だと思ってないでしょうか?

おそらくC等のforに引きずられた理解なのでしょう。次のXSLTとCのコードを、ほぼ等価のように考えている人が多いようです。

XSLT

<xsl:for-each select="1 to 5">
  <xsl:variable name="i" select="." />
  <xsl:value-of select="$i " />
</xsl:for-each>

C

for (i = 1; i <= 5; i++) {
  printf("%d ", i);
}

「上から下に処理して、また上に戻る(ループする)」というイメージですね。次の図のような。

Loop

ループしているようにも見える

たしかに、xsl:for-eachはループしているようにも見えます。

たとえば次のサンプルXSLTを実行すると、1秒間隔で$i=1$i=5の順にメッセージが出力されます。

xsl:for-eachの例(要Saxon-EE)
<xsl:stylesheet exclude-result-prefixes="#all"
                extension-element-prefixes="saxon"
                version="3.0"
                xmlns:saxon="http://saxon.sf.net/"
                xmlns:xs="http://www.w3.org/2001/XMLSchema"
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

  <xsl:output indent="yes"
              omit-xml-declaration="true" />

  <xsl:param name="THREADS"
             as="xs:positiveInteger"
             select="xs:positiveInteger(1)" />

  <xsl:template name="xsl:initial-template"
                as="element(out)">
    <out>
      <xsl:for-each select="1 to 5"
                    saxon:threads="{$THREADS}">
        <xsl:variable name="i"
                      as="xs:integer"
                      select="." />
        <xsl:message select="'$i=' || $i"
                     saxon:time="yes" />

        <!-- Sleep for a second -->
        <saxon:do action="Q{java:java.lang.Thread}sleep(1000)" />

        <xsl:sequence select="$i" />
      </xsl:for-each>
    </out>
  </xsl:template>

</xsl:stylesheet>

Running single-threaded xsl:for-each

そうした挙動からも、xsl:for-eachが次のように「ループ」するものと感じられるのでしょう。

  1. $i=1を1秒間処理して
  2. 上に戻って
  3. $i=2を1秒間処理して
  4. 上に戻って
  5. $i=5が終わるまでループ…全体の処理に約5秒かかる

xsl:for-eachはループしない

実際は、xsl:for-eachループしません

xsl:for-eachは「ループ」ではなく、次のように「処理を割り当てる」命令です。

  • @selectで指定したアイテム一つ一つに、同じ処理を割り当てる
  • 各アイテムに割り当てた処理は、他のアイテムとは関係なく、独立して実行する
  • 処理の実行結果は、アイテムの順に出力する

イメージとしてはこう。

Parallel

ループしないからこそ便利

xsl:for-eachがループしないのは、欠点ではなく長所です。

  • 各アイテムの処理には順序関係がないので、並列処理できる
  • それでいて、処理結果はアイテム順になる
  • 各アイテムの処理は独立しているので、基本的には排他制御が不要

たとえば、先ほどと同じサンプルXSLTを、$THREADSパラメーターに5をセットして実行してみます。

Running multi-threaded xsl:for-each

  • $i=1$i=5は、並列に処理される
  • 並列処理なので、メッセージの出力順はタイミングによってランダム
  • 並列処理のおかげで、全体の処理は5秒ではなく約1秒で終わる
  • それでいて、処理の結果はちゃんと$i=1$i=5の順になる(<out>1 2 3 4 5</out>

特に、I/O待ちが長い処理など複数実行するときは、xsl:for-eachでこうした並列化をしなければ、やってられません。

ループしたければxsl:iterate

このように、xsl:for-eachがループしないのは長所なんですが、あたかも短所のように語られることがあります。たとえば次のような。

  • xsl:for-eachでは、ループしながら変数を順に更新できない
  • xsl:for-eachでは、ループの途中で条件によってbreakすることができない

これらは、どれもxsl:for-eachを「ループ」と誤解しているにすぎません。ループしないので、「順」も「途中」も意味をなしません。

ループしたいときは、xsl:for-eachではなく、xsl:iterateの出番です。xsl:iterateでは、変数を順に更新したり、途中でbreakすることもできます。

Discussion