Chapter 16. Generic and Functional Programming

The brilliant moves we occasionally make would not have been possible without the prior dumb ones.

Stanley Goldstein

Introduction

This chapter renders all previous chapters moot. Okay, maybe that is a slight exaggeration. The fact is that the examples in previous chapters solve the particular problems they address. They are also useful for didactic purposes. If an example does not solve a problem you face, it might point the way to a solution. The desired solution could be a small modification of the code or a reapplication of the same techniques.

This chapter sets its goals a little higher. It presents examples that solve a very broad range of problems without requiring customization of the example’s core code. Those of you who are familiar with C++, and specifically the Standard Template Library (STL), already know the power you can obtain by creating generic code (generic algorithms) and reusing them in various contexts. Others who use functional programming languages (e.g., Lisp, ML, or Haskell) also know of the great power obtained through the creation of higher-order functions: general-purpose functions that are specialized by accepting special purpose functions as arguments. This chapter shows that XSLT, although not specifically designed as a generic or functional language, has inherent capabilities to enable similar usage.

Warning

The techniques used in this chapter stretch the abilities of XSLT quite a bit. Not everyone will want to use the examples, some of which are complex and slow. Nevertheless, I am reminded of the days before C++ had native support for templates. You could fake generic programming by using macros, but the results were awkward. However, enough people saw the potential, and templates soon became a first-class feature in C++, and possibly one of C++’s most important characteristics, despite the proliferation of other OO languages. Pushing the language envelope in this way puts pressure on the language and possibly makes it evolve faster. This faster development is good because languages that cease to evolve often die out.

Before diving into the examples, let’s first discuss some of the general techniques used in this chapter. This will allow the examples to concentrate on the application of the techniques rather than their mechanics.

Extending the Content of Global Variables

This chapter extensively uses XSLT’s ability to import (xsl:import) and override templates, variables, and other top-level elements in the importing spreadsheet.

Tip

I like to use the object-oriented term override when discussing xsl:import; however, a more technically correct explanation notes that some top-level elements in the importing stylesheet have higher import precedence than matching elements in the imported stylesheet. You can find a complete explanation of how each XSLT top-level element works with respect to xsl:import in Michael Kay’s XSLT Programmer’s Reference (Wrox, 2001).

This chapter takes advantage of the ability to combine a global variable’s contents defined in an imported stylesheet with one defined in an importing stylesheet.

The following stylesheet defines two variables. The first, $data1-public-data , is unique to this stylesheet. The second, $data, is defined in terms of the first, but can be overridden:

<!-- data1.xslt -->
   
<xsl:stylesheet version="1.0" 
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:d="http://www.ora.com/XSLTCookbook/NS/data">
   
<xsl:output method="xml" indent="yes"/>
   
<d:data value="1"/>
<d:data value="2"/>
<d:data value="3"/>
   
<xsl:variable name="data1-public-data" select="document('')/*/d:*"/>
<xsl:variable name="data" select="$data1-public-data"/>
   
<xsl:template match="/">
  <demo>
    <xsl:copy-of select="$data"/>
  </demo>
</xsl:template>
   
</xsl:stylesheet>

Now define another stylesheet that extends the value of $data. It too defines a unique variable that is the union of the first stylesheet’s public data and locally defined data. It then redefines $data in terms of this union:

<!-- data2.xslt -->
   
<xsl:stylesheet version="1.0" 
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:d="http://www.ora.com/XSLTCookbook/NS/data">
   
<xsl:import href="data1.xslt"/>
   
<xsl:output method="xml" indent="yes"/>
   
<d:data value="5"/>
<d:data value="7"/>
<d:data value="11"/>
   
<xsl:variable name="data2-public-data" 
    select="document('')/*/d:* | $data1-public-data"/>
   
<xsl:variable name="data" select="$data2-public-data"/>
   
</xsl:stylesheet>

The output of data1.xslt is:

<demo xmlns:d="data">
   <d:data value="1"/>
   <d:data value="2"/>
   <d:data value="3"/>
</demo>

The output of data2.xslt is:

<demo xmlns:d="data">
   <d:data value="1"/>
   <d:data value="2"/>
   <d:data value="3"/>
   <d:data value="5"/>
   <d:data value="7"/>
   <d:data value="11"/>
</demo>

Defining the second stylesheet’s $data in terms of the first’s, without the need for the extra variables, would be convenient; however, XSLT treats this definition circularly. The technique defines a named set that is operated on by templates in a core stylesheet but allows importing stylesheets to expand the set. The motivation for this will become more clear as you proceed.

Using Template Tags

XSLT provides no direc t way to pass the name of a template to another template so that the second template can invoke the first indirectly. In other words, the following code is illegal in XSLT 1.0 and 2.0:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
   
<!-- THIS IS NOT LEGAL XSLT -->
   
<xsl:template match="/">
  <!-- We can call templates by name ...-->
  <xsl:call-template name="sayIt">
    <xsl:with-param name="aTempl" select=" 'sayHello' "/>
  </xsl:call-template>
  
  <xsl:call-template name="sayIt">
    <xsl:with-param name="aTempl" select=" 'sayGoodbye' "/>
  </xsl:call-template>
</xsl:template>
   
<xsl:template name="sayIt">
  <xsl:param name="aTempl"/>
               <!--But not when the name is indirectly specified with a variable -->
               <xsl:call-template name="{$aTemple}"/>
</xsl:template>
   
<xsl:template name="sayHello">
  <xsl:value-of select=" 'Hello!' "/>
</xsl:template>
   
<xsl:template name="sayGoodbye">
  <xsl:value-of select=" 'Goodbye!' "/>
</xsl:template>
   
</xsl:stylesheet>

As it turns out, you can create some powerful and reusable code if you can figure out how to achieve this level of indirection within the confines of XSLT. Fortunately, you can achieve this goal by using matching instead of naming. The trick is to define a template that can match only one particular piece of data. That piece of data is called a template tag, and by convention, you define the tag directly above the template it matches:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:f="http://www.ora.com/XSLTCookbook/namespaces/func">
   
<xsl:output method="text"/>
   
<xsl:template match="/">
  <xsl:call-template name="sayIt">
    <xsl:with-param name="aTempl" select=" 'sayHello' "/>
  </xsl:call-template>
  <xsl:call-template name="sayIt">
    <xsl:with-param name="aTempl" select=" 'sayGoodbye' "/>
  </xsl:call-template>
</xsl:template>
   
<xsl:template name="sayIt">
  <xsl:param name="aTempl"/>
  <!--Apply templates by selecting a tag element that is unique to the template we
  want to invoke -->
  <xsl:apply-templates select="document('')/*/f:func[@name=$aTempl]"/>
</xsl:template>
   
<!-- A tagged template consists of a tag element and a template that matches that 
tagged element -->
<f:func name="sayHello"/>
<xsl:template match="f:func[@name='sayHello']">
  <xsl:text>Hello!&#xa;</xsl:text>
</xsl:template>
   
<!-- Another tagged template -->
               <f:func name="sayGoodbye"/>
<xsl:template match="f:func[@name='sayGoodbye']">
  <xsl:text>Goodbye!&#xa;</xsl:text>
</xsl:template>
   
</xsl:stylesheet>

In this particular case, these contortions are pure overkill because you could simply create a template that takes the output string as data. The true power of this technique is only realized when the tagged functions compute something the caller can use.

When using this technique, use a sanity-checking template that will match when no tagged template matches:

  <xsl:template match="f:func">
       <xsl:message terminate="yes">
     BAD FUNC! Template may not match generic:func declaration.
       </xsl:message>
  </xsl:template>

Mike Kay points out that another generic programming technique has the templates match themselves, as in:

<xsl:template name="f:sayHello"
              match="xsl:template[@name='f:sayHello']">
  <xsl:text>Hello!&#xa;</xsl:text>
</xsl:template>
   
<xsl:template name="f:sayGoodbye"
              match="xsl:template[@name='f:sayGoodbye']">
  <xsl:text>Goodbye!&#xa;</xsl:text>
</xsl:template>

By using this technique, you can still call the template by name without any problems, and it still looks like a normal template. This chapter does not use this technique because we sometimes like to associate other data with the template tags and thus prefer them to be separate elements.

See Also

XSLT 1.0

Dimitre Novatchev was the first person, to my knowledge, to discover techniques for generic and functional programming in XSLT. He wrote several articles on the topic. See http://fxsl.sourceforge.net. The generic programming recipes in this chapter were developed before I discovered Dimitre’s work and vary in some ways from Dimitre’s approach. I would recommend viewing Dimitre’s work only after you are comfortable with these examples. Dimitre pushes the edge of what can be done further than I do, and his techniques are thus somewhat more challenging. Dimitre has an XSLT library called FXSL - an XSLT functional programming library that can be downloaded from http://sourceforge.net/project/showfiles.php?group_id=53841.

XSLT 2.0

