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

FLWOR Expressions

FLWOR (say "flower") stands for the clauses for, let, where, order by and return. A FLWOR expression is a pipeline: each clause takes a stream of variable bindings and passes on a new stream, and return makes one result for each binding that reaches the end.

for, where, order by, return

Which requirements are recommendations, SHOULD or SHOULD NOT?

$ sxml xquery --context-item=vane.xml --method=adaptive --expression='
    for $requirement in //t[@anchor]
    where starts-with($requirement/bcp14, "SHOULD")
    order by $requirement/@anchor
    return `{$requirement/@anchor}: {$requirement/bcp14}`'
"req-10: SHOULD NOT"
"req-3: SHOULD"
"req-8: SHOULD"

for binds $requirement to each paragraph with an anchor in turn, where drops the ones whose keyword doesn't start with SHOULD, order by sorts the rest, and return gives a line for each. A FLWOR expression can be written across several lines; the shell keeps the line breaks inside the quotes.

The order is right for strings, and wrong for people: an anchor is text, and "req-10" comes before "req-3" as "10" comes before "3" in a dictionary. Sort by the number in the anchor instead:

$ sxml xquery --context-item=vane.xml --method=adaptive --expression='
    for $requirement in //t[@anchor]
    where starts-with($requirement/bcp14, "SHOULD")
    order by xs:integer(substring-after($requirement/@anchor, "req-"))
    return `{$requirement/@anchor}: {$requirement/bcp14}`'
"req-3: SHOULD"
"req-8: SHOULD"
"req-10: SHOULD NOT"

let

let binds a variable once for each binding that reaches it, without making more bindings. Which requirements are long?

$ sxml xquery --context-item=vane.xml --method=adaptive --expression='
    for $requirement in //t[@anchor]
    let $words := count(tokenize(normalize-space($requirement)))
    where $words > 15
    order by $words descending
    return `{$requirement/@anchor}: {$words} words`'
"req-6: 24 words"
"req-10: 22 words"
"req-4: 19 words"
"req-11: 17 words"
"req-3: 16 words"
"req-7: 16 words"

The string value of a paragraph is all of its text, the words inside bcp14 and xref included, so tokenize splits the whole sentence.

Positions

at binds a second variable to the position of each item. The requirements in document order, numbered as a reader of the draft sees them, with the section each one is in:

$ sxml xquery --context-item=vane.xml --method=adaptive --expression='
    for $requirement at $position in //t[@anchor]
    return `{$position}. {$requirement/@anchor} ({$requirement/ancestor::section[1]/name})`'
"1. req-1 (Flag States)"
"2. req-2 (Flag States)"
"3. req-3 (Flag States)"
"4. req-4 (Rollout)"
"5. req-5 (Rollout)"
"6. req-9 (Rollout)"
"7. req-6 (Status)"
"8. req-10 (Status)"
"9. req-7 (Pause)"
"10. req-8 (Pause)"
"11. req-11 (Pause)"

The anchors and the positions differ: an anchor is a name given once, when a requirement is written, and the later draft put req-9, req-10 and req-11 into sections that already had requirements. The number a reader sees is computed from the position, so it never has to be kept up to date by hand.

Joins

A FLWOR expression can bring things together that the document keeps apart. A term is defined once, as a dt with an anchor, and used many times, as an xref that targets that anchor. For each use, a let finds the definition:

$ sxml xquery --context-item=vane.xml --method=adaptive --expression='
    for $xref in //xref[starts-with(@target, "term-")]
    let $term := //dt[@anchor = $xref/@target]
    return `{$xref} -> {$term}, in {$xref/ancestor::*[@anchor][1]/@anchor}`'
"SDK -> SDK, in intro"
"flag -> Flag, in terminology"
"flag -> Flag, in req-1"
"percentage -> Percentage, in req-4"
"pause -> Pause, in req-5"
"pause -> Pause, in req-7"

ancestor::*[@anchor][1] is the nearest enclosing element with an anchor: a requirement, or else a section.

A join can bring two documents together as well. doc("vane-00.xml") reads the earlier draft. Anchors don't change between versions, so a requirement of the new draft finds its earlier version by its anchor, and comparing the text tells what changed:

$ sxml xquery --context-item=vane.xml --method=adaptive --expression='
    for $new in //t[@anchor]
    let $old := doc("vane-00.xml")//t[@anchor = $new/@anchor]
    where normalize-space($old) ne normalize-space($new)
    return `{$new/@anchor}: {if (empty($old)) then "added" else "changed"}`'
"req-9: added"
"req-10: added"
"req-7: changed"
"req-8: changed"
"req-11: added"

For an added requirement $old is empty, its string value is the empty string, and ne finds it different too. normalize-space makes the comparison ignore how the lines are broken. A requirement that the new draft removed would not be found this way, as the loop runs over the new draft only; the project at the end of the chapter looks in both directions.

doc reads a document by its URI. The sxml command resolves a relative URI against the working directory, so vane-00.xml is the file in the directory you run the command from. Files and the Outside World explains how files are found.

group by

group by gathers the bindings that have the same key into one. After it, the grouping variable holds the key, and every other variable holds all the values of its group:

$ sxml xquery --context-item=vane.xml --method=adaptive --expression='
    for $requirement in //t[@anchor]
    group by $section := string($requirement/ancestor::section[1]/name)
    return $section || ": " || count($requirement)'
"Flag States: 3"
"Rollout: 3"
"Status: 2"
"Pause: 3"

Before group by, $requirement is one paragraph. After it, $requirement is the sequence of all the paragraphs of a section, so count($requirement) counts them. The groups come in the order their first members had. Grouping goes further.

count

count numbers the bindings at that point of the pipeline, which makes ranking easy. Now keywords.xq from the start of the chapter reads clearly: group the requirements by keyword, count each group, sort by the count and then the keyword, number the groups, and keep the first three.

for $requirement in //t[@anchor]
group by $keyword := string($requirement/bcp14)
let $count := count($requirement)
order by $count descending, $keyword
count $rank
where $rank le 3
return `{$rank}. {$keyword} ({$count}): {string-join($requirement/@anchor, ", ")}`

where after count filters by the number that count gave. Before the order by, the number would be the position in document order instead.

while

while ends the pipeline at the first binding for which its condition is false, unlike where, which only skips it. Which requirements come before the first SHOULD?

$ sxml xquery --context-item=vane.xml --method=adaptive --expression='
    for $requirement in //t[bcp14]
    while $requirement/bcp14 ne "SHOULD"
    return string($requirement/@anchor)'
"req-1"
"req-2"

req-3 is the first SHOULD, and nothing after it is looked at, though req-4 is a MUST again. With where, the result would have every requirement except the SHOULDs.

More clauses

FLWOR expressions have two more tools: group by with several keys and computed keys, in the next section, and window clauses, which bind a variable to runs of consecutive items. Windows are taught on trace data, where runs of neighbouring calls are the whole point, in Who Calls Whom, and How Often.