Progress Acquires MarkLogic! Learn More

Introducing the simple mapping operator (!)

Back to blog
4 minute read
Back to blog
4 minute read

MarkLogic supports some of the coolest features of XQuery 3.0, including higher-order functions and switch expressions. This article describes one of my favorites: the simple mapping operator (!). If you like FLWOR expressions, you might just love the mapping operator.

One of the things that has always bugged me about XQuery (and this shows my XSLT background) is that you’re always having to bind variables, even if you don’t really have a good reason to. Here are some good reasons for binding variables:

  • re-use a value in more than one place
  • name a value to explain what’s going on
  • retain access to a value that would otherwise be out of scope

With FLWOR expressions, you’re forced to use a variable for each item you iterate over, whether or not the above reasons apply. In contrast, the “/” operator provides a sort of iteration as well, but it doesn’t require you to bind a variable each time. Instead, it makes use of the “context item” (.) for referring to each item being iterated over. In some cases, it gives you a shorter way of expressing what you might otherwise use a FLWOR expression for. For example, if you wanted to get the lower-case version of the string-value of each node in a sequence, you could write this FLWOR expression:

for $item in $my-nodes return lower-case($item)

Or you could just use a path expression like this:


Not only does this require less typing, but it requires less reading (and I find it easier to read). So “/” is a sort of mapping operator by itself. However, as soon as you get excited about using it this way, you soon find out that it’s quite limited. Let’s say you wanted to also remove extra whitespace in each string. You might be tempted to chain them together like this:

$my-nodes/lower-case(.)/normalize-space(.)  (: illegal :)

But this will result in an XDMP-NOTANODE error. Everything to the left of “/” has to be a node, which means you only get to use “/” once when the right-hand side returns simple values (instead of nodes). After that you have to nest your functions (rather than chain them):


That’s okay, but it would be much nicer to chain these, rather than have to switch back and forth between nesting and chaining. Another limitation of “/” is that it has two additional behaviors when returning nodes:

  • the result is sorted into document order, regardless of the input order, and;
  • all duplicate nodes are removed.

This means that not only is “/” very limited in what you can do with it, but it likely does other things that you wouldn’t want it to do were you to remove those limitations.

Okay, enough about the limitations of slash (/)! Let’s see what this operator (!) can do for us!

Let’s say you want to generate a list of numbers, each prefixed with “#”. You could always use a FLWOR expression:

for $n in (1 to 100) return concat("#", $n)

But now, you can use the simple mapping operator:

(1 to 100) ! concat("#", .)

Unlike “/”, you can you have any sequence to the left of “!”. The original order is preserved, just as with a FLWOR expression.

Here’s a real-world example (for combining multiple .js files into one) that can be rewritten much more nicely with the simple mapping operator:

/unparsed-text(resolve-uri(concat($js-relative-path-prefix,.), $base-uri))

Nested function calls like the above can be hard to read. In a sense, it reads backwards. When you nest function calls, what you do last (call unparsed-text()) comes first, and what you do first (call concat()) comes last. One way to make this more readable would be to introduce some variable definitions:

for $src in /html/head/script/@src
let $path := concat($js-relative-path-prefix,.)
let $resolved-uri := resolve-uri($path, $base-uri)
return unparsed-text($resolved-uri)

But now we’re back to defining variables just so we can avoid the awkward nesting, not necessarily because the variable names add any particular value. Without further adieu, let’s look at the rewritten expression using the simple mapping operator:

/html/head/script/@src ! concat($js-relative-path-prefix,.)
                       ! resolve-uri(., $base-uri)
                       ! unparsed-text(.)

Much nicer! Now it reads naturally. First you call concat(), then you call resolve-uri() against that result, and finally, you call unparsed-text() against that result.

Interestingly, you’ll note that ! replaces both “for” and “let” in the above example. The first sequence (/html/head/script/@src) potentially returns multiple items; that’s why “for” was necessary. For the sequences that contain only one item, “for” and “let” are effectively equivalent. The thing to remember is that “!” behaves like “for”: the right-hand side is evaluated once for every item in the sequence returned by the left-hand side.

Of course, FLWOR expressions are still necessary for various reasons. To name a few, you need them for sorting (order by), for local variable bindings (“let” expressions), and for joins (where you need to refer to two variables being iterated over, not just one).

With the simple mapping operator, you now have the option to write XQuery code in a very different style than you may be accustomed to. I encourage you to experiment with it, push the bar, and decide for yourself where it helps and where it might hinder the power and readability of your code.

Share this article

Read More

Related Posts

Like what you just read, here are a few more articles for you to check out or you can visit our blog overview page to see more.


Poker Fun with XQuery

In this post, we dive into building a full five-card draw poker game with a configurable number of players. Written in XQuery 1.0, along with MarkLogic extensions to the language, this game provides examples of some great programming capabilities, including usage of maps, recursions, random numbers, and side effects. Hopefully, we will show those new to XQuery a look at the language that they may not get to see in other tutorials or examples.

All Blog Articles

Protecting passwords in ml-gradle projects

If you are getting involved in a project using ml-gradle, this tip should come in handy if you are not allowed to put passwords (especially the admin password!) in plain text. Without this restriction, you may have multiple passwords in your file if there are multiple MarkLogic users that you need to configure. Instead of storing these passwords in, you can retrieve them from a location where they’re encrypted using a Gradle credentials plugin.

All Blog Articles

Getting Started with Apache Nifi: Migrating from Relational to MarkLogic

Apache NiFi introduces a code-free approach of migrating content directly from a relational database system into MarkLogic. Here we walk you through getting started with migrating data from a relational database into MarkLogic

All Blog Articles

Sign up for a Demo

Don’t waste time stitching together components. MarkLogic combines the power of a multi-model database, search, and semantic AI technology in a single platform with mastering, metadata management, government-grade security and more.

Request a Demo