Dimitre Novatchev has a version of FXSL that takes advantage of the new capabilities of XSLT 2.0 (http://sourceforge.net/project/showfiles.php?group_id=53841). Most notably, his library takes great advantage of 2.0’s functions and function overloading. I stongly recommend his library to XSLT 2.0 developers interested in “higher-order” programming in XSLT.

Here are some excerpts from Dimitre’s post regarding fxsl and higher order XSLT programming. Here, HOF stands for Higher Order Function.

The benefit should be obvious:

1. Composability: f:map can be in any place where we could have a functional composition chain.

2. It is much simpler and more understandable to write:

f:map(fsomeFun(), $seq)

than

<xsl:for-each select="$seq">

<xsl:sequence select="f:apply(fsomeFun(), $seq)"/>

</xsl:for-each>

Having a HOF wrapper with the same name and signature as any one of all the F(unctions) & O(perators) provides essentially a functional programming system, where all standard XPath 2.0 F & Os (and all standard XSLT 2.0 functions) are higher order.

The programmer can immediately start using all standard XPath 2.0 F & Os (which he knows well) without having to do anything in addition. The necessary steep learning curve is almost avoided. This is wealth of functions that are already implemented and they have guaranteed support by any compliant XSLT 2.0 processor.

Another important result is a set of strict and simple rules how to write user-defined xsl:function’s that are implemented as higher order.

Adhering to these rules makes XSLT 2.0 + FXSL really a closed (in relation of HOF support) functional programming system.

Yet another benefit is that all HOF functions could potentially be used in a non-XSLT environment (for example, if they are available in pre-compiled form), such as using embedded XPath 2.0 in a programming language, or why not in XQuery. Of course, in this approach any new HOF will have to be implemented in XSLT and precompiled before they can be referenced.

Trying to summarize all the benefits:

The FXSL + XSLT 2.0 functional programming system has already reached a state where the solutions to many very difficult problems can be simply expressed as one-liner XPath expressions.

This may represent a radical change in the way of reasoning and in how problems are solved in XSLT.

Here Dimitre demonstrates the ease of performing complex mathematical manipulations using FXSL.

The other, factual reason is that using XSLT for math computations is simple, compact, elegant, and fun.

For example, to get the sequence:

N^10, N = 1 to 10

one simply writes this one-line expression:

f:map(f:flip(f:pow(),10), 1 to 10)

To get the sum of the tenth powers of all numbers from 1 to 10, one writes this one-liner:

sum(f:map(f:flip(f:pow(),10), 1 to 10))

To get the tenth root of the above, this one-liner is used:

f:pow(sum(f:map(f:flip(f:pow(),10), 1 to 10)), 0.1)

16.1. Creating Polymorphic XSLT

Problem

You want to create XSLT that performs the same function on disparate data.

Solution

There are two kinds of polymorphic behavior in XSLT. The first form is reminiscent of overloading, and the second is similar to overriding.

Some modern languages, notably C++, let you create overloaded functions: functions that have the same name but take different types as arguments. The compiler figures out which version of the function to call based on the type of data passed to it at the point of call. XSLT does not have this exact capability; however, consider the following stylesheet:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
   
<xsl:output method="html" />
   
<xsl:template match="/">
  <html>
    <head>
      <title>Area of Shapes</title>
    </head>
    <body>
      <h1>Area of Shapes</h1>
      <table cellpadding="2" border="1">
        <tbody>
          <tr>
            <th>Shape</th>
            <th>Shape Id</th>
            <th>Area</th>
          </tr>
          <xsl:apply-templates/>
        </tbody>
      </table>
    </body>
  </html>
</xsl:template>
   
<xsl:template match="shape">
  <tr>
    <td><xsl:value-of select="@kind"/></td>
    <td><xsl:value-of select="@id"/></td>
    <xsl:variable name="area">
      <xsl:apply-templates select="." mode="area"/>
    </xsl:variable> 
    <td align="right"><xsl:value-of select="format-number($area,'#.000')"/></td>
  </tr>  
</xsl:template>
     
<xsl:template match="shape[@kind='triangle']" mode="area">
  <xsl:value-of select="@base * @height"/>
</xsl:template>
   
<xsl:template match="shape[@kind='square']" mode="area">
  <xsl:value-of select="@side * @side"/> 
</xsl:template>
   
<xsl:template match="shape[@kind='rectangle']" mode="area">
  <xsl:value-of select="@width * @height"/>
</xsl:template>
   
<xsl:template match="shape[@kind='circle']" mode="area">
  <xsl:value-of select="3.1415 * @radius * @radius"/> 
</xsl:template>
   
</xsl:stylesheet>

Notice that several templates have an area mode. These templates differ in what types of shapes they accept and compute a different function based on the particular shape. If you equate mode to function name and the match pattern to the data type, you will immediately see this technique as an instance of polymorphic overloading.

Overriding is the second form of polymorphism. You can see numerous examples of overriding in this book’s examples. In XSLT, you use xsl:import to achieve this form of polymorphic behavior. The following example is a rewrite of the DocBook stylesheet from Recipe 10.1. It was engineered for extensibility in the following ways:

  • It uses variables to define primitive components of otherwise monolithic attribute content. The variables can be redefined when importing stylesheets.

  • It uses attribute sets, which can be augmented with additional attributes or have existing attributes overridden in importing stylesheets.

  • It uses simple templates for each section of the document that can be overridden in importing stylesheets.

  • It provides a hook via a call to the named template extra-head-meta-data whose default implementation does nothing.

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="html"/>
   
  <!-- Variables defining various style components -->
  <xsl:variable name="standard-font-family" select=" 'font-family: 
  Times serif; font-weight' "/>
   
  <xsl:variable name="chapter-label-font-size" select=" 'font-size : 48pt' "/>

  <xsl:variable name="chapter-title-font-size" select=" 'font-size : 24pt' "/>
  <xsl:variable name="epigraph-font-size" select=" 'font-size : 10pt' "/>
  <xsl:variable name="sect1-font-size" select=" 'font-size : 18pt' "/>
  <xsl:variable name="sect2-font-size" select=" 'font-size : 14pt' "/>
  <xsl:variable name="normal-font-size" select=" 'font-size : 12pt' "/>
   
  <xsl:variable name="normal-text-color" select=" 'color: black' "/>
  <xsl:variable name="chapter-title-color" select=" 'color: red' "/>
  
  <xsl:variable name="epigraph-padding" select=" 'padding-top:4; 
  padding-bottom:4' "/>
  
  <xsl:variable name="epigraph-common-style" select="concat($standard-font-
family,'; ', $epigraph-font-size, '; ', $epigraph-padding, '; ',$normal-text-
color)"/>
  <xsl:variable name="sect-common-style" select="concat($standard-font-family,'; 
font-weight: bold', '; ',$normal-text-color)"/>
   
  <!-- Attribute sets -->
  <xsl:attribute-set name="chapter-align">
    <xsl:attribute name="align">right</xsl:attribute> 
  </xsl:attribute-set>
   
  <xsl:attribute-set name="normal-align">
  </xsl:attribute-set>
  
  <xsl:attribute-set name="chapter-label" use-attribute-sets="chapter-align">
    <xsl:attribute name="style">
      <xsl:value-of select="$standard-font-family"/>; 
      <xsl:value-of select="$chapter-label-font-size"/>; 
      <xsl:value-of select="$chapter-title-color"/>
      <xsl:text>; padding-bottom:10; font-weight: bold</xsl:text>
    </xsl:attribute> 
  </xsl:attribute-set>  
   
  <xsl:attribute-set name="chapter-title" use-attribute-sets="chapter-align">
    <xsl:attribute name="style">
      <xsl:value-of select="$standard-font-family"/>; 
      <xsl:value-of select="$chapter-title-font-size"/>; 
      <xsl:value-of select="$chapter-title-color"/>
      <xsl:text>; padding-bottom:150; font-weight: bold</xsl:text>
    </xsl:attribute> 
  </xsl:attribute-set>  
   
  <xsl:attribute-set name="epigraph-para" use-attribute-sets="chapter-align">
    <xsl:attribute name="style">
      <xsl:value-of select="$epigraph-common-style"/><xsl:text>; 
      font-style: italic</xsl:text>
    </xsl:attribute> 
  </xsl:attribute-set>
   
  <xsl:attribute-set name="epigraph-attribution" 
  use-attribute-sets="chapter-align">
    <xsl:attribute name="style">
      <xsl:value-of select="$epigraph-common-style"/>
    </xsl:attribute> 
  </xsl:attribute-set>
   
  <xsl:attribute-set name="sect1">
    <xsl:attribute name="align">left</xsl:attribute> 
    <xsl:attribute name="style">
      <xsl:value-of select="$sect-common-style"/>; 
      <xsl:value-of select="$sect1-font-size"/>
    </xsl:attribute> 
  </xsl:attribute-set>
   
  <xsl:attribute-set name="sect2">
    <xsl:attribute name="align">left</xsl:attribute> 
    <xsl:attribute name="style">
      <xsl:value-of select="$sect-common-style"/>; 
      <xsl:value-of select="$sect2-font-size"/>
    </xsl:attribute> 
  </xsl:attribute-set>
   
  <xsl:attribute-set name="normal">
    <xsl:attribute name="align">left</xsl:attribute> 
    <xsl:attribute name="style">
      <xsl:value-of select="$standard-font-family"/>; 
      <xsl:value-of select="$normal-font-size"/>; 
      <xsl:value-of select="$normal-text-color"/>
    </xsl:attribute> 
  </xsl:attribute-set>
   
  <!-- Templates -->
  <xsl:template match="/">
    <html>
      <head>
        <xsl:apply-templates mode="head"/>
        <xsl:call-template name="extra-head-meta-data"/>
      </head>
      <body style=
      "margin-left:100;margin-right:100;margin-top:50;margin-bottom:50">
        <xsl:apply-templates/> 
        <xsl:apply-templates select="chapter/chapterinfo/*" mode="copyright"/> 
      </body>
    </html>
  
  </xsl:template>
   
  <!-- Head -->
  
  <xsl:template match="chapter/title" mode="head">
        <title><xsl:value-of select="."/></title>
  </xsl:template>
   
  <xsl:template match="author" mode="head">
        <meta name="author" content="{concat(firstname,' ', surname)}"/>
  </xsl:template>
   
  <xsl:template match="copyright" mode="head">
        <meta name="copyright" content="{concat(holder,' ',year)}"/>
  </xsl:template>
   
  <xsl:template match="text()" mode="head"/>
   
  <!--Override in importing stylesheet if necessary -->
  <xsl:template name="extra-head-meta-data"/>
  
<!-- Body -->
  
  <xsl:template match="chapter">
    <div xsl:use-attribute-sets="chapter-label">
      <xsl:value-of select="@label"/>
    </div>
    <xsl:apply-templates/>
  </xsl:template>  
   
  <xsl:template match="chapter/title">
    <div xsl:use-attribute-sets="chapter-title">
      <xsl:value-of select="."/>
    </div>
  </xsl:template>
   
  <xsl:template match="epigraph/para">
    <div xsl:use-attribute-sets="epigraph-para">
      <xsl:value-of select="."/>
    </div>
  </xsl:template>
   
  <xsl:template match="epigraph/attribution">
    <div xsl:use-attribute-sets="epigraph-attribution">
      <xsl:value-of select="."/>
    </div>
  </xsl:template>
  
  
  <xsl:template match="sect1">
    <h1 xsl:use-attribute-sets="sect1">
      <xsl:value-of select="title"/>
    </h1>
    <xsl:apply-templates/>
  </xsl:template>
   
  <xsl:template match="sect2">
    <h2 xsl:use-attribute-sets="sect2">
    <xsl:value-of select="title"/>
    </h2>
     <xsl:apply-templates/>
  </xsl:template>
  
  <xsl:template match="para">
    <p xsl:use-attribute-sets="normal">
      <xsl:value-of select="."/>
    </p>
  </xsl:template>
   
  <xsl:template match="text()"/>
   
<xsl:template match="copyright" mode="copyright">
  <div style="font-size : 10pt; font-family: Times serif; padding-top : 100">
    <xsl:text>Copyright </xsl:text>
    <xsl:value-of select="holder"/>
    <xsl:text> </xsl:text>
    <xsl:value-of select="year"/>
    <xsl:text>. All rights reserved.</xsl:text>
  </div>
