Skip to content

Composition with logical operators

Queries we've performed so far operated on single, discrete expressions, like selecting an identifier, or an array whose elements match a specific type. While useful in their own, in reality the selection often needs to consider several dimensions of an expression.

For this purpose, SYNG supports three logical operators that can turn a single selector into a composition that is no longer confined to a single dimension:

clojure
(:and [selector...])
(:or  [selector...])
(:not [selector])
(:and [selector...])
(:or  [selector...])
(:not [selector])

Operators behave like other selectors and may appear anywhere a selector is allowed, unless otherwise noted.

The previous chapter explained how to select Array literals, let's revisit the queries found there and see how we can turn them into more precise selections.

Selecting an Array by multiple elements

Or, intersections.

Suppose we want to select an array that begins with a string, and also contains a numeric element? The (:and) operator allows us to combine multiple selectors to form a predicate that will match only when they all do:

clojure
(arr (:and (el 1 (str))
           (el _ (num))))
(arr (:and (el 1 (str))
           (el _ (num))))

In place of the (el) argument that we passed to (arr) before, we're now passing (:and) which in turn hosts two (el) selectors, and the query will match only if both (el) operands match. In other words, this will match ["a", 1] and ["a", null, 42] but neither ["a", {}] nor [1, "a"].

Selecting an Array by one or another element

Or, unions.

Instead of asking for an array that has both a string for the first element and a number at some other position, let's be a little more lenient and settle down for any of the two. The (:or) operator, similar to what you're used to in a programming context, forms a predicate that is true when any of its operands is true:

clojure
(arr (:or (el 3 (str))
          (el _ (num))))
(arr (:or (el 3 (str))
          (el _ (num))))

With this adjustment, the query will now match [{}, {}, "a"] because it has a string for its third element, and both of [1], [{}, 1] because they have a number element just somewhere, but none of these:

  • [], because it has neither a number nor a string for the third element, nor
  • [{}, "a"], because while it does have a string for an element, it's not the third one.

Ignoring an array altogether

Or, complements.

Last of the trio is (:not) which, as you'd expect, completely flips the equation on its head. Our previous query (arr (el _ (num))) was asking SYNG to find some array that has a numeric element, now what if we wanted to ignore arrays that have a numeric element instead?

To express this, the (:not) operator disqualifies an expression if its operand is successful in finding a match:

clojure
(arr (:not (el _ (num))))
(arr (:not (el _ (num))))

This will match [], ['a'] but not [1] nor ['a', 1]. Before we move on from (:not) though, can you think of what this query is looking for?

clojure
(arr (el _ (:not (num))))
(arr (el _ (:not (num))))

The position of (:not) was flipped where now it's an argument to (el), so what implication does this have?

Whereas the first query was looking for an array whose set of elements does not contain one that is a number, this second one is actually asking for an array whose set of elements contains some element that is not a number. Notation-wise, the difference might be subtle, but it's a big one!

Composed selection

By combining (:and) and (:not), we could form a predicate that is true when one part is true and another is false. Let's revisit our query so that it selects some array that has a string for the first element, but also has no numeric elements:

clojure
(arr (:and (el 1 (str))
           (:not (el _ (num)))))
(arr (:and (el 1 (str))
           (:not (el _ (num)))))

This will match ["a"] and ["a", {}] but neither [{}] nor ["a", 1].

Recap

This form of composition is allowed anywhere a selector may appear in a query because, after all, operators are selectors themselves. It's not exclusive to the(arr) selector, either. We used it as an example since it's the first of the selectors we encountered for which compositions are useful.

As a proof, and at the risk of angering your computer, consider running a query with an operator for a root selector:

clojure
(:not x) ; don't!
(:not x) ; don't!

Bad joke aside, composition is very powerful in forming elaborate queries. This will become more evident when we get to selecting function calls, member properties and even object literals -- our next topic.

Copyright © 2022-present Semantic Works, Inc.