If you are familiar with Lisp or Scheme, you know that a function call can replace a variable binding, and function calls can also replace most loops. This is also true in XQuery.
In XQuery this leads to a style of coding that I call "let-free". In this style, there are no FLWOR expressions. Really this is "FLWOR-free", not "let-free", but that's too much of a mouthful for me.
But why would you write let-free code?
The answer is scalability - you knew it would be, right?
This breaks out into concurrency and streaming.
Let's talk about concurrency first.
In the MarkLogic Server implementation of XQuery,
let is evaluated in sequence. However, other expressions
are evaluated lazily with concurrency-friendly "future values".
So a performance-critical single-threaded request can sometimes
benefit from let-free style. You can see this technique in use
in some of my code:
the semantic library
or the task-server forest rebalancer.
Both of these projects try to benefit from multi-core CPUs.
The let-free style can also help with query scalability
by allowing the results to stream,
rather than buffering the entire result sequence.
If you need to export large result sets, for example,
this technique can help avoid
Those errors result when your query's working set is too large
to fit in the expanded tree cache, a sort of scratch space for XML trees.
But streaming results don't have to fit into the cache.
For example, let's suppose you need to list every document URI in the database. But you do not have the URI lexicon enabled, and you cannot reindex to create it.
Note that nested evaluations cannot stream, either. So even a let-free query may throw XDMP-EXPNTREECACHEFULL in cq or another development tool. To test this query, use an http module instead. This is ideal for web service implementations too.
In this example we used function mapping, a MarkLogic extension to XQuery 1.0. If a function takes a single argument but is called using a sequence, the evaluator simply maps the sequence to multiple function calls. This is somewhat faster than a FLWOR, and it can stream.
Besides using function mapping, let-free style can use XPath steps. However, this technique only works for sequences of nodes.
While these techniques are useful, they can make for code that is
hard to read and tricky to debug. Function mapping is especially prone to errors
that are difficult to diagnose. If a function signature specifies an argument
without a quantifier or with the
and the runtime argument is empty, the function will not be called at all.
This is surprising, since normally the function would be called
and would cause a strong typing error.
The first expression returns the empty sequence,
while the second throws the expected strong typing error
This behavior is annoying, but in some applications
the benefits of function mapping outweigh this drawback.
We can make debugging easier if we weaken the function signature
document-node()? so that the function will be called
even when the argument is empty. If needed, we can include an explicit check
for empty input too.
Another let-free trick is to use module variables.
These act much like
let bindings, but they can stream.
This example is a bit contrived, since the module variable doesn't add anything.
But if you find yourself struggling to refactor a
let as a function call
or an XPath step, consider using a module variable.
Module variables are also excellent tools for avoiding repeated work,
since the right-hand expression is evaluated lazily and is never
evaluated more than once. If the evaluation does not use the module variable,
then the right-hand expression is never evaluated.
In contrast, the right-expression of a
let is evaluated
even when the
return does not use its value.
As always, do not optimize code unless there is a problem to solve.
There are also some situations where the let-free style isn't appropriate.
Aside from making your code harder to read and more difficult to debug,
let-free style simply doesn't work in situations where your FLWOR
would have an
order by clause.
And after all, streaming won't work for that case anyway.
The evaluator can't sort the result set without buffering it first.