</xsl:template>   
   
<xsl:template match="*" mode="copyright"/>
   
</xsl:stylesheet>

Discussion

Calling XSLT an object-oriented language would stretch the truth. The behavior of xsl:import is only loosely similar to inheritance, and it operates at the level of the entire stylesheet. Furthermore, it has no notion of encapsulation or data abstraction. However, XSLT developers who already have an object-oriented mindset can often leverage that experience to the creation of more modular and reusable stylesheets.

Consider the example of overloading in the “Solution” section. Whenever you need to perform similar operations on disparate data, you will often want to structure your stylesheet in this manner. Of course, you could achieve the same result using conditional logic:

<xsl:template match="shape">
  <tr>
    <td><xsl:value-of select="@kind"/></td>
    <td><xsl:value-of select="@id"/></td>
    <xsl:variable name="area">
      <xsl:call-template name="area"/>
    </xsl:variable> 
    <td align="right"><xsl:value-of select="format-number($area,'#.000')"/></td>
  </tr>  
</xsl:template>
     
<xsl:template name="area">
  <xsl:choose>
  
    <xsl:when test="@kind='triangle' ">
      <xsl:value-of select="@base * @height"/>
    </xsl:when>
    
    <xsl:when test="@kind='square' " >
      <xsl:value-of select="@side * @side"/> 
    </xsl:when>
    
    <xsl:when test="@kind='rectangle' ">
      <xsl:value-of select="@width * @height"/>
    </xsl:when>
    
    <xsl:when test="@kind='circle' ">
      <xsl:value-of select="3.1415 * @radius * @radius"/> 
    </xsl:when>
    
  </xsl:choose>
  
</xsl:template>

In a simple case such as this, it is difficult to argue that the overloading method is superior to the conditional one. However, consider cases when someone needs to reuse the stylesheet, except that he must deal with triangles encoded in terms of sides and angles. If the stylesheet is written using separate templates for each shape, then importing the stylesheet and extending its behavior is simple. However, if the logic for each shape is written in a monolithic conditional manner, then the user must either copy the entire stylesheet and edit the parts that must change or extend the original and risk breaking it unintentionally.

Overriding is the key to creating modular and reusable XSLT. Nevertheless, reuse does not come for free; it requires planning. Specifically, you must think about three things:

  1. What clients of your stylesheet might want to alter

  2. What you may not want them to alter

  3. What they are unlikely to alter

Failing to think about Item 1 results in an inflexible stylesheet that can be reused only by the cut, paste, and edit mechanism. Failing to think about Item 2 could create pitfalls for the client of your stylesheet. Failing to think about Item 3 results in overly complicated stylesheets with potentially poor performance and low maintainability.

In the DocBook stylesheet, we made it convenient to override font and other text attributes individually. We made it less convenient to override alignment attributes in a way that would create inconsistent or poor alignment choices between text elements where you desire consistency or a particular alignment. For example, it is more difficult to change the alignment of section and normal text elements by specifying their alignment separately. Finally, we did not try to make it easy for particular HTML element names to be overridden because doing so would have complicated the stylesheet in a way that adds little value.

See Also

Chris Rathman has a more extensive example of polymorphic XSLT at http://www.angelfire.com/tx4/cus/shapes/xsl.html.

16.2. Creating Generic Element Aggregation Functions

Problem

You want to create reusable templates that perform a wide variety of node-set aggregation operations.

Solution

A fully generic extensible solution exploits the template-tagging method discussed in this chapter’s introduction:

<xsl:stylesheet version="1.0" 
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:generic="http://www.ora.com/XSLTCookbook/namespaces/generic"
  xmlns:aggr="http://www.ora.com/XSLTCookbook/namespaces/aggregate"
  extension-element-prefixes="generic">
   
  <xsl:variable name="generic:public-generics" 
                 select="document('')/*/generic:*"/> 
  
  <xsl:variable name="generic:generics" select="$generic:public-generics"/>
  
  
  <!-- Primitive generic functions on x -->     
  
<generic:func name="identity"/>
  <xsl:template match="generic:func[@name='identity']">
       <xsl:param name="x"/>
       <xsl:value-of select="$x"/>
  </xsl:template>
  
  <generic:func name="square"/>
  <xsl:template match="generic:func[@name='square']">
       <xsl:param name="x"/>
       <xsl:value-of select="$x * $x"/>
  </xsl:template>
  
  <generic:func name="cube"/>
  <xsl:template match="generic:func[@name='cube']">
       <xsl:param name="x"/>
       <xsl:value-of select="$x * $x * $x"/>
  </xsl:template>
  
  <generic:func name="incr" param1="1"/>
  <xsl:template match="generic:func[@name='incr']">
       <xsl:param name="x"/>
       <xsl:param name="param1" select="@param1"/> 
       <xsl:value-of select="$x + $param1"/>
  </xsl:template>
   
<!-- Primitive generic aggregators -->     
   
  <generic:aggr-func name="sum" identity="0"/>
  <xsl:template match="generic:aggr-func[@name='sum']">
       <xsl:param name="x"/>
       <xsl:param name="accum"/>
       <xsl:value-of select="$x + $accum"/>
  </xsl:template>
  
  <generic:aggr-func name="product" identity="1"/>
  <xsl:template match="generic:aggr-func[@name='product']">
       <xsl:param name="x"/>
       <xsl:param name="accum"/>
       <xsl:value-of select="$x * $accum"/>
  </xsl:template>
   
  <!-- Generic aggregation template -->
  <xsl:template name="generic:aggregation">
    <xsl:param name="nodes"/>
    <xsl:param name="aggr-func" select=" 'sum' "/>
    <xsl:param name="func" select=" 'identity' "/>
    <xsl:param name="func-param1" 
               select="$generic:generics[self::generic:func and 
                                         @name = $func]/@param1"/>
    <xsl:param name="i" select="1"/>
    <xsl:param name="accum" 
               select="$generic:generics[self::generic:aggr-func and 
                                         @name = $aggr-func]/@identity"/>
     <xsl:choose>
      <xsl:when test="$nodes">
        
       <!--Compute func($x) --> 
        <xsl:variable name="f-of-x">
          <xsl:apply-templates 
                select="$generic:generics[self::generic:func and 
                                          @name = $func]">
            <xsl:with-param name="x" select="$nodes[1]"/>
            <xsl:with-param name="i" select="$i"/>
            <xsl:with-param name="param1" select="$func-param1"/>
          </xsl:apply-templates>
        </xsl:variable>
   
        <!-- Aggregate current $f-of-x with $accum -->    
        <xsl:variable name="temp">
          <xsl:apply-templates 
              select="$generic:generics[self::generic:aggr-func and 
                                  @name = $aggr-func]">
            <xsl:with-param name="x" select="$f-of-x"/>
            <xsl:with-param name="accum" select="$accum"/>
            <xsl:with-param name="i" select="$i"/>
          </xsl:apply-templates>
        </xsl:variable>       
        
        <!--We tail recursively process the remaining nodes using position() 
        -->
        <xsl:call-template name="generic:aggregation">
          <xsl:with-param name="nodes" select="$nodes[position()!=1]"/>
          <xsl:with-param name="aggr-func" select="$aggr-func"/>
          <xsl:with-param name="func" select="$func"/>
          <xsl:with-param name="func-param1" select="$func-param1"/>
          <xsl:with-param name="i" select="$i + 1"/>
          <xsl:with-param name="accum" select="$temp"/>
        </xsl:call-template>
      </xsl:when>
      <xsl:otherwise>
        <xsl:value-of select="$accum"/>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>
   
</xsl:stylesheet>

The generic code has three basic parts.

The first part consists of tagged generic functions on a single variable x. These functions allow performance of aggregation operations on functions of an input set. The simplest such function is identity, which is used when you want to aggregate the input set itself. Square, cube, and incr functions are also predefined. Users of the stylesheet can define other functions.

The second part consists of tagged generic aggregator functions. You will see two common implemented aggregators: sum and product. Again, importing stylesheets can add other forms of aggregation.

The third part consists of the generic aggregation algorithm. It accepts as parameters a set of nodes to aggregate, the name of an aggregator function (default is sum), and the name of a single element function (the default is identity). The $i parameter keeps track of the position of the currently processed node and is made available to both the element and aggregation functions, should they desire it. The $accum keeps a working value of the aggregation. Notice how the default value is initialized from the @identity attribute kept with the aggregate function’s tag. This initialization demonstrates a powerful feature of the generic approach with which metadata can be associated with the function tags. This feature is reminiscent of the way C++-based generic programming uses traits classes.

The first step to understanding this code is to show a simple application that both uses and extends the aggregation facilities, as shown in Example 16-1.

Example 16-1. Using and extending generic aggregation
<xsl:stylesheet version="1.0" 
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:generic="http://www.ora.com/XSLTCookbook/namespaces/generic"
  xmlns:aggr="http://www.ora.com/XSLTCookbook/namespaces/aggregate"
  extension-element-prefixes="generic aggr">
   
<xsl:import href="aggregation.xslt"/>
   
<xsl:output method="xml" indent="yes"/>
   
<!-- Extend the available generic functions -->
<xsl:variable name="generic:generics" select="$generic:public-generics | document('')/*/
generic:*"/>
   
<!--Add a generic element function for computing reciprocal -->
<generic:func name="reciprocal"/>
<xsl:template match="generic:func[@name='reciprocal']">
     <xsl:param name="x"/>
     <xsl:value-of select="1 div $x"/>
</xsl:template>
   
<!--Add generic aggregators for computing the min and the max values in a node set-->
<generic:aggr-func name="min" identity=""/>
<xsl:template match="generic:aggr-func[@name='min']">
     <xsl:param name="x"/>
     <xsl:param name="accum"/>
  <xsl:choose>
    <xsl:when test="$accum = @identity or $accum >= $x"> 
      <xsl:value-of select="$x"/>
    </xsl:when>
    <xsl:otherwise>
      <xsl:value-of select="$accum"/>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>
   
<generic:aggr-func name="max" identity=""/>
<xsl:template match="generic:aggr-func[@name='max']">
     <xsl:param name="x"/>
     <xsl:param name="accum"/>
  <xsl:choose>
    <xsl:when test="$accum = @identity or $accum &lt; $x"> 
      <xsl:value-of select="$x"/>
    </xsl:when>
    <xsl:otherwise>
      <xsl:value-of select="$accum"/>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>
   
<!--Test aggregation functionality -->
<xsl:template match="numbers">
   
