Collections
NQE provides three types for working with collections of values.
| Type | Description |
|---|---|
| Bag | An unordered collection of elements where elements may appear any number of times. |
| List | Like Bag, but the elements are arranged in a specific order. |
| Set | Like Bag, but each element can appear only once. In other words, an unordered collections of elements where multiplicity does not matter. |
While Bag and List are generic datatypes that support any type of element, the Set datatype is currently focused on representing sets of IP addresses. Bags and Lists support iteration, but Sets do not. This remainder of this guide focuses only on Bags and Lists. See the guide for Sets for more information on Sets.
The primary difference between Bags and Lists is that Lists are ordered collections, whereas bags are unordered. Ordered collections have special operations that make use of the fact that the collection is a sequence of values. Examples include list indexing and limit.
The following query retrieves all devices, sorts them by name, and limits the results to the first 10:
foreach device in network.devices
select { Name: device.name, Platform: device.platform }
order by Name ascending
limit 10
NQE 25.11 introduced stricter typing for Bags and Lists that may affect existing queries. If you have queries written before 25.11, please review the Breaking Changes and Migration section.
Table of Contents
- Breaking Changes and Migration
- Creating Collections
- Converting Between Bags and Lists
- List Indexing
- Ordering
- Limiting Results
- Operations
- Extracting Single Elements
- Deduplication
- List-Specific Data Model Fields
- See Also
Breaking Changes and Migration
Changes in NQE 25.11
NQE 25.11 introduced stricter typing rules for Bags and Lists that may affect existing queries. In particular, some collection-valued expressions are now typed as Bags rather than Lists. This section outlines the key changes and how to migrate your code.
Bag/List Comparisons Generate Warnings
Comparing a Bag with a List using equality (==, !=) or relational operators (<, <=, >, >=) is allowed but
discouraged ─since List comparisons care about the positional order of elements, while Bag comparisons are
order-insensitive. Such comparisons will generate warnings, and you should fix them with one of the suggested actions.
When you compare a Bag with a List, the List is implicitly converted to a Bag (losing its order), and then Bag comparison semantics are used. This can lead to unexpected results if you intended to compare order-sensitive Lists.
Example of changed behavior:
Before 25.11: This worked without any issues.
deviceNames = foreach device in network.devices select device.name;
{[
isEqual: deviceNames == ["device1", "device2"]
]}
Here, isEqual is true precisely when there are two devices, the very first is named "device1" and the very last is
named "device2".
After 25.11: Square brackets “[…]” are typed as Lists. As such, comparing a List with a Bag uses Bag comparison semantics (order-insensitive), which could give you unexpected results. As such, you will get a warning.
deviceNames = foreach device in network.devices select device.name; // This is now a Bag of strings
{[
isEqual: deviceNames == ["device1", "device2"]
]}
Here, isEqual is true precisely when there are two devices, one is named "device1" and the other is named
"device2", in no specified order.
Migration strategy: Make both operands the same type!
Option 1: If order matters then explicitly use order by to get a List.
// Use “order by” clause to sort the devices alphabetically by name
deviceNames = foreach device in network.devices
let name = device.name
select name
order by name;
{[
isEqual: deviceNames == ["device1", "device2"]
]}
Option 2: If order does not matter then use bag to get a Bag.
deviceNames = foreach device in network.devices select device.name; // This is a Bag of strings
// Ensure the comparison is done against two bags.
{[
isEqual: deviceNames == bag(["device1", "device2"])
]}
Creating Collections
There are several ways to construct Bags and Lists in NQE.
Literals
The simplest way to create a list collection is by writing a list literal, a comma-separated sequence of expressions within brackets:
numbers = [1, 2, 3, 4, 5];
names = ["router1", "router2", "router3"];
mixed = [[10, 20], [30, 40]]; // list of lists
The Bag function
We can wrap a list literal with the bag method to create a bag collection.
integers = bag([1, 2, 3, 4, 5]); // a bag of integers
Since bags ignore order, the following expression is true:
bag([1, 2, 1]) == bag([2, 1, 1])
Comprehensions
Collections can be created using foreach ... select ... expressions, also known as
comprehensions.
A comprehension evaluates to a Bag if the source collection is a Bag (and there is no order by clause).
// When iterating over a Bag (like network.devices), the result is a Bag
deviceNames = foreach device in network.devices select device.name; // Bag<String>
A comprehension evaluates to a List if the source collection is a List (or if there is an order by clause).
// When iterating over a List, the result is a List
numbers = [1, 2, 3, 4]; // List<Integer>
doubled = foreach n in numbers select n * 2; // List<Integer>: [2, 4, 6, 8]
Concatenation
Collections can be combined using the + operator, which concatenates two collections: It preserves order if both
collections are lists, and otherwise produces a bag.
list1 = [1, 2, 3];
list2 = [4, 5, 6];
combined = list1 + list2; // == [1, 2, 3, 4, 5, 6]
Combining Bags and Lists results in a bag:
myList = [1, 2, 3];
myBag = bag([4, 5, 6]);
combined = myList + myBag; // == bag([1, 2, 3, 4, 5, 6])
// == bag([2, 4, 6, 5, 3, 1])
Difference
The - operator removes all elements from the first collection that appear in the second one. The resulting difference
is an ordered collection exactly when the first argument is ordered.
Removing from a bag results in a bag:
all = bag([1, 2, 3, 4, 5, 4]);
exclude = [2, 4];
difference = all - exclude; // == bag([1, 3, 5])
// == bag([5, 1, 3])
Removing from a list results in a list:
all = [1, 2, 3, 4, 5, 4];
exclude = bag([2, 4]);
difference = all - exclude; // == [1, 3, 5]
Notice that the item 4 is duplicated in all, however the resulting difference removes all occurances.
Builtins That Return Collections
Many builtin functions return collections as their result. For example:
- fromTo(start, ebd): Returns a list of numbers from
starttoend(inclusive). - regexMatches(text, regex): Returns a list of all regex matches found in the text
- splitJsonObjects(json): Returns a list of JSON object strings
- patternMatches(config, pattern): Returns a bag of pattern matches from configuration
- blockMatches(config, pattern): Returns a bag of block matches from configuration
See the standard library reference for more functions that return collections.
Converting Between Bags and Lists
bag Function
Convert a List to a Bag using the bag(list) function:
orderedDevices = [/* ... */]; // List
deviceSet = bag(orderedDevices); // Bag
This is useful to make explicit the implicit downcasts when using ==, !=, <, <=, =>, or > with a Bag and a
List, which produce a type check warning and will eventually produce a type check error. See
Comparing Bags and Lists for more details.
Lists are subtypes of Bags
Since a list is just a bag with a specified order, a list can be used anywhere a bag is expected ---we just "forget" the ordering of the list to obtain a bag.
If-Then-Else: When branches return different types:
aList = [1, 2, 3]; // List<Integer>
aBag = bag([3, 4, 5]); // Bag<Integer>
condition = false;
combined = if condition then aList else aBag; // Bag<Integer>
User-Defined Functions: When a function expects a Bag parameter but receives a List:
processDevices(devices: Bag<Device>) : Integer = length(devices);
[{ useCase1: processDevices(["Device 1", "Device 2"])
, useCase2: processDevices(bag(["D 10", "D 20"]))
}]
Notice that useCase1 works since we automatically get to use processDevices as if it were typed
processDevices(devices: List<Device>) : Integer as well ---since List is a subtype of Bag.
List Indexing
Lists support zero-based indexing to access individual elements. Use square brackets with an integer index to retrieve an element at a specific position.
numbers = [10, 20, 30, 40, 50];
first = numbers[0]; // 10
second = numbers[1]; // 20
last = numbers[4]; // 50
When an index is out of bounds (negative or greater than or equal to the list length), a runtime error is emitted.
numbers = [10, 20, 30];
tooLarge = numbers[10]; // 🚫 Error: Index too large
negative = numbers[-1]; // 🚫 Error: Index must be non-negative
Notes:
- List indexing only works with Lists, not Bags (bags are unordered and don't support indexing)
- Indices must be integer values
- Attempting to index a bag results in a type error
- Out-of-bounds access results in a runtime error
Ordering
One of the most powerful features of Lists is the ability to specify and maintian order.
The order by Clause
The order by clause sorts a comprehension by one or more expressions, resulting in an ordered collection.
1. foreach device in network.devices
2. select { Name: device.name, Platform: device.platform }
3. order by Name asc
Notice that line (3) orders all records specified in line (2) by the Name field in ascending order.
To order by names in descending order, we can use the desc keyword:
1. foreach device in network.devices
2. select { Name: device.name, Platform: device.platform }
3. order by Name desc
Instead of asc and desc, for readability you can use ascending and descending to specify sorting direction.
1. foreach device in network.devices
2. select { Name: device.name, Platform: device.platform }
3. order by Name ascending
You can sort by multiple columns: Line (3) below orders by the Platform field of each record, specified in line (2),
in descending order and then orders by the Name field of each record in ascending order.
1. foreach device in network.devices
2. select { Name: device.name, Platform: device.platform }
3. order by Platform desc, Name asc
Instead of asc and desc, for readability you can use ascending and descending to specify sorting direction.
1. foreach device in network.devices
2. select { Name: device.name, Platform: device.platform }
3. order by Platform descending, Name ascending
Finally, you can even sort by local let variables:
/** 😲 Results are ordered by platform ascending,
* then by model descending, then by name;
* however, the results only show the name and platform. */
foreach device in network.devices
let Platform = device.platform
// 🔽️ This is used for sorting only & not shown in the rows.
let Model = Platform.model
select {
Name: device.name,
Platform
}
order by Platform asc, Model desc, Name ascending
Important: The order by clause requires each ordering expression to be a name that refers to either:
- A field of the record returned by the
selectclause, or - A variable defined in a
letclause
You cannot use arbitrary expressions directly in order by. If you need to sort by a computed value, bind it to a name
using let first.
bad = foreach device in network.devices
select device
order by device.name; // 🚫ERROR: “order by” clause expects an identifier, not an expression
works = foreach device in network.devices
let name = device.name
select device
order by name; // 🟢 Works
The orderBy Function
To sort collections directly, use the orderBy(collection, keySelector) function:
getDeviceName(device) = device.name;
devicesSortedByName = orderBy(network.devices, getDeviceName);
For descending order, negate numeric keys or use the comprehension syntax.
The order Function
To sort a collection by its natural order without specifying a key selector, use the order(collection) function:
devices = order(network.devices);
The order function sorts elements using their natural ordering - the same ordering used by comparison operators like
< and >. See Comparisons for details on how different types are ordered.
This is equivalent to the following comprehension:
devices = foreach device in network.devices
select device
order by device ascending;
The order function always returns a List in ascending order. For descending order, use the comprehension syntax with
order by ... descending.
Limiting Results
Basic Use
The limit clause restricts the number of elements returned from a list:
foreach device in network.devices
select { Name: device.name }
order by Name ascending
limit 10 // Only first 10 devices
Without An order by Clause
While limit is commonly used with order by to get the "top N" results, it can be used without order by as long as
the comprehension produces a list. For example, iterating over an already-ordered list will allow limit to work
without requiring order by. For instance, if you already have your devices ordered, then you can do:
devicesList = ["device1", "device2", "device3"];
foreach device in devicesList
select { device }
limit 2 // No “order by” clause needed since “devicesList” is a list
Limiting by an expression
The limit value can be any expression that evaluates to a number, not just a numeric literal. For example, the following query gets half of the possible devices.
foreach device in network.devices
select { Name: device.name }
order by Name ascending
limit length(network.devices) / 2
Note: The order by and limit clauses can appear in nested queries, not just at the outermost level. This allows
you to sort and limit intermediate results within a larger query.
The limit function
The limit function can also be applied directly to collections:
getDeviceName(device) = device.name;
devicesSortedByName = orderBy(network.devices, getDeviceName);
firstFiveDevicesByName = limit(devicesSortedByName, 5);
Operations
Membership Testing
Test whether an element is in a collection using in or not in:
[{ present: "Jasim" in ["Jane", "Jasim", "Jerry"] // = true
, notPresent: 46 not in bag([3, 7, 365]) // = true
}]
This can be used in comprehensions to filter out results:
invalidDevices = ["Device1", "Device5", "Device9"];
foreach device in network.devices
let Name = device.name
where Name not in invalidDevices
select { Name }
Comparison Operators
Collections can be compared using <, <=, >, >= operators.
- Lists are compared lexicographically (element by element).
- Bags are compared by sorting them (using the natural order) then doing list comparisons.
[{ true1: [1, 2, 3] < [1, 2, 4] // = true since 3 < 4 at first differing position
, true2: [1, 2] < [1, 2, 4] // = true since first list is a prefix of the second list
, true3: bag([2, 1, 1]) < bag([1, 3, 1]) // = true since [1, 1, 2] < [1, 1, 3] is true
}]
Equality
Lists are equal if and only if they have the same elements in the same order:
[1, 2, 3] == [1, 2, 3] // = true
[1, 2, 3] == [3, 2, 1] // = false; different order
[1, 2, 3] == [1, 2, 3, 4] // = false; different length
Bag equality is order-independent but cares about multiplicity:
bag([1, 2, 3]) == bag([3, 2, 1]) // = true; same elements, order ignored
bag([1, 2]) == bag([2, 1]) // = true; same elements, order ignored
bag([1, 2]) == bag([2, 1, 1]) // = false; “1” occurs more times in the right than in the left
Comparing Bags and Lists
Comparing a Bag with a List using equality or relational operators is allowed but generates a warning. When comparing a Bag with a List, the List is implicitly converted to a Bag (losing order information), and Bag comparison semantics are applied.
myList = [1, 2, 3];
myBag = bag([3, 2, 1]);
myList == myBag // ⚠️ Warning: comparing List with Bag
// Result: true (order is ignored, both become bags for comparison)
To avoid warnings and make your intent clear, convert both operands to the same type:
// If order matters
myList == order(myBag) // No warning; false since [1, 2, 3] != [3, 2, 1]
// If order doesn't matter
bag(myList) == myBag // No warning; true
For more details about language changes that may affect existing queries, see Breaking Changes and Migration.
Extracting Single Elements
Use the(collection) to extract a single element from a collection. This function returns:
- The single element if the collection contains exactly one element
nullif the collection is empty or contains more than one element
// The device named "core-router-1", if present; else null
coreRouter1Device = the(
foreach device in network.devices
where device.name == "core-router-1"
select device
);
This is particularly useful when you expect a unique result but want to handle the case gracefully when the assumption doesn't hold.
Deduplication
Remove duplicate elements from a collection using distinct. For lists, this preserves order: The first occurance of an element is kept and all other subsequent occurances are dropped.
list = [1, 2, 3, 1, 4, 2];
uniqueList = distinct(list); // [1, 2, 3, 4]
bag = bag([1, 2, 3, 1, 4, 2]);
uniqueBag = distinct(bag); // == bag([1, 2, 3, 4])
// == bag([2, 4, 3, 1])
List-Specific Data Model Fields
Several data model fields return Lists instead of Bags to reflect their inherent ordering. Examples include:
Device.aclEntriesandDevice.natEntriesIfaceAcls.inboundAclNamesandIfaceAcls.outboundAclNamesNetworkAcl.ingressRulesandNetworkAcl.egressRulesSecurityGroup.ingressRulesandSecurityGroup.egressRulesLoadBalancer.loadBalancerRulesandLoadBalancer.outboundRules
See Also
Guides
- Comprehensions - In-depth coverage of foreach expressions
- Comparisons - How values are ordered and compared
Functions
The following operations are available on collections.
-
fromTo(start, end) Creates a range of numbers
[start, start + 1, start + 2, ..., end]. -
bag(list): Converts a list to a bag
-
length(collection): Gets the number of elements in a collection
-
limit(list, n): Gets the first
nelements from a list -
item in collection: Checks that
itemis an element ofcollection. For example,"b" in ["a", "b"]returns true. -
item not in collection: Checks that
itemis not an element ofcollection. -
distinct(collection): Removes duplicates from a collection.
-
orderBy(collection, keySelector): Sorts a collection by applying the key selector function to each element. The result is a list.
- This can be written as a comprehension
orderBy(collection, selector)
=
foreach item in collection
let key = selector(item)
select item order by key ascending - Often, we want to order a collection by the natural order on elements; that is, the same order used for comparing
items with relational expressions such as less-than (
<) and greater-than (>). See Section comparisons for more detail on the order of values. This is done by order(collection) ---no selector function required.
- This can be written as a comprehension
-
max(collection): Gets the maximum element in a collection, or
nullif the collection is empty.- Note:
max(collection)is the last element oforder(collection).
- Note:
-
min(collection): Gets the minimum element, or
nullif the collection is empty.- Note:
min(collection) = order(collection)[0]
- Note:
-
maxBy(collection, keySelector): Gets the element with the maximum key value, or
nullif the collection is empty.- This gets the greatest item in the
collection, where items are ordered by the value returned by applyingkeySelectorto each item. - Note:
maxBy(collection, keySelector)is the last element oforderBy(collection, keySelector).
- This gets the greatest item in the
-
minBy(collection, keySelector): Gets the element with minimum key value, or
nullif the collection is empty.- This gets the least item in the
collection, where items are ordered by the value returned by applyingkeySelectorto each item. - Note:
minBy(collection, keySelector) = orderBy(collection, keySelector)[0].
- This gets the least item in the
-
the(collection): Extracts the single element from a collection, if it has a unique element; else
null. -
sum(collection): Sums the numeric elements in a given collection.
-
join(delimiter, collection): Concatenates together the string elements in
collectionusingdelimiterbetween each element. -
any(collection): Checks if any boolean element is true
-
all(collection): Checks if all boolean elements are true