Dynamic variables, we finally meet!
I was reading in Chapter 6 of Practical Common Lisp about dynamic (a.k.a. “special”) variables and it hit me that this is something that I have longed for in XSLT.
I generally favor using template rules, and hence xsl:apply-templates
, over xsl:for-each
whenever possible. But sometimes it’s a pain—particularly when you have to pass parameters around.
An example using <xsl:for-each>
Take this contrived piece of code, for example:
<xsl:template match="foo"> <xsl:variable name="x" select="string(@x)"/> <xsl:variable name="y" select="string(@y)"/> <xsl:for-each select="document('some-doc.xml')//bar"> <my-element x="{$x}"> <xsl:for-each select="document('another-doc.xml')//bat"> <another-element y="{$y}"/> </xsl:for-each> </my-element> </xsl:for-each> </xsl:template>
Okay, not so bad. But it’s inflexible.
The template rule version
If I wanted to convert this to a solution using template rules, I’d end up with this monstrosity:
<xsl:template match="foo"> <xsl:variable name="x" select="string(@x)"/> <xsl:variable name="y" select="string(@y)"/> <xsl:apply-templates select="document('some-doc.xml')//bar"> <xsl:with-param name="x" select="$x"/> <xsl:with-param name="y" select="$y"/> </xsl:apply-templates> </xsl:template> <xsl:template match="bar"> <xsl:param name="x"/> <xsl:param name="y"/> <my-element x="{$x}"> <xsl:apply-templates select="document('another-doc.xml')//bat"> <xsl:with-param name="x" select="$x"/> <xsl:with-param name="y" select="$y"/> </xsl:apply-templates> </my-element> </xsl:template> <xsl:template match="bat"> <xsl:param name="x"/> <xsl:param name="y"/> <another-element y="{$y}"/> </xsl:template>
UGH! It’s bad enough to have to pass parameters around. Add in XSLT’s syntax for parameter-passing and it becomes excruciating. So… in this case, the solution using <xsl:for-each>
has to be preferred, because the variables stay within lexical scope and require no parameter-passing. The only reason to go with template rules is when you really do need the flexibility afforded by the latter solution (e.g. enabling a template rule to be overridden, etc.).
Using a global variable is often the solution (and a perfectly acceptable one), but in this case, the values may vary for each <foo>
element that is processed, and I don’t know upfront how many <foo>
elements there will be. So that won’t work here.
Enter dynamic variables. As explained in the aforementioned chapter on Lisp, a dynamic variable is a global variable that can have multiple bindings. You have the option of shadowing it in a given lexical context and—here’s the kicker—for the full extent of any calls you make to other functions. For XSLT, just replace “functions” with “templates”.
The fanciful template rule version
XSLT doesn’t have such a thing, but if it did, it might look something like this:
<xsl:variable name="x" special="yes"/> <!-- made-up "special" attribute --> <xsl:variable name="y" special="yes"/> <xsl:template match="foo"> <xsl:let name="x" select="string(@x)"> <!-- made-up "let" element --> <xsl:let name="y" select="string(@y)"> <xsl:apply-templates select="document('some-doc.xml')//bar"/> </xsl:let> </xsl:let> </xsl:template> <xsl:template match="bar"> <my-element x="{$x}"> <xsl:apply-templates select="document('another-doc.xml')//bat"/> </my-element/> </xsl:template> <xsl:template match="bat"> <another-element y="{$y}"/> </xsl:template>
To me, this would be ideal. No parameter-passing required.
XSLT 2.0 offers a very good compromise
XSLT 2.0 introduces what are called “tunnel parameters”. I’ve known about these for a while, but my new acquaintance with dynamic variables in Lisp is helping me understand where these fall in the spectrum of programming language features. If the above example shows what a “global dynamic variable” looks like, then tunnel parameters represent what you would call a “local dynamic variable”. So here’s the bona fide XSLT 2.0 version of the above, without resorting to any made-up syntax:
<xsl:template match="foo"> <xsl:apply-templates select="document('some-doc.xml')//bar"> <xsl:with-param name="x" select="string(@x)" tunnel="yes"/> <xsl:with-param name="y" select="string(@y)" tunnel="yes"/> </xsl:apply-templates> </xsl:template> <xsl:template match="bar"> <xsl:param name="x" tunnel="yes"/> <my-element x="{$x}"> <xsl:apply-templates select="document('another-doc.xml')//bat"/> </my-element/> </xsl:template> <xsl:template match="bat"> <xsl:param name="y" tunnel="yes"/> <another-element y="{$y}"/> </xsl:template>
With tunnel parameters, you don’t have to declare a parameter unless you need access to it. And the only time you have to explicitly pass the parameters is when you initialize them. After that, you can just trust that they will be there for you when you do need them, later on down the call stack.
Since this is just a simple example (with only two template rules being invoked), it doesn’t do justice to how helpful tunnel parameters actually are. Imagine 10 more template rules that get invoked between the first and last one. Most of those won’t require any xsl:param
declarations. In other words, the final example above is much better than it looks. Conversely, the first template rule example above (the XSLT 1.0 solution) is much, much worse than it looks. Imagine having to type out <xsl:param>
or <xsl:with-param>
40 times in a row. That’s essentially what you’d have to do.
Conclusion
I may actually write some programs in Lisp at some point, but so far the insights I’m getting into XSLT alone are worth the effort of learning it.