Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.