<results>
   
  <!-- Sum the numbers -->  
  <sum>
    <xsl:call-template name="generic:aggregation">
      <xsl:with-param name="nodes" select="number"/>
    </xsl:call-template>
 </sum>
   
  <!-- Sum the squares -->
  <sumSq>
    <xsl:call-template name="generic:aggregation">
      <xsl:with-param name="nodes" select="number"/>
      <xsl:with-param name="func" select=" 'square' "/>
    </xsl:call-template>
  </sumSq>
  
  <!-- Product of the reciprocals -->
  <prodRecip>
    <xsl:call-template name="generic:aggregation">
      <xsl:with-param name="nodes" select="number"/>
      <xsl:with-param name="aggr-func" select=" 'product' "/>
      <xsl:with-param name="func" select=" 'reciprocal' "/>
    </xsl:call-template>
  </prodRecip>
   
  <!-- Maximum -->
  <max>
      <xsl:call-template name="generic:aggregation">
      <xsl:with-param name="nodes" select="number"/>
      <xsl:with-param name="aggr-func" select=" 'max' "/>
    </xsl:call-template>
  </max>
   
  <!-- Minimum -->
  <min>
      <xsl:call-template name="generic:aggregation">
      <xsl:with-param name="nodes" select="number"/>
      <xsl:with-param name="aggr-func" select=" 'min' "/>
    </xsl:call-template>
  </min>
   
</results> 
  
</xsl:template>
   
</xsl:stylesheet>

Example 16-1 shows how new element and aggregation functions can be added to those prepackaged with aggregation.xslt. You might not initially expect that computing minimums and maximums can be accomplished with this generic code, but it is quite easy to do.

You can test this code against the following input:

<numbers>
  <number>1</number>
  <number>2</number>
  <number>3</number>
</numbers>

The result is:

<?xml version="1.0" encoding="utf-8"?>
<results>
   <sum>6</sum>
   <sumSq>14</sumSq>
   <prodRecip>0.16666666666666666</prodRecip>
   <max>3</max>
   <min>1</min>
</results>

Discussion

The “Solution” section shows only the tip of the iceberg in relation to what can be done with this generic aggregation framework. For example, nothing says you must aggregate numbers. The following code shows how this generic code can be applied to strings as well:

<strings>
  <string>camel</string>
  <string>through</string>
  <string>the</string>
  <string>eye</string>
  <string>of</string>
  <string>needle</string>
</strings>
   
<!DOCTYPE stylesheet [
     <!ENTITY % standard SYSTEM "../strings/standard.ent">
     %standard;
]>
<xsl:stylesheet version="1.0" 
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:generic="http://www.ora.com/XSLTCookbook/namespaces/generic"
  extension-element-prefixes="generic">
   
<xsl:import href="aggregation.xslt"/>
   
<xsl:output method="xml" indent="yes"/>
   
<!-- Extend the available generic functions -->
<xsl:variable name="generic:generics" select="$generic:public-generics | 
document('')/*/generic:*"/>
   
<!--Add a generic element function for converting first character of $x 
to uppercase -->
<generic:func name="upperFirst"/>
<xsl:template match="generic:func[@name='upperFirst']">
     <xsl:param name="x"/>
   <!-- See Recipe 2.8 for an explanation of LOWER_TO_UPPER -->
     <xsl:variable name="upper" 
         select="translate(substring($x,1,1),&LOWER_TO_UPPER;)"/>
     <xsl:value-of select="concat($upper, substring($x,2))"/>
</xsl:template>
   
<!--Add generic aggregator that concatenates -->
<generic:aggr-func name="concat" identity=""/>
<xsl:template match="generic:aggr-func[@name='concat']">
     <xsl:param name="x"/>
     <xsl:param name="accum"/>
     <xsl:value-of select="concat($accum,$x)"/>
</xsl:template>
   
<!--Test aggregation functionality -->
<xsl:template match="strings">
   
<results>
   
  <camelCase>
    <xsl:call-template name="generic:aggregation">
      <xsl:with-param name="nodes" select="string"/>
      <xsl:with-param name="aggr-func" select=" 'concat' "/>
      <xsl:with-param name="func" select=" 'upperFirst' "/>
    </xsl:call-template>
 </camelCase>
   
</results> 
   
</xsl:template>
   
</xsl:stylesheet>
   
<results>
   <camelCase>CamelThroughTheEyeOfNeedle</camelCase>
</results>

Aggregation can also compute the statistical functions’ average and variance. Here you exploit the $i index parameter. You need to be a little crafty to compute variance; you need to maintain three values in the $accum parameter—the sum, the sum of the squares, and the running variance. You can do this by using an element with attributes. The only downside is that you are forced to use a node-set function in XSLT 1.0:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" 
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:generic="http://www.ora.com/XSLTCookbook/namespaces/generic"
  xmlns:exslt="http://exslt.org/common"
  extension-element-prefixes="generic exslt">
   
<xsl:import href="aggregation.xslt"/>
   
<xsl:output method="xml" indent="yes"/>
   
<!-- Extend the available generic functions -->
<xsl:variable name="generic:generics" select="$generic:public-generics | 
document('')/*/generic:*"/>
   
<!--Add generic aggregators for computing the min and the max values in a node set-->
<generic:aggr-func name="avg" identity="0"/>
<xsl:template match="generic:aggr-func[@name='avg']">
     <xsl:param name="x"/>
     <xsl:param name="accum"/>
     <xsl:param name="i"/>
     <xsl:value-of select="(($i - 1) * $accum + $x) div $i"/>
</xsl:template>
   
<generic:aggr-func name="variance" identity=""/>
<xsl:template match="generic:aggr-func[@name='variance']">
  <xsl:param name="x"/>
  <xsl:param name="accum"/>
  <xsl:param name="i"/>
     
  <xsl:choose>
    <xsl:when test="$accum = @identity">
      <!-- Initialize the sum, sum of squares, and variance. 
           The variance of a single number is zero -->
      <variance sum="{$x}" sumSq="{$x * $x}">0</variance>
    </xsl:when>
    <xsl:otherwise>
      <!-- Use node-set to convert $accum to a nodes set containing 
           the variance element -->
      <xsl:variable name="accumElem" select="exslt:node-set($accum)/*"/>
      <!-- Aggregate the sum of $x component -->
      <xsl:variable name="sum" select="$accumElem/@sum + $x"/>
      <!-- Aggregate the sum of $x squared component -->
      <xsl:variable name="sumSq" select="$accumElem/@sumSq + $x * $x"/>
      <!-- Return the element with attributes and the current variance 
           as its value -->
      <variance sum="{$sum}" sumSq="{$sumSq}">
        <xsl:value-of 
              select="($sumSq - ($sum * $sum) div $i) div ($i - 1)"/>
      </variance>
    </xsl:otherwise>
  </xsl:choose>
 
</xsl:template>
   
<xsl:template match="numbers">
   
<results>
   
  <!-- Average -->  
  <avg>
    <xsl:call-template name="generic:aggregation">
      <xsl:with-param name="nodes" select="number"/>
      <xsl:with-param name="aggr-func" select=" 'avg' "/>
    </xsl:call-template>
 </avg>
   
  <!-- Average of the squares -->
  <avgSq>
    <xsl:call-template name="generic:aggregation">
      <xsl:with-param name="nodes" select="number"/>
      <xsl:with-param name="func" select=" 'square' "/>
      <xsl:with-param name="aggr-func" select=" 'avg' "/>
    </xsl:call-template>
  </avgSq>
   
  <!-- Variance -->
  <variance>
    <xsl:call-template name="generic:aggregation">
      <xsl:with-param name="nodes" select="number"/>
      <xsl:with-param name="aggr-func" select=" 'variance' "/>
    </xsl:call-template>
  </variance>
   
</results> 
  
</xsl:template>
   
</xsl:stylesheet>

This example shows how you can use your aggregation facilities to compute sums of polymorphic functions:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:
generic="http://www.ora.com/XSLTCookbook/namespaces/generic" xmlns:aggr="http://www.
ora.com/XSLTCookbook/namespaces/aggregate" xmlns:exslt="http://exslt.org/common" 
extension-element-prefixes="generic aggr">
   
  <xsl:import href="aggregation.xslt"/>
  
  <xsl:output method="xml" indent="yes"/>
  
  <!-- Extend the available generic functions -->
  <xsl:variable name="generic:generics" 
      select="$generic:public-generics | document('')/*/generic:*"/>
  
  <!-- Extend the primitives to compute commission-->
  <generic:func name="commission"/>
  <xsl:template match="generic:func[@name='commission']">
    <xsl:param name="x"/>
    <!-- defer actual computation to a polymorphic template using mode commission -->
    <xsl:apply-templates select="$x" mode="commission"/>
  </xsl:template>
  
  <!-- By default salespeople get 2% commission and no base salary -->
  <xsl:template match="salesperson" mode="commission">
    <xsl:value-of select="0.02 * sum(product/@totalSales)"/>
  </xsl:template>
  
  <!-- salespeople with seniority > 4 get $10000.00 base + 0.5% commission -->
  <xsl:template match="salesperson[@seniority > 4]" mode="commission" priority="1">
    <xsl:value-of select="10000.00 + 0.05 * sum(product/@totalSales)"/>
  </xsl:template>
  
  <!-- salespeople with seniority > 8 get (seniority * $2000.00) base + 0.8% 
commission -->
  <xsl:template match="salesperson[@seniority > 8]" mode="commission" priority="2">
    <xsl:value-of select="@seniority * 2000.00 + 0.08 * 
          sum(product/@totalSales)"/>
  </xsl:template>
  
  <xsl:template match="salesBySalesperson">
    <results>
      <result>
        <xsl:text>Total commission = </xsl:text>
        <xsl:call-template name="generic:aggregation">
          <xsl:with-param name="nodes" select="*"/>
          <xsl:with-param name="aggr-func" select=" 'sum' "/>
          <xsl:with-param name="func" select=" 'commission' "/>
        </xsl:call-template>
      </result>
      <result>
        <xsl:text>Min commission = </xsl:text>
        <xsl:call-template name="generic:aggregation">
          <xsl:with-param name="nodes" select="*"/>
          <xsl:with-param name="aggr-func" select=" 'min' "/>
          <xsl:with-param name="func" select=" 'commission' "/>
        </xsl:call-template>
      </result>
      <result>
        <xsl:text>Max commission = </xsl:text>
        <xsl:call-template name="generic:aggregation">
          <xsl:with-param name="nodes" select="*"/>
          <xsl:with-param name="aggr-func" select=" 'max' "/>
          <xsl:with-param name="func" select=" 'commission' "/>
        </xsl:call-template>
      </result>
      <result>
        <xsl:text>Avg commission = </xsl:text>
        <xsl:call-template name="generic:aggregation">
          <xsl:with-param name="nodes" select="*"/>
          <xsl:with-param name="aggr-func" select=" 'avg' "/>
          <xsl:with-param name="func" select=" 'commission' "/>
        </xsl:call-template>
      </result>
      <result>
        <xsl:text>Avg sales = </xsl:text>
        <xsl:call-template name="generic:aggregation">
          <xsl:with-param name="nodes" select="*/product/@totalSales"/>
          <xsl:with-param name="aggr-func" select=" 'avg' "/>
        </xsl:call-template>
      </result>
      <result>
        <xsl:text>Min sales = </xsl:text>
        <xsl:call-template name="generic:aggregation">
          <xsl:with-param name="nodes" select="*/product/@totalSales"/>
          <xsl:with-param name="aggr-func" select=" 'min' "/>
        </xsl:call-template>
      </result>
      <result>
        <xsl:text>Max sales = </xsl:text>
        <xsl:call-template name="generic:aggregation">
          <xsl:with-param name="nodes" select="*/product/@totalSales"/>
          <xsl:with-param name="aggr-func" select=" 'max' "/>
        </xsl:call-template>
      </result>
    </results>
  </xsl:template>
  
