Skip to content

Sequences and types

The single biggest change from 1.0 is underfoot in everything else on these pages: 1.0's node-set is gone, replaced by the sequence. Understanding the sequence — and the as= type system that describes it — is what makes xsl:function, maps and arrays, and higher-order functions feel natural rather than bolted on.

What a sequence is

A sequence is an ordered list of items, where an item is either a node or an atomic value (a string, number, boolean, date, …). That is the whole model. Two consequences trip up people coming from 1.0:

  • Sequences carry order — document order, or the order you built them in. Node-sets were unordered; sequences are not.
  • Sequences are flat. There is no such thing as a sequence of sequences: (1, (2, 3), 4) is (1, 2, 3, 4). Nesting only exists once you reach for arrays.

You build them with the comma operator, and the empty sequence is ():

<xsl:variable name="primes" select="(2, 3, 5, 7)"/>
<xsl:variable name="prices" select="/catalog/cd/price"/>   <!-- a sequence of 3 nodes -->
<xsl:variable name="nothing" select="()"/>

A single value and a one-item sequence are the same thing — 42 and (42) are indistinguishable. This is why a function can declare it returns one integer and you use the result without unwrapping.

Describing a sequence: as=

A sequence type names what is allowed: an item type plus an occurrence indicator. You attach it with as= on xsl:variable, xsl:param, and xsl:function:

<xsl:variable name="count"  as="xs:integer"   select="count(/catalog/cd)"/>
<xsl:variable name="titles" as="xs:string*"   select="/catalog/cd/title/string()"/>
<xsl:param    name="cd"     as="element(cd)"/>

The occurrence indicator on the end controls how many:

Type Means
xs:integer exactly one integer
xs:integer? zero or one
xs:integer* zero or more
xs:integer+ one or more

And the item type controls what:

Item type Matches
xs:string, xs:decimal, xs:date, … a typed atomic value
element(cd) an element named cd
element() / attribute() any element / attribute
node() any node
item() anything — node or atomic
function(*), map(*), array(*) a function, map, or array

So element(cd)+ is "one or more cd elements" and item()* is "any sequence at all" — the implicit default when you omit as=.

Types in HE vs schema-awareness (EE)

Everything above works in the free Saxon-HE: the built-in atomic types (xs:integer, xs:date, …), occurrence indicators, and element-name tests like element(cd). What needs Saxon-EE is schema-awareness — validating input against an XSD so elements carry their schema-defined types, plus the schema-aware tests schema-element(cd) / schema-attribute(...). In HE, cd/price atomizes to an untyped value (effectively a string), which is exactly why the examples cast explicitly with xs:decimal(...) rather than relying on a schema.

Declare types — the errors move earlier

as= is optional, but adding it turns a class of silent wrong-answer bugs into compile-time errors. Declare a function returns xs:integer and accidentally return a string, and Saxon tells you at compile time instead of producing surprising output at run time. The discipline pays for itself on anything non-trivial.

Atomization: nodes become values

When an operation wants a value but you hand it a node, the node is atomized — its typed value is extracted. This is why comparing a node to a number works:

<!-- price is a node; 9.90 is a number; atomization bridges them -->
<xsl:if test="cd/price > 9.90">  </xsl:if>

Atomization is also why xs:decimal(cd/price) is worth writing when you mean a number: it pins the type down rather than leaving it as untyped atomic, so later arithmetic and sorting behave.

Returning items, not just text

In 1.0, xsl:value-of was the only way out and it produced text. 2.0/3.0 add xsl:sequence, which returns the items themselves — nodes, atomics, maps, functions — without flattening them to a string:

<xsl:variable name="expensive" as="element(cd)*">
  <xsl:sequence select="/catalog/cd[price > 9.90]"/>
</xsl:variable>

$expensive now holds actual cd elements you can navigate into ($expensive/title), not a copy and not a string. xsl:sequence is the workhorse for building and returning typed values, and it is what xsl:function uses to hand back its result.

Testing and converting types

A handful of XPath operators work on sequence types directly:

. instance of element(cd)        <!-- boolean: is the context an element named cd? -->
$x castable as xs:date           <!-- boolean: would the cast succeed? -->
$x cast as xs:integer            <!-- convert, or error if it can't -->
$x treat as element()+           <!-- assert the type without converting -->

instance of and castable as are the safe, test-first pair; cast as and treat as commit and raise a type error if they are wrong.

Where to go next