Functions and Modules
As queries grow, you'll want to name the pieces. The prolog of a query
declares functions and variables, and a library module collects
declarations that several queries share. In this section the
specification project gets its module, spec.xqm, which the rest of the
chapter uses.
Declaring functions
A function declared in a query's prolog has a name with a prefix. For
your own functions in a main query, the prefix local is ready to use.
Declared functions can call themselves, which suits a document whose
sections nest. A table of contents lists each section, and after it the
table of contents of the sections inside it:
toc.xq:
declare option output:method "text";
declare option output:item-separator " ";
(:~ The table of contents of some sections and, after each one, of the
sections inside it. :)
declare function local:toc($sections as element(section)*, $prefix as xs:string) as xs:string* {
for $section at $position in $sections
let $number := $prefix || $position
return ($number || ". " || $section/name, local:toc($section/section, $number || "."))
};
local:toc(/rfc/middle/section, "")
$ sxml xquery --context-item=vane.xml toc.xq
1. Introduction
1.1. Terminology
2. Flag States
3. Messages
3.1. Rollout
3.2. Status
4. Pause
5. Security Considerations
6. IANA Considerations
The types in the signature are checked: local:toc takes sections and a
string, and gives strings. Passing a paragraph instead of a section
would be a type error at the call, not a wrong table later.
sxml evaluates function calls without growing the C stack, so deep recursion is limited only by the memory sxml may use, not by the stack of the program it runs in:
$ sxml xquery --method=adaptive --expression='
declare function local:count-down($n as xs:integer) as xs:integer {
if ($n eq 0) then 0 else local:count-down($n - 1)
};
local:count-down(100000)'
0
Library modules
A library module starts with module namespace, which names a prefix
and a namespace URI. It contains only declarations. This is the
project's module:
spec.xqm:
(:~ What the specification project knows about a draft in the xml2rfc
version 3 vocabulary: its requirements, their keywords, and the
numbers a reader sees. :)
module namespace spec = "urn:example:spec";
(:~ The keywords of BCP 14 (RFC 2119, RFC 8174), strongest first. :)
declare variable $spec:keywords as xs:string+ := (
"MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT",
"SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED",
"MAY", "OPTIONAL"
);
(:~ One line of the requirements index. :)
declare record spec:entry(
anchor as xs:string,
number as xs:integer,
section as xs:string,
keyword as xs:string,
text as xs:string
);
(:~ The requirements of a draft: the paragraphs with an anchor and a
keyword, in document order. :)
declare function spec:requirements($draft as document-node()) as element(t)* {
$draft//t[@anchor][bcp14]
};
(:~ The keywords of a paragraph, as BCP 14 writes them. :)
declare function spec:keywords($paragraph as element(t)) as xs:string* {
$paragraph/bcp14 ! normalize-space()
};
(:~ The number of a section as a reader sees it, such as "3.1". :)
declare function spec:section-number($section as element(section)) as xs:string {
string-join($section/ancestor-or-self::section ! (count(preceding-sibling::section) + 1), ".")
};
(:~ The number of a requirement as a reader sees it: its position among
the requirements of the draft. :)
declare function spec:requirement-number($requirement as element(t)) as xs:integer {
count($requirement/preceding::t[@anchor][bcp14]) + 1
};
(:~ Each anchor of a draft and the element that has it. :)
declare function spec:anchor-map($draft as document-node()) as map(xs:string, element()) {
map:merge($draft//*[@anchor] ! { string(@anchor): . })
};
(:~ The entry of the requirements index for a requirement. :)
declare function spec:entry($requirement as element(t)) as spec:entry {
let $section := spec:section($requirement)
return spec:entry(
anchor := string($requirement/@anchor),
number := spec:requirement-number($requirement),
section := spec:section-number($section),
keyword := string-join(spec:keywords($requirement), " "),
text := spec:text($requirement)
)
};
(:~ A line describing a requirement. :)
declare function spec:describe($requirement as element(t)) as xs:string {
let $entry := spec:entry($requirement)
return `{$entry?anchor} ({$entry?keyword}, section {$entry?section}): {$entry?text}`
};
(:~ The section a paragraph is in. :)
declare %private function spec:section($paragraph as element(t)) as element(section) {
$paragraph/ancestor::section[1]
};
(:~ The text of a paragraph on one line, without editorial notes, and
with the text a renderer gives an empty cross reference. :)
declare %private function spec:text($paragraph as element(t)) as xs:string {
normalize-space(string-join(
for $part in $paragraph/node() except $paragraph/cref
return
if ($part instance of element(xref) and empty($part/node()))
then spec:xref-text($part)
else string($part)
))
};
(:~ What an empty cross reference shows: "Section 4" or "[RFC6238]". :)
declare %private function spec:xref-text($xref as element(xref)) as xs:string {
let $target := root($xref)//*[@anchor = $xref/@target]
return
if ($target instance of element(section))
then "Section " || spec:section-number($target)
else "[" || $xref/@target || "]"
};
It knows what the rest of the project should not have to repeat: which
paragraphs are requirements, how sections and requirements are numbered,
and what a line of the requirements index holds, as the record type
spec:entry. A query imports it by its namespace, and at says where
the file is:
requirement.xq:
import module namespace spec = "urn:example:spec" at "spec.xqm";
declare option output:method "text";
declare variable $anchor as xs:string external := "req-7";
spec:describe(spec:anchor-map(/)($anchor))
$ sxml xquery --context-item=vane.xml requirement.xq
req-7 (MUST, section 4): After three failed checks in a row, a service MUST put the flag in a pause.
The sxml command reads the module from a local file, relative to the
working directory. Programs that embed sxml decide themselves where
modules come from, through a resolver (Files and the Outside
World).
Try the functions one by one with --expression; that's the quickest
way to see what a module does:
$ sxml xquery --context-item=vane.xml --method=adaptive --expression='
import module namespace spec = "urn:example:spec" at "spec.xqm";
spec:section-number(//section[@anchor = "status"]),
spec:requirement-number(//t[@anchor = "req-8"]),
map:size(spec:anchor-map(/)),
spec:entry(//t[@anchor = "req-9"])'
"3.2"
10
33
{"anchor":"req-9","number":6,"section":"3.1","keyword":"MAY","text":"A service MAY ask an owner for a time-based one-time password [RFC6238] before a rollout."}
The text of an entry is the paragraph on one line, with an editorial
note left out and an empty cross reference written as a renderer shows
it: <xref target="RFC6238"/> became "[RFC6238]".
External variables
requirement.xq declares $anchor as external, with a default value.
An external variable is a parameter of the whole query. --param sets
it:
$ sxml xquery --context-item=vane.xml --param=anchor=req-5 requirement.xq
req-5 (MUST NOT, section 3.1): A service MUST NOT roll out a flag while it is in a pause (Section 4).
When the type doesn't fit, the error says where:
$ sxml xquery --context-item=vane.xml --param=anchor=req-12 requirement.xq
requirement.xq:7:1: err:XPTY0004: The value does not match the required type (XPath 4.0, section 3.4, coercion rules)
$ sxml xquery --context-item=vane.xml --param=anchor=pause requirement.xq
requirement.xq:7:1: err:XPTY0004: The value does not match the required type (XPath 4.0, section 3.4, coercion rules)
There is no element with the anchor req-12, so the map lookup gives
the empty sequence; and pause is a section, not a paragraph.
spec:describe requires exactly one t element, and says so in both
cases.
Documents, Parameters and the Context has more on external variables and their types.
Typed functions catch mistakes
The signatures in spec.xqm say exactly what each function takes and
returns: element(t)* is any number of paragraphs, map(xs:string, element()) a map from strings to elements, spec:entry a record with
five fields of known types. They make a module self-documenting, and
they turn mistakes into clear errors at the place they happen instead of
wrong results later. In XQuery 4.0, parameters can also have default
values, and arguments can be passed by name, as spec:entry is called
inside the module:
$ sxml xquery --context-item=vane.xml --expression='
declare function local:label($section as element(section), $separator as xs:string := ".") {
string-join($section/ancestor-or-self::section ! (count(preceding-sibling::section) + 1), $separator)
};
local:label(//section[@anchor = "status"]), local:label(//section[@anchor = "status"], separator := "-")'
3.2 3-2
Private functions and module variables
A library module can declare variables as well as functions, and it can
keep helpers to itself. %private is an annotation: it makes a
function or variable visible only inside its own module. spec.xqm
keeps spec:section, spec:text and spec:xref-text private, and
offers $spec:keywords:
$ sxml xquery --method=adaptive --expression='
import module namespace spec = "urn:example:spec" at "spec.xqm";
count($spec:keywords), $spec:keywords[1]'
11
"MUST"
$ sxml xquery --method=adaptive --expression='
import module namespace spec = "urn:example:spec" at "spec.xqm";
spec:text(<t>A <bcp14>MUST</bcp14>.</t>)'
expression:3:5: err:XPST0017: no function with this name and arity is known
Keeping helpers private lets you change them later without breaking the
queries that import the module: how the text of a paragraph is made can
change, and spec:entry stays what it is.
Functions as values
A declared function is a value too. spec:requirement-number#1 is a
reference to the function with one parameter, which you can pass to a
higher-order function such as for-each:
$ sxml xquery --context-item=vane.xml --method=adaptive --expression='
import module namespace spec = "urn:example:spec" at "spec.xqm";
spec:requirements(/) => for-each(spec:requirement-number#1) => string-join(" ")'
"1 2 3 4 5 6 7 8 9 10 11"
Parameters can have function types, written fn(...) as .... This
function counts the requirements that pass a test it is given:
$ sxml xquery --context-item=vane.xml --method=adaptive --expression='
import module namespace spec = "urn:example:spec" at "spec.xqm";
declare function local:count-where($draft as document-node(), $test as fn(element(t)) as xs:boolean) as xs:integer {
count(spec:requirements($draft)[$test(.)])
};
local:count-where(/, fn($t) { spec:keywords($t) = "MUST" }),
local:count-where(/, fn($t) { contains($t, "owner") })'
5
3
Functions as Values explains inline functions, partial application and the higher-order functions of the library; all of it works in XQuery.
When an import fails
The namespace in import module must be the one that the module
declares, and the file must exist. Both mistakes are static errors,
found before the query runs:
$ sxml xquery --expression='
import module namespace spec = "urn:example:spec" at "specs.xqm";
1'
expression:2:5: err:XQST0059: no module is found for an imported namespace
$ sxml xquery --expression='
import module namespace spec = "urn:example:specification" at "spec.xqm";
1'
expression:2:5: err:XQST0059: the module found is not a library module of the imported namespace
An error inside a module is reported with the module's own file name,
line and column, so you know where to look. That holds for errors the
module raises as it runs, too: a paragraph that is in no section has no
section number, and spec:section promises one:
$ sxml xquery --expression='
import module namespace spec = "urn:example:spec" at "spec.xqm";
spec:describe(<t anchor="req-12">A service <bcp14>MAY</bcp14> log a rollout.</t>)'
spec.xqm:68:86: err:XPTY0004: The value does not match the required type (XPath 4.0, section 3.4, coercion rules)
Line 68 is the declaration of spec:section, and column 86 its result
type, element(section), which the empty result does not match.