</xsl:stylesheet>

The result when run against salesBySalesperson.xml (see Chapter 4) is:

<results xmlns:exslt="http://exslt.org/common">
   <result>Total commission = 471315</result>
   <result>Min commission = 19600</result>
   <result>Max commission = 364440</result>
   <result>Avg commission = 117828.75</result>
   <result>Avg sales = 584636.3636363636</result>
   <result>Min sales = 5500.00</result>
   <result>Max sales = 2920000.00</result>
</results>

This section has demonstrated that many of the recipes implemented separately in Chapter 3 can be implemented easily in terms of this single generic example. In fact, this generic example can compute an infinite range of aggregation-like functions over a set of nodes. Unfortunately, this flexibility and generality is not free. A generic implementation will typically be 40% slower than a custom hand-coded solution. If speed is the most important consideration, then you may want to consider an optimized hand-coded solution. However, if you need to implement a complex piece of XSLT rapidly that performs a wider variety of aggregation operations, a generic solution will speed up development substantially.[1] One of the tricks of getting the most mileage out of this approach is to have many common generic element and aggregation functions that are ready to be used. In the actual implementation of aggregation.xslt, I have all the functions from this example (and several others). You can access the complete code at the book’s web site (http://www.oreilly.com/catalog/xsltckbk).

In cases when the aggregate function is not symmetric, you might need to aggregate over a node list in reverse order. This aggregation requires only a minor change to the generic aggregation function:

  <xsl:template name="generic:reverse-aggregation">
    <xsl:param name="nodes"/>
    <xsl:param name="aggr-func" select=" 'sum' "/>
    <xsl:param name="func" select=" 'identity' "/>
    <xsl:param name="func-param1" select="$generic:generics[self::generic:func and 
                                  @name = $func]/@param1"/>
    <xsl:param name="i" select="1"/>
    <xsl:param name="accum" select="$generic:generics[self::generic:aggr-func and 
                                    @name = $aggr-func]/@identity"/>
    
    <xsl:choose>
      <xsl:when test="$nodes">
        
       <!--Compute func($x) --> 
        <xsl:variable name="f-of-x">
          <xsl:apply-templates select=
          "$generic:generics[self::generic:func and @name = $func]">
            <xsl:with-param name="x" select="$nodes[last()]"/>
            <xsl:with-param name="i" select="$i"/>
          </xsl:apply-templates>
        </xsl:variable>
   
        <!-- Aggregate current $f-of-x with $accum -->    
        <xsl:variable name="temp">
          <xsl:apply-templates 
              select="$generic:generics[self::generic:aggr-func and 
                                  @name = $aggr-func]">
            <xsl:with-param name="x" select="$f-of-x"/>
            <xsl:with-param name="accum" select="$accum"/>
            <xsl:with-param name="i" select="$i"/>
          </xsl:apply-templates>
        </xsl:variable>       
        
        <xsl:call-template name="generic:reverse-aggregation">
          <xsl:with-param name="nodes" 
                          select="$nodes[position()!=last()]"/>
          <xsl:with-param name="aggr-func" select="$aggr-func"/>
          <xsl:with-param name="func" select="$func"/>
          <xsl:with-param name="func-param1" select="$func-param1"/>
          <xsl:with-param name="i" select="$i + 1"/>
          <xsl:with-param name="accum" select="$temp"/>
        </xsl:call-template>
      </xsl:when>
      <xsl:otherwise>
        <xsl:value-of select="$accum"/>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>

See Also

FXSL (see the “See Also” section of this chapter’s introduction) has fold and foldr functions that are similar to generic:aggregation and generic:reverse-aggregation, respectively.

16.3. Creating Generic Bounded Aggregation Functions

Problem

You want to create reusable templates for performing a wide variety of bounded aggregation operations.

Solution

  <xsl:template name="generic:bounded-aggregation">
    <xsl:param name="x" select="0"/>
    <xsl:param name="func" select=" 'identity' "/>
    <xsl:param name="func-param1"/>
    <xsl:param name="test-func" select=" 'less-than' "/>
    <xsl:param name="test-param1" select="$x + 1"/> 
    <xsl:param name="incr-func" select=" 'incr' "/>
    <xsl:param name="incr-param1" select="1"/> 
    <xsl:param name="i" select="1"/>
    <xsl:param name="aggr-func" select=" 'sum' "/>
    <xsl:param name="aggr-param1"/> 
    <xsl:param name="accum" 
               select="$generic:generics[self::generic:aggr-func and 
                                         @name = $aggr-func]/@identity"/>
   
    
    <!-- Check if aggregation should continue -->  
    <xsl:variable name="continue">
      <xsl:apply-templates 
           select="$generic:generics[self::generic:func and 
                                     @name = $test-func]">
        <xsl:with-param name="x" select="$x"/>
        <xsl:with-param name="param1" select="$test-param1"/>
      </xsl:apply-templates>
    </xsl:variable>
   
    <xsl:choose>
      <xsl:when test="string($continue)">
       <!--Compute func($x) --> 
        <xsl:variable name="f-of-x">
          <xsl:apply-templates select="$generic:generics[self::generic:func 
                                                         and 
                                                         @name = $func]">
            <xsl:with-param name="x" select="$x"/>
            <xsl:with-param name="i" select="$i"/>
            <xsl:with-param name="param1" select="$func-param1"/>
          </xsl:apply-templates>
        </xsl:variable>
   
        <!-- Aggregate current $f-of-x with $accum -->    
        <xsl:variable name="temp">
          <xsl:apply-templates
               select="$generic:generics[self::generic:aggr-func and 
                                         @name = $aggr-func]">
            <xsl:with-param name="x" select="$f-of-x"/>
            <xsl:with-param name="i" select="$i"/>
            <xsl:with-param name="param1" select="$aggr-param1"/>
            <xsl:with-param name="accum" select="$accum"/>
          </xsl:apply-templates>
        </xsl:variable>       
   
        <!-- Compute the next value of $x-->    
        <xsl:variable name="next-x">
          <xsl:apply-templates 
               select="$generic:generics[self::generic:func and 
                                         @name = $incr-func]">
            <xsl:with-param name="x" select="$x"/>
            <xsl:with-param name="param1" select="$incr-param1"/>
          </xsl:apply-templates>
        </xsl:variable>    
      
          <!--We tail recursively process the remaining nodes using 
              position() -->
          <xsl:call-template name="generic:bounded-aggregation">
            <xsl:with-param name="x" select="$next-x"/>
            <xsl:with-param name="func" select="$func"/>
            <xsl:with-param name="func-param1" select="$func-param1"/>
            <xsl:with-param name="test-func" select="$test-func"/>
            <xsl:with-param name="test-param1" select="$test-param1"/> 
            <xsl:with-param name="incr-func" select="$incr-func"/>
            <xsl:with-param name="incr-param1" select="$incr-param1"/> 
            <xsl:with-param name="i" select="$i + 1"/>
            <xsl:with-param name="aggr-func" select="$aggr-func"/>
            <xsl:with-param name="aggr-param1" select="$aggr-param1"/> 
            <xsl:with-param name="accum" select="$temp"/>
          </xsl:call-template>
        </xsl:when>
        <xsl:otherwise>
          <xsl:value-of select="$accum"/>
        </xsl:otherwise>
      </xsl:choose>
  </xsl:template>

This template does not aggregate a node set, but rather aggregates over a set of values defined by an initial value, an increment function, and a predicate that determines when the aggregation should terminate. The incrementing function and test function each can take their own parameters. The other generic:bounded-aggregation parameters are the same as generic:aggregation in Recipe 16.2.

Discussion

This recipe addresses the problem of aggregating XML content generically. However, sometimes you need to perform aggregation-like operations on mathematically synthesized data.

The simplest thing to do with this generic bounded aggregation function is to implement the factorial and prod-range templates from Recipe 3.5:

<xsl:stylesheet version="1.0" 
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:generic="http://www.ora.com/XSLTCookbook/namespaces/generic"
  xmlns:aggr="http://www.ora.com/XSLTCookbook/namespaces/aggregate"
  xmlns:exslt="http://exslt.org/common"
  extension-element-prefixes="generic aggr exslt">
   
  <xsl:import href="aggregation.xslt"/>
  
  <xsl:output method="xml" indent="yes"/>
   
  <xsl:template name="factorial">
    <xsl:param name="n" select="0"/>
    
    <xsl:call-template name="generic:bounded-aggregation">
      <xsl:with-param name="x" select="$n"/>
      <xsl:with-param name="test-func" select=" 'greater-than' "/>
      <xsl:with-param name="test-param1" select="0"/> 
      <xsl:with-param name="incr-func" select=" 'decr' "/>
      <xsl:with-param name="aggr-func" select=" 'product' "/>
    </xsl:call-template>
   
  </xsl:template>
   
  <xsl:template name="prod-range">
    <xsl:param name="start" select="1"/>
    <xsl:param name="end" select="1"/>
    
    <xsl:call-template name="generic:bounded-aggregation">
      <xsl:with-param name="x" select="$start"/>
      <xsl:with-param name="test-func" select=" 'less-than-eq' "/>
      <xsl:with-param name="test-param1" select="$end"/> 
      <xsl:with-param name="incr-func" select=" 'incr' "/>
      <xsl:with-param name="aggr-func" select=" 'product' "/>
    </xsl:call-template>
   
  </xsl:template>
   
