List comprehensions

A list comprehension is a syntactic sugar construct for filtering and mapping sequences, vectors and series. They simplify writing lambda functions for methods, and they are easier to read and understand.

Syntax

Suppose you need to write a formula like this one:

Austra
seq(1, 100).filter(x => x.odd).map(x => x^2)

It is not a candidate for the Turing Award: it takes the squares of all odd numbers between 0 and 100. You had to type two lambda functions, including arrows and lambda parameters, and you also had to explicitly mention the filter and the map methods, including the parentheses enclosing their arguments.

This alternative expression does the same, is shorter to type and easier to read:

Austra
[x <- seq(1, 100) : x.odd => x^2]

With this trick, we have avoided repeating the declaration of the parameter x in the two lambda definitions used in the expression.

In this example, since the source of all numbers is a range sequence, you could also use a simpler expression for the range:

Austra
[x <- 1..100 : x.odd => x^2]

The syntax for this construct can be summarized like this:

Austra
[identifier <- generator : filter => mapping]

Both filter and mapping are optional:

Austra
-- This expression...
[x <- 1..100];
-- ... is equivalent to this one:
seq(1, 100)

Types in list comprehensions

The type assigned to the whole list comprehension expression is the same of its generator. You can keep applying methods or operators to the result:

Austra
[x <- 1..100 : x % 2 = 1 => x^2].sortDesc;
[x <- 1..100] .* ([x <- 1..100] + 1)

Special care is needed when the generator is a time series, because the identifier in the head of the list comprehension is typed as double in the mapping section, but it is a Point<Date> in the filter section:

Austra
let mean = msft.mean in
  [x <- msft : x.date >= jan2015 => x - mean]

Generators

As we have seen, range expressions can be used as generators. We support four variants of range expressions inside list comprehensions:

Austra
-- Equivalent to iseq(1, 100)
[x <- 1..100];
-- Equivalent to seq(1, 100)
[x <- 1.0..100.0];
-- Even integers from 0 to 100.
[x <- 0..2..100];
-- The same as seq(0, 1024, 2 * pi)).
[x <- 0..1024..2pi];

In the last example, only the upper bound is real, so the compiler handles the generator as a real sequence.

The parameter identifier and the membership operator can also be drop, and the above examples simplify this way:

Austra
[1..100];
[1.0..100.0];
[0..2..100];
[0..1024..2pi];

We have mostly used constants for the range generators so far but, of course, each part of the generator could be an expression:

Austra
[x <- pi - 1..32 * 32..sqrt(200)];

Quantifiers in list comprehensions

Logical quantifiers can be used at the beginning of a list comprehension. The allowed quantifiers are all and any, as the corresponding methods in vectors and sequences. They are no keywords, but used at the beginning of a list comprehension, they are considered contextual keywords for syntax highlighting.

This is a very simple example of a quantifier in a list comprehension expression and its equivalent form using methods:

Austra
[any x <- 10..100 : x * x = x + x];
iseq(10, 100).any(x => x * x = x + x)

Both expressions are compiled as Boolean expressions. Note that a qualified list comprehension does not allow a mapping section.

The quantified list comprehension is marginally shorter than a call to any or all. Why, then, do we bother supporting this syntax? The reason is that we can embed a qualified predicate inside a normal list comprehension:

Austra
-- Find all prime numbers between 2 and 100:
[x <- 2..100 : all div in 2 .. x - 1 : x % div != 0];
-- Equivalent, but longer:
iseq(2, 100).filter(x => iseq(2, x - 1).all(div => x % div != 0))

We need no inner brackets inside the main list comprehension, since it is evident how the qualified condition is nested. We could even add a mapping at the end of the comprehension to transform the calculated prime numbers, if required.

If we use regular lambdas, we will be nesting a lambda definition inside another. The generated code for the list comprehension also use nested lambdas, but with an easier to understand syntax. The inner lambda is "capturing" the parameter of the outer lambda, so we must be careful naming local variables.

The mathematical symbols and are also accepted as synonyms of all and any:

Austra
[∃x <- 10..100 : x * x = x + x];
[x < 2..100 : ∀y < 2 .. x - 1 : x % y != 0];

These symbols can be typed by pressing CtrlQ+A or CtrlQ+E in the Code Editor.

See Also