#User-Defined Functions
xsl:function lets you define custom functions that you can call from any XPath expression — in select, test, match, AVTs, sort keys, and predicates. Unlike templates, which process nodes and produce output, functions take arguments and return values. They are the XSLT equivalent of static utility methods.
#Contents
#xsl:function
A function is a top-level declaration (child of xsl:stylesheet) with a namespaced name, optional parameters, and a body that produces a return value.
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:my="http://example.com/functions"
version="3.0">
<xsl:function name="my:format-price" as="xs:string">
<xsl:param name="amount" as="xs:decimal"/>
<xsl:param name="currency" as="xs:string"/>
<xsl:sequence select="concat($currency, ' ', format-number($amount, '#,##0.00'))"/>
</xsl:function>
</xsl:stylesheet>#Key Attributes
|
Attribute |
Description |
|---|---|
|
|
Required. Must be a prefixed QName (e.g., |
|
|
Return type. Optional but strongly recommended. Examples: |
|
|
Package visibility: |
|
|
|
|
|
|
|
|
|
#Naming Rules
Function names must use a namespace prefix. This is a hard requirement — unprefixed function names are reserved for the built-in XPath function library:
<!-- CORRECT: prefixed name -->
<xsl:function name="my:discount">...</xsl:function>
<!-- ERROR: unprefixed name — conflicts with built-in function namespace -->
<xsl:function name="discount">...</xsl:function>Choose a namespace URI for your project's functions and declare it on the stylesheet:
<xsl:stylesheet xmlns:fn="http://example.com/catalog/functions" ...>
<xsl:function name="fn:is-premium" as="xs:boolean">
<xsl:param name="product" as="element(product)"/>
<xsl:sequence select="$product/price > 100"/>
</xsl:function>
</xsl:stylesheet>
C# parallel: The namespace requirement is like C# namespaces — you must qualify your types to avoid collisions with System.*.
#Function Parameters
Parameters are declared with xsl:param inside the function body, in the order they will be passed by the caller. Unlike template parameters, function parameters are always positional and cannot have defaults.
<xsl:function name="my:tax" as="xs:decimal">
<xsl:param name="price" as="xs:decimal"/>
<xsl:param name="rate" as="xs:decimal"/>
<xsl:sequence select="$price * $rate"/>
</xsl:function>
<!-- Called as: my:tax(29.99, 0.08) -->#Arity (Parameter Count)
XSLT allows function overloading by arity — you can define multiple functions with the same name but different numbers of parameters:
<!-- One-argument version: uses default tax rate -->
<xsl:function name="my:tax" as="xs:decimal">
<xsl:param name="price" as="xs:decimal"/>
<xsl:sequence select="$price * 0.08"/>
</xsl:function>
<!-- Two-argument version: custom tax rate -->
<xsl:function name="my:tax" as="xs:decimal">
<xsl:param name="price" as="xs:decimal"/>
<xsl:param name="rate" as="xs:decimal"/>
<xsl:sequence select="$price * $rate"/>
</xsl:function>Now my:tax(29.99) calls the one-argument version and my:tax(29.99, 0.10) calls the two-argument version.
C# parallel: Method overloading:
static decimal Tax(decimal price) => price * 0.08m;
static decimal Tax(decimal price, decimal rate) => price * rate;#Typing Parameters
Always declare types with as. Without it, the parameter accepts any value, which makes errors harder to diagnose:
<!-- Typed: error if called with wrong type -->
<xsl:function name="my:format-date" as="xs:string">
<xsl:param name="date" as="xs:date"/>
<xsl:sequence select="format-date($date, '[MNn] [D], [Y]')"/>
</xsl:function>
<!-- Untyped: accepts anything, may fail unpredictably -->
<xsl:function name="my:format-date">
<xsl:param name="date"/>
<xsl:sequence select="format-date($date, '[MNn] [D], [Y]')"/>
</xsl:function>#Returning Values
Functions return whatever their body produces. Use xsl:sequence to return typed values:
<!-- Return a string -->
<xsl:function name="my:full-name" as="xs:string">
<xsl:param name="person" as="element(person)"/>
<xsl:sequence select="concat($person/first-name, ' ', $person/last-name)"/>
</xsl:function>
<!-- Return a boolean -->
<xsl:function name="my:is-in-stock" as="xs:boolean">
<xsl:param name="product" as="element(product)"/>
<xsl:sequence select="$product/stock > 0"/>
</xsl:function>
<!-- Return a sequence of nodes -->
<xsl:function name="my:active-products" as="element(product)*">
<xsl:param name="catalog" as="element(catalog)"/>
<xsl:sequence select="$catalog/product[@status = 'active']"/>
</xsl:function>
<!-- Return a map -->
<xsl:function name="my:product-summary" as="map(xs:string, item()*)">
<xsl:param name="products" as="element(product)*"/>
<xsl:sequence select="map {
'count': count($products),
'total': sum($products/price),
'avg': avg($products/price)
}"/>
</xsl:function>#Do Not Use xsl:value-of in Functions
This is a common mistake. xsl:value-of creates a text node, not a typed value. If your function declares as="xs:integer", returning via xsl:value-of will fail because a text node is not an integer:
<!-- WRONG: returns a text node, not an integer -->
<xsl:function name="my:double" as="xs:integer">
<xsl:param name="n" as="xs:integer"/>
<xsl:value-of select="$n * 2"/>
</xsl:function>
<!-- RIGHT: returns the integer value -->
<xsl:function name="my:double" as="xs:integer">
<xsl:param name="n" as="xs:integer"/>
<xsl:sequence select="$n * 2"/>
</xsl:function>
C# parallel: xsl:sequence is return value;. xsl:value-of is return value.ToString(); — it loses the type.
#Multi-Step Return Values
When the return value requires multiple instructions to construct, the function body is a sequence constructor — all items produced by the body form the return value:
<xsl:function name="my:price-label" as="xs:string">
<xsl:param name="product" as="element(product)"/>
<xsl:choose>
<xsl:when test="$product/@on-sale = 'true'">
<xsl:sequence select="concat('SALE: $', format-number($product/price * 0.8, '#,##0.00'))"/>
</xsl:when>
<xsl:when test="$product/price > 100">
<xsl:sequence select="concat('$', format-number($product/price, '#,##0.00'), ' (Premium)')"/>
</xsl:when>
<xsl:otherwise>
<xsl:sequence select="concat('$', format-number($product/price, '#,##0.00'))"/>
</xsl:otherwise>
</xsl:choose>
</xsl:function>#Calling Functions
Functions are called from XPath expressions, which means they can appear anywhere an expression is allowed:
<!-- In a select expression -->
<xsl:value-of select="my:format-price(price, 'USD')"/>
<!-- In a predicate -->
<xsl:for-each select="product[my:is-in-stock(.)]">
<!-- In a sort key -->
<xsl:sort select="my:sort-rank(.)"/>
<!-- In an AVT -->
<span class="{my:status-class(@status)}">
<!-- In an xsl:if test -->
<xsl:if test="my:is-premium(.)">
<span class="badge">Premium</span>
</xsl:if>
<!-- In another function -->
<xsl:function name="my:total-with-tax" as="xs:decimal">
<xsl:param name="price" as="xs:decimal"/>
<xsl:sequence select="$price + my:tax($price)"/>
</xsl:function>
<!-- In a variable -->
<xsl:variable name="summary" select="my:product-summary(//product)"/>#Context Node in Functions
Unlike templates, functions do not have a context node. Inside a function body, . (dot) is not meaningful unless you explicitly pass a node as a parameter:
<!-- WRONG: what is "." inside the function? -->
<xsl:function name="my:bad-example" as="xs:string">
<xsl:sequence select="./name"/> <!-- Error or meaningless -->
</xsl:function>
<!-- RIGHT: pass the node explicitly -->
<xsl:function name="my:product-name" as="xs:string">
<xsl:param name="product" as="element(product)"/>
<xsl:sequence select="$product/name"/>
</xsl:function>
C# parallel: Functions are like static methods — they have no this reference. You must pass everything they need as arguments.
#Recursive Functions
Since XSLT variables are immutable, recursion replaces loops for algorithms that need accumulating state. Functions can call themselves.
#Simple Recursion
<xsl:function name="my:factorial" as="xs:integer">
<xsl:param name="n" as="xs:integer"/>
<xsl:sequence select="if ($n le 1) then 1 else $n * my:factorial($n - 1)"/>
</xsl:function>
<!-- my:factorial(5) returns 120 -->#Building Strings with Recursion
<!-- Repeat a string N times -->
<xsl:function name="my:repeat" as="xs:string">
<xsl:param name="str" as="xs:string"/>
<xsl:param name="count" as="xs:integer"/>
<xsl:sequence select="if ($count le 0) then ''
else concat($str, my:repeat($str, $count - 1))"/>
</xsl:function>
<!-- my:repeat('*', 5) returns '*****' -->
<!-- my:repeat('ab', 3) returns 'ababab' -->#Tree Traversal
<!-- Compute the depth of a node in the tree -->
<xsl:function name="my:depth" as="xs:integer">
<xsl:param name="node" as="node()"/>
<xsl:sequence select="if ($node/parent::*) then 1 + my:depth($node/parent::*)
else 0"/>
</xsl:function>#Tail Recursion Optimization
Many XSLT processors optimize tail-recursive functions (where the recursive call is the last operation). Write your recursive functions in tail-recursive form when possible:
<!-- Tail-recursive factorial -->
<xsl:function name="my:factorial" as="xs:integer">
<xsl:param name="n" as="xs:integer"/>
<xsl:sequence select="my:factorial-helper($n, 1)"/>
</xsl:function>
<xsl:function name="my:factorial-helper" as="xs:integer">
<xsl:param name="n" as="xs:integer"/>
<xsl:param name="acc" as="xs:integer"/>
<xsl:sequence select="if ($n le 1) then $acc
else my:factorial-helper($n - 1, $n * $acc)"/>
</xsl:function>
C# parallel: This is like writing a loop as a recursive method — common in functional C# or when using LINQ's Aggregate:
static int Factorial(int n) => Factorial(n, 1);
static int Factorial(int n, int acc) => n <= 1 ? acc : Factorial(n - 1, n * acc);#Higher-Order Functions
XSLT 3.0 supports higher-order functions — you can pass functions as arguments to other functions. This enables powerful abstractions like map, filter, and reduce over sequences.
#Function Items
A named function can be referenced as a value using the function-name#arity syntax:
<!-- Get a reference to the built-in contains() function (arity 2) -->
<xsl:variable name="fn" select="contains#2"/>
<!-- Get a reference to a user-defined function -->
<xsl:variable name="formatter" select="my:format-price#2"/>#Passing Functions as Arguments
<!-- A generic "apply to each" function -->
<xsl:function name="my:map-items" as="xs:string*">
<xsl:param name="items" as="item()*"/>
<xsl:param name="fn" as="function(item()) as xs:string"/>
<xsl:sequence select="for $item in $items return $fn($item)"/>
</xsl:function>
<!-- Formatter functions -->
<xsl:function name="my:as-currency" as="xs:string">
<xsl:param name="item" as="item()"/>
<xsl:sequence select="concat('$', format-number(number($item), '#,##0.00'))"/>
</xsl:function>
<xsl:function name="my:as-percentage" as="xs:string">
<xsl:param name="item" as="item()"/>
<xsl:sequence select="concat(format-number(number($item) * 100, '#,##0.0'), '%')"/>
</xsl:function>
<!-- Usage -->
<xsl:variable name="formatted-prices"
select="my:map-items(//product/price, my:as-currency#1)"/>#Anonymous Functions (Inline Functions)
XSLT 3.0 also supports anonymous functions (lambda expressions) using the XPath function() syntax:
<!-- Sort products by a custom key using an anonymous function -->
<xsl:variable name="price-extractor" select="function($p) { $p/price }"/>
<!-- Filter with an anonymous predicate -->
<xsl:variable name="expensive" select="
for-each(//product, function($p) {
if ($p/price > 100) then $p else ()
})"/>
C# parallel: Higher-order functions map directly to C#'s Func<T> and lambda expressions:
// Function reference
Func<decimal, string> formatter = FormatPrice;
// Lambda
var expensive = products.Where(p => p.Price > 100);
// Passing functions
var formatted = products.Select(p => FormatPrice(p.Price));#Function Visibility in Packages
When using XSLT packages (modular stylesheet libraries), you control which functions are visible to importing stylesheets with the visibility attribute:
|
Visibility |
Description |
|---|---|
|
|
Callable by any stylesheet that uses this package. Default. |
|
|
Only callable within the defining package. Not visible to importers. |
|
|
Public, but cannot be overridden by the using stylesheet. |
|
|
Must be overridden by the using stylesheet. Has no body. |
<!-- In the package -->
<xsl:package name="http://example.com/utils" version="1.0">
<!-- Available everywhere -->
<xsl:function name="util:format-date" as="xs:string" visibility="public">
<xsl:param name="date" as="xs:date"/>
<xsl:sequence select="format-date($date, '[MNn] [D], [Y]')"/>
</xsl:function>
<!-- Internal helper — not visible outside the package -->
<xsl:function name="util:pad-zero" as="xs:string" visibility="private">
<xsl:param name="n" as="xs:integer"/>
<xsl:sequence select="if ($n lt 10) then concat('0', $n) else string($n)"/>
</xsl:function>
<!-- Public but cannot be overridden -->
<xsl:function name="util:version" as="xs:string" visibility="final">
<xsl:sequence select="'2.1.0'"/>
</xsl:function>
<!-- Must be implemented by the using stylesheet -->
<xsl:function name="util:site-url" as="xs:string" visibility="abstract"/>
</xsl:package>C# parallel:
|
XSLT Visibility |
C# Equivalent |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
#Caching and Side Effects
#The cache Attribute
XSLT 3.0 introduces the cache attribute for memoization. When cache="yes", the processor stores the result of each unique combination of arguments and returns the cached result on subsequent calls with the same arguments:
<xsl:function name="my:expensive-lookup" as="xs:string" cache="yes">
<xsl:param name="code" as="xs:string"/>
<xsl:sequence select="doc('lookup-table.xml')//entry[@code = $code]/@description"/>
</xsl:function>If my:expensive-lookup('A001') is called 1000 times (once per product in the catalog), the document lookup happens only once. Subsequent calls return the cached result.
C# parallel: ConcurrentDictionary as a memoization cache:
private static readonly ConcurrentDictionary<string, string> _cache = new();
static string ExpensiveLookup(string code) =>
_cache.GetOrAdd(code, c => LookupTable.Entries[c].Description);#The new-each-time Attribute
This attribute tells the processor whether the function might have side effects or depend on context that changes between calls:
|
Value |
Meaning |
|---|---|
|
|
The function may return different results for the same arguments (has side effects or depends on external state). The processor must call it every time. |
|
|
The function is pure — same arguments always produce the same result. The processor may optimize calls away or reorder them. |
|
|
Default. The processor makes no assumptions. |
<!-- Pure function — safe to cache and reorder -->
<xsl:function name="my:tax" as="xs:decimal" new-each-time="no">
<xsl:param name="amount" as="xs:decimal"/>
<xsl:sequence select="$amount * 0.08"/>
</xsl:function>
<!-- Impure function — depends on current date -->
<xsl:function name="my:is-expired" as="xs:boolean" new-each-time="yes">
<xsl:param name="date" as="xs:date"/>
<xsl:sequence select="$date lt current-date()"/>
</xsl:function>
Tip: If your function only depends on its parameters (no global variables, no doc(), no current-date()), set new-each-time="no" and consider cache="yes". This gives the processor maximum freedom to optimize.
#Practical Examples
#Slug Generator
<xsl:function name="my:slugify" as="xs:string">
<xsl:param name="text" as="xs:string"/>
<xsl:variable name="lower" select="lower-case($text)"/>
<xsl:variable name="cleaned" select="replace($lower, '[^a-z0-9\s-]', '')"/>
<xsl:variable name="dashed" select="replace(normalize-space($cleaned), '\s+', '-')"/>
<xsl:sequence select="$dashed"/>
</xsl:function>
<!-- Usage: my:slugify('Widget Pro 2000!') returns 'widget-pro-2000' -->#Safe Division
<xsl:function name="my:safe-divide" as="xs:decimal">
<xsl:param name="numerator" as="xs:decimal"/>
<xsl:param name="denominator" as="xs:decimal"/>
<xsl:param name="default" as="xs:decimal"/>
<xsl:sequence select="if ($denominator = 0) then $default
else $numerator div $denominator"/>
</xsl:function>
<!-- Usage: my:safe-divide($total, $count, 0) -->#CSS Class Builder
<xsl:function name="my:classes" as="xs:string">
<xsl:param name="pairs" as="map(xs:string, xs:boolean)"/>
<xsl:sequence select="string-join(
map:keys($pairs)[map:get($pairs, .)],
' '
)"/>
</xsl:function>
<!-- Usage -->
<div class="{my:classes(map {
'product': true(),
'on-sale': @on-sale = 'true',
'featured': @featured = 'true',
'out-of-stock': stock = 0
})}">#Recursive Path Builder
<!-- Build a breadcrumb path string from a node's ancestry -->
<xsl:function name="my:breadcrumb" as="xs:string">
<xsl:param name="node" as="element()"/>
<xsl:variable name="ancestors" select="$node/ancestor-or-self::*[not(self::root)]"/>
<xsl:sequence select="string-join($ancestors/name, ' > ')"/>
</xsl:function>
<!-- Given: <root><category name="..."><subcategory><product>
Returns: "category > subcategory > product" -->#Function Composition with Higher-Order Functions
<!-- Compose two functions into one -->
<xsl:function name="my:compose" as="function(item()) as item()">
<xsl:param name="f" as="function(item()) as item()"/>
<xsl:param name="g" as="function(item()) as item()"/>
<xsl:sequence select="function($x) { $f($g($x)) }"/>
</xsl:function>
<!-- Usage: create a "format then uppercase" function -->
<xsl:variable name="shout-price"
select="my:compose(upper-case#1, my:as-currency#1)"/>
<!-- $shout-price(29.99) returns "$29.99" uppercased to "$29.99" -->C# parallel summary:
|
XSLT |
C# |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Memoization with |
|
|
|