<xsl:template match="/">
   
  <results>
  
    <factorial n="0">
      <xsl:call-template name="factorial"/>
    </factorial>
    
    <factorial n="1">
      <xsl:call-template name="factorial">
        <xsl:with-param name="n" select="1"/>
      </xsl:call-template>
    </factorial>
   
    <factorial n="5">
      <xsl:call-template name="factorial">
        <xsl:with-param name="n" select="5"/>
      </xsl:call-template>
    </factorial>
   
    <factorial n="20">
      <xsl:call-template name="factorial">
        <xsl:with-param name="n" select="20"/>
      </xsl:call-template>
    </factorial>
   
    <product start="1" end="20">
      <xsl:call-template name="prod-range">
        <xsl:with-param name="start" select="1"/>
        <xsl:with-param name="end" select="20"/>
      </xsl:call-template>
    </product>
   
    <product start="10" end="20">
      <xsl:call-template name="prod-range">
        <xsl:with-param name="start" select="10"/>
        <xsl:with-param name="end" select="20"/>
      </xsl:call-template>
    </product>
   
  </results>
   
</xsl:template>
  
</xsl:stylesheet>

The resulting output is:

<results>
   <factorial n="0">1</factorial>
   <factorial n="1">1</factorial>
   <factorial n="5">120</factorial>
   <factorial n="20">2432902008176640000</factorial>
   <product start="1" end="20">2432902008176640000</product>
   <product start="10" end="20">6704425728000</product>
</results>

However, this is only the tip of the iceberg! You can also use generic:bounded-aggregation for numeric integration:

<xsl:stylesheet version="1.0" 
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:generic="http://www.ora.com/XSLTCookbook/namespaces/generic"
  xmlns:aggr="http://www.ora.com/XSLTCookbook/namespaces/aggregate"
  xmlns:exslt="http://exslt.org/common"
  extension-element-prefixes="generic aggr exslt">
   
  <xsl:import href="aggregation.xslt"/>
  
  <xsl:output method="xml" indent="yes"/>
   
<!-- Extend the available generic functions -->
<xsl:variable name="generic:generics" select="$generic:public-generics | 
document('')/*/generic:*"/>
   
  <xsl:template name="integrate">
    <xsl:param name="from" select="0"/>
    <xsl:param name="to" select="1"/>
    <xsl:param name="func" select=" 'identity' "/>
    <xsl:param name="delta" select="($to - $from) div 100"/>
    
    <xsl:call-template name="generic:bounded-aggregation">
      <xsl:with-param name="x" select="$from"/>
      <xsl:with-param name="func" select=" 'f-of-x-dx' "/>
      <xsl:with-param name="func-param1">
        <params f-of-x="{$func}" dx="{$delta}"/>
      </xsl:with-param>
      <xsl:with-param name="test-func" select=" 'less-than' "/>
      <xsl:with-param name="test-param1" select="$to"/> 
      <xsl:with-param name="incr-func" select=" 'incr' "/>
      <xsl:with-param name="incr-param1" select="$delta"/> 
      <xsl:with-param name="aggr-func" select=" 'sum' "/>
    </xsl:call-template>
   
  </xsl:template>
   
  <xsl:template name="integrate2">
    <xsl:param name="from" select="0"/>
    <xsl:param name="to" select="1"/>
    <xsl:param name="func" select=" 'identity' "/>
    <xsl:param name="delta" select="($to - $from) div 100"/>
    
    <xsl:call-template name="generic:bounded-aggregation">
      <xsl:with-param name="x" select="$from"/>
      <xsl:with-param name="func" select=" 'f-of-x-dx-2' "/>
      <xsl:with-param name="func-param1">
        <params f-of-x="{$func}" dx="{$delta}"/>
      </xsl:with-param>
      <xsl:with-param name="test-func" select=" 'less-than' "/>
      <xsl:with-param name="test-param1" select="$to"/> 
      <xsl:with-param name="incr-func" select=" 'incr' "/>
      <xsl:with-param name="incr-param1" select="$delta"/> 
      <xsl:with-param name="aggr-func" select=" 'sum' "/>
    </xsl:call-template>
   
  </xsl:template>
   
  <generic:func name="f-of-x-dx"/>
  <xsl:template match="generic:func[@name='f-of-x-dx']">
       <xsl:param name="x"/>
       <xsl:param name="param1"/>
       
      <xsl:variable name="f-of-x">
        <xsl:apply-templates select="$generic:generics[self::generic:func 
        and @name = exslt:node-set($param1)/*/@f-of-x]">
          <xsl:with-param name="x" select="$x"/>
        </xsl:apply-templates>
       </xsl:variable>
       
       <xsl:value-of select="$f-of-x * exslt:node-set($param1)/*/@dx"/>
  </xsl:template>
   
  <generic:func name="f-of-x-dx-2"/>
  <xsl:template match="generic:func[@name='f-of-x-dx-2']">
       <xsl:param name="x"/>
       <xsl:param name="param1"/>
       
       <xsl:variable name="func" select="exslt:node-set($param1)/*/@f-of-x"/>
       <xsl:variable name="dx" select="exslt:node-set($param1)/*/@dx"/>
       
      <xsl:variable name="f-of-x">
        <xsl:apply-templates 
             select="$generic:generics[self::generic:func and 
                                       @name = $func]">
          <xsl:with-param name="x" select="$x"/>
        </xsl:apply-templates>
       </xsl:variable>
   
      <xsl:variable name="f-of-x-plus-dx">
        <xsl:apply-templates select="$generic:generics[self::generic:func 
                                                       and @name = $func]">
          <xsl:with-param name="x" select="$x + $dx"/>
        </xsl:apply-templates>
       </xsl:variable>
   
      <!-- This is just the absolute value of $f-of-x-plus-dx - $f-of-x -->        
       <xsl:variable name="abs-diff" 
                  select="(1 - 2 *(($f-of-x-plus-dx - $f-of-x) &lt; 0)) * 
                          ($f-of-x-plus-dx - $f-of-x)"/>
       
       <xsl:value-of select="$f-of-x * $dx + ($abs-diff * $dx) div 2"/>
       
  </xsl:template>
   
  <xsl:template match="/">
   
  <results>
  
    <integrate desc="intgr x from 0 to 1">
      <xsl:call-template name="integrate"/>
    </integrate>
   
    <integrate desc="intgr x from 0 to 1 with more precision">
      <xsl:call-template name="integrate">
        <xsl:with-param name="delta" select="0.001"/>
      </xsl:call-template>
    </integrate>
   
    <integrate desc="intgr x from 0 to 1 with better algorithm">
      <xsl:call-template name="integrate2"/>
    </integrate>
   
    <integrate desc="intgr x**2 from 0 to 1">
      <xsl:call-template name="integrate">
        <xsl:with-param name="func" select=" 'square' "/>
      </xsl:call-template>
    </integrate>
   
    <integrate desc="intgr x**2 from 0 to 1 with better algorithm">
      <xsl:call-template name="integrate2">
        <xsl:with-param name="func" select=" 'square' "/>
      </xsl:call-template>
    </integrate>
    
  </results>
   
</xsl:template>
  
</xsl:stylesheet>

The challenge here is that you want the user of the integrate templates to pass in any old function of x. However, you need to compute a sum that is a function of that function. Hence, you need a way to define a higher-order function. In addition, you must pass the higher-order function an additional parameter—in this case, the delta used to approximate the integration. You can do this by passing an element that you synthesize on the fly, <params f-of-x="{$func}" dx="{$delta}"/>. This compound parameter is used in the higher-order functions f-of-x-dx and f-of-x-dx-2. Unfortunately, XSLT 1.0 forces you to use exsl:node-set to extract information from the compound parameter.

Here is the output of the stylesheet shown earlier:

<results>
  <integrate desc="intgr x from 0 to 1">
    0.4950000000000004
  </integrate>
  <integrate desc="intgr x from 0 to 1 with more precision">
    0.4995000000000005
  </integrate>
   <integrate desc="intgr x from 0 to 1 with better algorithm">
     0.5000000000000001
     </integrate>
   <integrate desc="intgr x**2 from 0 to 1">
     0.32835000000000036
  </integrate>
  <integrate desc="intgr x**2 from 0 to 1 with better algorithm">
    0.33335000000000037
  </integrate>
</results>

You are unlikely to need numerical integration in XSLT. Numerical integration is not the point of this example. Instead, it demonstrates the power of the generic programming style to accomplish much by using generic reusable code.

16.4. Creating Generic Mapping Functions

Problem

You want to create reusable templates for performing operations on items in a node set.

Solution

The solution involves recursively processing the elements in $nodes and invoking the generic function, $func, on each element. You allow the possibility that the function specified by $func is parameterized. This parameter can be specified by $func-param. You further state that the default value of the $func-param is obtained from an attribute, @param1, in the generic functions tag. This stipulation allows the default to be a function of the specified generic:

<xsl:template name="generic:map">
  <xsl:param name="nodes" select="/.."/>
  <xsl:param name="func" select=" 'identity' "/>
  <xsl:param name="func-param1" 
      select="$generic:generics[self::generic:func and @name = $func]/@param1"/>
  <xsl:param name="i" select="1"/>
  <xsl:param name="result" select="/.."/>
  
  <xsl:choose>
    <xsl:when test="$nodes">
      <xsl:variable name="temp">
        <xsl:apply-templates 
             select="$generic:generics[self::generic:func and 
                                       @name = $func]">
            <xsl:with-param name="x" select="$nodes[1]"/>
            <xsl:with-param name="i" select="$i"/>
            <xsl:with-param name="param1" select="$func-param1"/>
        </xsl:apply-templates>
      </xsl:variable>
   
      <xsl:call-template name="generic:map">
        <xsl:with-param name="nodes" select="$nodes[position() > 1]"/>
        <xsl:with-param name="func" select="$func"/>
        <xsl:with-param name="func-param1" select="$func-param1"/>
        <xsl:with-param name="i" select="$i +1"/>
        <xsl:with-param name="result" 
                        select="$result | exslt:node-set($temp)"/>
      </xsl:call-template>
    </xsl:when>
    <xsl:otherwise>
      <xsl:apply-templates select="$result" mode="generic:map"/>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>
   
<xsl:template match="/ | node() | @*" mode="generic:map">
  <node>
    <xsl:copy-of select="."/>
  </node>
</xsl:template>

You can see the effect of this process by considering the incr generic function:

  <generic:func name="incr" param1="1"/>
  <xsl:template match="generic:func[@name='incr']">
       <xsl:param name="x"/>
       <xsl:param name="param1" select="@param1"/> 
       <xsl:value-of select="$x + $param1"/>
  </xsl:template>

