Tour of Ceylon: Comprehensions

This is the thirteenth stop in our Tour of Ceylon. In the previous section we looked at invoking functions using named arguments. We're now ready to learn about comprehensions.

Comprehensions

A comprehension is a convenient way to transform, filter, or combine a stream or streams of values before passing the result to a function. Comprehensions act upon, and produce, instances of Iterable. A comprehension may appear:

  • inside brackets, producing a sequence,
  • inside braces, producing an iterable, or
  • inside a named argument list.

The syntax for instantiating a sequence, that we met earlier is considered to have a parameter of type Iterable, so we can use a comprehension to build a sequence:

String[] names = [ for (p in people) p.name ]; 

But comprehensions aren't just useful for building sequences! Suppose we had a class HashMap, with the following signature:

class HashMap<Key,Item>({Key->Item*} entries) { ... }

Then we could construct a HashMap<String,Person> like this:

value peopleByName = HashMap { for (p in people) p.name->p };

As you've already guessed, the for clause of a comprehension works a bit like the for loop we met earlier. It takes each element of the Iterable stream in turn. But it does it lazily, when the receiving function actually iterates its argument!

This means that if the receiving function never actually needs to iterate the entire stream, the comprehension will never be fully evaluated. This is extremely useful for functions like every() and any():

if (every { for (p in people) p.age>=18 }) { ... }

The function every() (in ceylon.language) accepts a stream of Boolean values, and stops iterating the stream as soon as it encounters false in the stream.

If we just need to store the iterable stream somewhere, without evaluating any of its elements, we can use an iterable constructor expression, like this:

{String*} names = { for (p in people) p.name }; 

Now let's see what the various bits of a comprehension do.

Transformation

The first thing we can do with a comprehension is transform the elements of the stream using an expression to produce a new value for each element. This expression appears at the end of a comprehension. It's the thing that the resulting Iterable actually iterates!

For example, this comprehension

for (p in people) p.name->p

results in an Iterable<String->Person>. For each element of people, a new Entry<String,Person> is constructed by the -> operator.

Filtering

The if clause of a comprehension allows us to skip certain elements of the stream. This comprehension produces a stream of numbers which are divisible by 3.

for (i in 0..100) if (i%3==0) i

It's especially useful to filter using if (exists ...).

for (p in people) if (exists s=p.spouse) p->s

You can even use multiple if conditions:

for (p in people) 
        if (exists s=p.spouse, 
            nonempty inlaws=s.parents) 
                p->inlaws

Products and joins

A comprehension may have more than one for clause. This allows us to combine two streams to obtain a stream of values of their cartesian product:

for (i in 0..100) for (j in 0..10) Node(i,j)

Even more usefully, it lets us obtain a stream of associated values, a lot like a join in SQL.

for (o in orgs) for (e in o.employees) e.name

There's more...

Next we're going to discuss some of the basic types from the language module, in particular numeric types, and introduce the idea of operator polymorphism.

You can read more about working with iterable objects in Ceylon in this blog post.