The incr generic function’s parameter specifies the amount by which to increment and is defaulted to one. Here you create a stylesheet that maps incr across a node set of numbers, first using the default parameter and then by setting it to 10. For good measure, you also extend the set of generics to include a reciprocal function and map the numbers using that function:

<xsl:stylesheet version="1.0" 
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:generic="http://www.ora.com/XSLTCookbook/namespaces/generic"
  xmlns:exslt="http://exslt.org/common"
  extension-element-prefixes="exslt" exclude-result-prefixes="generic">
   
<xsl:import href="aggregation.xslt"/>
   
<xsl:output method="xml" indent="yes"/>
   
<!-- Extend the available generic functions -->
<xsl:variable name="generic:generics" select="$generic:public-generics | 
document('')/*/generic:*"/>
   
<!--Add a generic element function for computing reciprocal -->
<generic:func name="reciprocal"/>
<xsl:template match="generic:func[@name='reciprocal']">
     <xsl:param name="x"/>
     <xsl:value-of select="1 div $x"/>
</xsl:template>
   
<!--Test map functionality -->
<xsl:template match="numbers">
   
<results>
   
<incr>
  <xsl:call-template name="generic:map">
    <xsl:with-param name="nodes" select="number"/>
    <xsl:with-param name="func" select=" 'incr' "/>
  </xsl:call-template>
</incr>
<incr10>
    <xsl:call-template name="generic:map">
      <xsl:with-param name="nodes" select="number"/>
      <xsl:with-param name="func" select=" 'incr' "/>
      <xsl:with-param name="func-param1" select="10"/>
   </xsl:call-template>
</incr10>
<recip>
      <xsl:call-template name="generic:map">
        <xsl:with-param name="nodes" select="number"/>
        <xsl:with-param name="func" select=" 'reciprocal' "/>
     </xsl:call-template>
</recip>
</results> 
  
</xsl:template>
   
</xsl:stylesheet>

This results in the following output:

   <incr>
      <node>11</node>
      <node>4.5</node>
      <node>5.44</node>
      <node>78.7777</node>
      <node>-7</node>
      <node>2</node>
      <node>445</node>
      <node>2.1234</node>
      <node>8.77</node>
      <node>4.1415927</node>
   </incr>
   <incr10>
      <node>20</node>
      <node>13.5</node>
      <node>14.440000000000001</node>
      <node>87.7777</node>
      <node>2</node>
      <node>11</node>
      <node>454</node>
      <node>11.1234</node>
      <node>17.77</node>
      <node>13.1415927</node>
   </incr10>
   <recip>
      <node>0.1</node>
      <node>0.2857142857142857</node>
      <node>0.2252252252252252</node>
      <node>0.012857155714298572</node>
      <node>-0.125</node>
      <node>1</node>
      <node>0.0022522522522522522</node>
      <node>0.8901548869503294</node>
      <node>0.1287001287001287</node>
      <node>0.31830988148145367</node>
   </recip>
</results>

Discussion

Map can extract a subset of a node set that meets specified criteria. Here you use a generic function that is a predicate. You achieve the desired effect by structuring your predicates so that they return their input when the predicate is true and nothing when the predicate is false. For example:

  <generic:func name="less-than"/>
  <xsl:template match="generic:func[@name='less-than']">
       <xsl:param name="x"/>
       <!-- limit -->
       <xsl:param name="param1"/> 
       <xsl:if test="$x &lt; $param1"><xsl:value-of select="$x"/></xsl:if>
  </xsl:template>

You then capture the nodes of the result with a simple filtering template:

xsl:template match="/ | node() | @*" mode="generic:map">
  <xsl:if test="string(.)">
    <node>
      <xsl:copy-of select="."/>
    </node>
  </xsl:if>
</xsl:template>

Here is a sample of the technique in action:

<xsl:stylesheet version="1.0" 
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:generic="http://www.ora.com/XSLTCookbook/namespaces/generic"
  xmlns:exslt="http://exslt.org/common"
  extension-element-prefixes="exslt" exclude-result-prefixes="generic">
   
<xsl:import href="aggregation.xslt"/>
   
<xsl:output method="xml" indent="yes"/>
   
<!--Test map functionality -->
<xsl:template match="numbers">
   
<results>
   
<less-than-5>
  <xsl:call-template name="generic:map">
    <xsl:with-param name="nodes" select="number"/>
    <xsl:with-param name="func" select=" 'less-than' "/>
    <xsl:with-param name="func-param1" select="5"/>
  </xsl:call-template>
</less-than-5>
   
<greater-than-5>
  <xsl:call-template name="generic:map">
    <xsl:with-param name="nodes" select="number"/>
    <xsl:with-param name="func" select=" 'greater-than' "/>
    <xsl:with-param name="func-param1" select="5"/>
  </xsl:call-template>
</greater-than-5>
   
</results>
   
  
</xsl:template>
   
<xsl:template match="/ | node() | @*" mode="generic:map">
  <xsl:if test="string(.)">
    <node>
      <xsl:copy-of select="."/>
    </node>
  </xsl:if>
</xsl:template>

A test of this stylesheet produces the following output:

<results>
   <less-than-5>
      <node>3.5</node>
      <node>4.44</node>
      <node>-8</node>
      <node>1</node>
      <node>1.1234</node>
      <node>3.1415927</node>
   </less-than-5>
   <greater-than-5>
      <node>10</node>
      <node>77.7777</node>
      <node>444</node>
      <node>7.77</node>
   </greater-than-5>
</results>

Mapping is not limited to numerical processing. Consider the following stylesheet that finds the length of all para elements in a DocBook document:

<xsl:stylesheet version="1.0" 
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:generic="http://www.ora.com/XSLTCookbook/namespaces/generic"
  xmlns:exslt="http://exslt.org/common"
  extension-element-prefixes="exslt" exclude-result-prefixes="generic">
   
<xsl:import href="aggregation.xslt"/>
   
<xsl:output method="xml" indent="yes"/>
   
<!-- Extend the available generic functions -->
<xsl:variable name="generic:generics" select="$generic:public-generics | 
document('')/*/generic:*"/>
   
<!--Add a generic element function for computing reciprocal -->
<generic:func name="length"/>
<xsl:template match="generic:func[@name='length']">
  <xsl:param name="x"/>
  <xsl:value-of select="string-length($x)"/>
</xsl:template>
   
<!--Test map functionality -->
<xsl:template match="/">
   
<para-lengths>
    <xsl:call-template name="generic:map">
      <xsl:with-param name="nodes" select="//para"/>
      <xsl:with-param name="func" select=" 'length' "/>
    </xsl:call-template>
</para-lengths>
  
</xsl:template>
   
<xsl:template match="/ | node() | @*" mode="generic:map">
  <length>
    <xsl:copy-of select="."/>
  </length>
</xsl:template>
   
</xsl:stylesheet>

Or this example, which creates a document summary by extracting the first three sentences of each paragraph:

<xsl:stylesheet version="1.0" 
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:generic="http://www.ora.com/XSLTCookbook/namespaces/generic"
  xmlns:exslt="http://exslt.org/common"
  extension-element-prefixes="exslt" exclude-result-prefixes="generic">
   
<xsl:import href="aggregation.xslt"/>
   
<xsl:output method="xml" indent="yes"/>
   
<!-- Extend the available generic functions -->
<xsl:variable name="generic:generics" select="$generic:public-generics | 
document('')/*/generic:*"/>
   
<!--A generic function for extracting sentences -->
<generic:func name="extract-sentences" param1="1"/>
<xsl:template match="generic:func[@name='extract-
sentences']" name="generic:extract-sentences">
  <xsl:param name="x"/>
  <xsl:param name="param1" select="@param1"/> 
   <xsl:choose>
    <xsl:when test="$param1 >= 1 and contains($x,'.')">
      <xsl:value-of select="substring-before($x,'.')"/>
      <xsl:text>.</xsl:text>
      <xsl:call-template name="generic:extract-sentences">
        <xsl:with-param name="x" select="substring-after($x,'.')"/>
        <xsl:with-param name="param1" select="$param1 - 1"/>
      </xsl:call-template>
    </xsl:when>
    <xsl:otherwise/>
  </xsl:choose>
</xsl:template>
   
<xsl:template match="/">
   
<summary>
    <xsl:call-template name="generic:map">
      <xsl:with-param name="nodes" select="//para"/>
      <xsl:with-param name="func" select=" 'extract-sentences' "/>
      <xsl:with-param name="func-param1" select="3"/>
    </xsl:call-template>
</summary>
  
</xsl:template>
   
<xsl:template match="/ | node() | @*" mode="generic:map">
  <para>
    <xsl:copy-of select="."/>
  </para>
</xsl:template>
   
</xsl:stylesheet>

These examples are convoluted ways of creating results that can easily be obtained with more straightforward stylesheets. Creating stylesheets that perform transformations a node at a time is easy. For example, the summary stylesheet is more clearly implemented as follows:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
     <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
     
<xsl:template name="extract-sentences">
  <xsl:param name="text"/>
  <xsl:param name="num-sentences" select="1"/> 
   <xsl:choose>
    <xsl:when test="$num-sentences >= 1 and contains($text,'.')">
      <xsl:value-of select="substring-before($text,'.')"/>
      <xsl:text>.</xsl:text>
      <xsl:call-template name="extract-sentences">
        <xsl:with-param name="text" select="substring-after($text,'.')"/>
        <xsl:with-param name="num-sentences" select="$num-sentences - 1"/>
      </xsl:call-template>
    </xsl:when>
    <xsl:otherwise/>
  </xsl:choose>
</xsl:template>
   
<xsl:template match="/">
  <summary>
    <xsl:apply-templates select=".//para"/>
  </summary>
</xsl:template>
   
<xsl:template match="para">
  <para>
      <xsl:call-template name="extract-sentences">
        <xsl:with-param name="text" select="."/>
        <xsl:with-param name="num-sentences" select="3"/>
      </xsl:call-template>
    </para>
</xsl:template>
     
</xsl:stylesheet>

However, if in a single stylesheet you need to perform several map-like operations, then the generic implementation could result in less custom-written code.

An alternate mapping generic function is generic:map2. Rather than applying a unary function to a single node set, generic:map2 applies a binary function to the elements of two node sets and returns the resulting node set:

<xsl:template name="generic:map2">
  <xsl:param name="nodes1" select="/.."/>
  <xsl:param name="nodes2" select="/.."/>
  <xsl:param name="func" select=" 'identity' "/>
  <xsl:param name="func-param1" select="$generic:generics[self::generic:func and 
@name = $func]/@param1"/>
  <xsl:param name="i" select="1"/>
  <xsl:param name="result" select="/.."/>
  
  <xsl:choose>
    <xsl:when test="$nodes1 and $nodes2">
      <xsl:variable name="temp">
        <xsl:apply-templates 
             select="$generic:generics[self::generic:aggr-func and 
                                       @name = $func]">
            <xsl:with-param name="x" select="$nodes1[1]"/>
            <xsl:with-param name="accum" select="$nodes2[1]"/>
            <xsl:with-param name="i" select="$i"/>
            <xsl:with-param name="param1" select="$func-param1"/>
        </xsl:apply-templates>
      </xsl:variable>
   
      <xsl:call-template name="generic:map2">
        <xsl:with-param name="nodes1" select="$nodes1[position() > 1]"/>
        <xsl:with-param name="nodes2" select="$nodes2[position() > 1]"/>
        <xsl:with-param name="func" select="$func"/>
        <xsl:with-param name="func-param1" select="$func-param1"/>
        <xsl:with-param name="i" select="$i +1"/>
        <xsl:with-param name="result" select="$result | exslt:node-set($temp)"/>
      </xsl:call-template>
    </xsl:when>
    <xsl:otherwise>
      <xsl:apply-templates select="$result" mode="generic:map"/>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

generic:map2 operates on two independent node sets in parallel and thus can apply any binary generic operation to the successive nodes. As with generic:map, the utility is a function of the frequency of usage.

16.5. Creating Generic Node-Set Generators

Problem

You want to create reusable templates for generating a node set computationally.

Solution

The first generic function in this category generates a node set by executing a function over successive values, as defined by an incrementing function, until an upper bound is reached:

<xsl:template name="generic:gen-set">
  <xsl:param name="x" select="1"/>
  <xsl:param name="func" select=" 'identity' "/>
  <xsl:param name="func-param1" 
      select="$generic:generics[self::generic:func and @name = $func]/@param1"/>
  <xsl:param name="test-func" select=" 'less-than' "/>
  <xsl:param name="test-param1" select="$x + 1"/> 
  <xsl:param name="incr-func" select=" 'incr' "/>
  <xsl:param name="incr-param1" select="1"/> 
  <xsl:param name="i" select="1"/>
  <xsl:param name="result" select="/.."/>
   
    <!-- Check if aggregation should continue -->  
    <xsl:variable name="continue">
      <xsl:apply-templates 
           select="$generic:generics[self::generic:func and 
                    @name = $test-func]">
        <xsl:with-param name="x" select="$x"/>
        <xsl:with-param name="param1" select="$test-param1"/>
      </xsl:apply-templates>
    </xsl:variable>
   
    <xsl:choose>
      <xsl:when test="string($continue)">
       <!--Compute func($x) --> 
        <xsl:variable name="f-of-x">
          <xsl:apply-templates 
               select="$generic:generics[self::generic:func and 
                                         @name = $func]">
            <xsl:with-param name="x" select="$x"/>
            <xsl:with-param name="i" select="$i"/>
            <xsl:with-param name="param1" select="$func-param1"/>
          </xsl:apply-templates>
        </xsl:variable>
   
        <!-- Compute the next value of $x-->    
        <xsl:variable name="next-x">
          <xsl:apply-templates 
               select="$generic:generics[self::generic:func and 
                                         @name = $incr-func]">
            <xsl:with-param name="x" select="$x"/>
            <xsl:with-param name="param1" select="$incr-param1"/>
          </xsl:apply-templates>
        </xsl:variable>    
      
          <xsl:call-template name="generic:gen-set">
            <xsl:with-param name="x" select="$next-x"/>
            <xsl:with-param name="func" select="$func"/>
            <xsl:with-param name="func-param1" select="$func-param1"/>
            <xsl:with-param name="test-func" select="$test-func"/>
            <xsl:with-param name="test-param1" select="$test-param1"/> 
            <xsl:with-param name="incr-func" select="$incr-func"/>
            <xsl:with-param name="incr-param1" select="$incr-param1"/> 
            <xsl:with-param name="i" select="$i + 1"/>
            <xsl:with-param name="result" 
                            select="$result | exslt:node-set($f-of-x)"/>
          </xsl:call-template>
        </xsl:when>
        <xsl:otherwise>
          <xsl:apply-templates select="$result" mode="generic:gen-set"/>
        </xsl:otherwise>
      </xsl:choose>
</xsl:template>
   
<xsl:template match="node()" mode="generic:gen-set">
  <gen-set>
    <xsl:copy-of select="."/>
  </gen-set>
</xsl:template>

Here you use this template to generate a list of squares of the first ten integers:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:generic="http://www.ora.com/XSLTCookbook/namespaces/generic">
   
  <xsl:import href="aggregation.xslt"/>
  
  <xsl:output method="text" />
  
  <xsl:template match="/">
    <xsl:call-template name="generic:gen-set">
      <xsl:with-param name="x" select="1"/>
      <xsl:with-param name="func" select=" 'square' "/>
      <xsl:with-param name="incr-param1" select="1"/>
      <xsl:with-param name="test-func" select=" 'less-than-eq' "/>
      <xsl:with-param name="test-param1" select="10"/> 
    </xsl:call-template>
  </xsl:template>
   
<xsl:template match="node()" mode="generic:gen-set">
<xsl:value-of select="."/>
<xsl:text> </xsl:text>
</xsl:template>
   
1 4 9 16 25 36 49 64 81 100

The second generic function in this category generates a node set by n successive applications of a function starting with an initial seed value:

<xsl:template name="generic:gen-nested">
  <xsl:param name="x" select="1"/>
  <xsl:param name="func" select=" 'identity' "/>
  <xsl:param name="func-param1" 
             select="$generic:generics[self::generic:func and 
                     @name = $func]/@param1"/>
  <xsl:param name="i" select="1"/>
  <xsl:param name="n" select="2"/>
  <xsl:param name="result">
    <xsl:value-of select="$x"/>
  </xsl:param> 
   
    <xsl:choose>
      <xsl:when test="$i &lt;= $n">
       <!--Compute func($x) --> 
        <xsl:variable name="f-of-x">
          <xsl:apply-templates 
               select="$generic:generics[self::generic:func and 
                                         @name = $func]">
            <xsl:with-param name="x" select="$x"/>
            <xsl:with-param name="i" select="$i"/>
            <xsl:with-param name="param1" select="$func-param1"/>
          </xsl:apply-templates>
        </xsl:variable>
   
          <xsl:call-template name="generic:gen-nested">
            <xsl:with-param name="x" select="$f-of-x"/>
            <xsl:with-param name="func" select="$func"/>
            <xsl:with-param name="func-param1" select="$func-param1"/>
            <xsl:with-param name="i" select="$i + 1"/>
            <xsl:with-param name="n" select="$n"/>
            <xsl:with-param name="result" 
                            select="exslt:node-set($result) | 
                                    exslt:node-set($f-of-x)"/>
          </xsl:call-template>
        </xsl:when>
        <xsl:otherwise>
          <xsl:apply-templates select="$result" mode="generic:gen-nested"/>
        </xsl:otherwise>
      </xsl:choose>
</xsl:template>
   
<xsl:template match="node()" mode="generic:gen-nested">
  <gen-nested>
    <xsl:copy-of select="."/>
  </gen-nested>
</xsl:template>

Here you use this template to build the series 2, 2 ** 2, (2 ** 2) ** 2, ((2 ** 2) ** 2) ** 2, (((2 ** 2) ** 2) ** 2) ** 2, where ** means to the power of:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:generic="http://www.ora.com/XSLTCookbook/namespaces/generic">
   
  <xsl:import href="aggregation.xslt"/>
  
  <xsl:output method="text" />
  
  <xsl:template match="/">
    <xsl:call-template name="generic:gen-nested">
      <xsl:with-param name="x" select="2"/>
      <xsl:with-param name="func" select=" 'square' "/>
      <xsl:with-param name="n" select="4"/>
    </xsl:call-template>
  </xsl:template>
   
<xsl:template match="node()" mode="generic:gen-nested">
  <xsl:value-of select="."/>
  <xsl:text> </xsl:text>
</xsl:template>
   
</xsl:stylesheet>
   
2 4 16 256 65536

Discussion

Recipe 16.2 and Recipe 16.3 were many-to-one generic transformations, and Recipe 16.5 explained many-to-many transformations. Naturally, this chapter would not be complete without a one-to-many generic transform.

With a generator, you can create random numbers that can select random nodes from an XML document. This chapter uses a simple linear congruential generator (see, for example, http://www.taygeta.com/rwalks/node1.html). Here is a stylesheet that displays a random selection of names from an input document:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:generic="http://www.ora.com/XSLTCookbook/namespaces/generic">
   
  <xsl:import href="aggregation.xslt"/>
   
  <xsl:output method="xml" indent="yes"/>
   
  <!-- Extend the available generic functions -->
  <xsl:variable name="generic:generics" select="$generic:public-generics 
  | document('')/*/generic:*"/>
   
  <!-- These values give good random results but you can tweak -->
  <xsl:variable name="a" select="16807"/>
  <xsl:variable name="c" select="0"/>
  <xsl:variable name="m"  select="2147483647"/>
   
  <!-- Store the root for later use -->
  <xsl:variable name="doc" select="/"/>
   
  <!-- The random generator -->
  <generic:func name="linear-congruential"/>
  <xsl:template match="generic:func[@name='linear-congruential']">
       <xsl:param name="x"/>
       <xsl:value-of select="($a * $x + $c) mod $m"/>
  </xsl:template>
   
  <xsl:template match="/">
  <names>
      <xsl:call-template name="generic:gen-nested">
        <xsl:with-param name="x" select="1"/>
        <xsl:with-param name="func" select=" 'linear-congruential' "/>
        <xsl:with-param name="n" select="100"/>
        <!-- Don't include initial seed -->
        <xsl:with-param name="result" select="/.."/>
      </xsl:call-template>
    </names>
  </xsl:template>
   
<xsl:template match="node()" mode="generic:gen-nested">
  
  <!-- Restrict the range to 1 through 100 -->
  <xsl:variable name="random" select=". mod 99 + 1"/>
  
   <name>
    <xsl:value-of select="$doc/names/name[$random]"/>
  </name>
  
</xsl:template>
          
</xsl:stylesheet>


[1] Not everyone would agree with this assessment. In fact, some would argue that using this approach slows down development because of the complexity caused by extra levels of indirection. However, repeated usage often makes the complex appear idiomatic. For example, recall how you felt when you first struggled with vanilla XSLT.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset