Tricks with iterable objects

Now that Ceylon has full support for mixin inheritance, we've been able to fill in all the interesting operations of the Iterable interface defined in ceylon.language. You can see the definition of Iterable here. (But be aware that, like the rest of the language module, the actual real implementation is in Java and JavaScript. The Ceylon definition is never actually compiled.)

Mapping and filtering an Iterable object

Of course, Iterable has the famous functions map(), and filter(). You can call them like this:

value filtered = "Hello Word".filter((Character c) c.uppercase);
value mapped = "Hello Word".map((Character c) c.uppercased);

(This works because a String is an Iterable<Character>.)

These operations each return Iterable—in this case, Iterable<Character>—so they don't actually allocate memory (except for a single object instantiation). If you want to actually get a new String, you need to call a function to do that:

print(string(filtered...)); //prints "HW"
print(string(mapped...)); //prints "HELLO WORLD"

As an aside, we think this is the right approach. I understand that some folks think it's better that calling filter() on a String results in a String, or that filter()ing a Set results in a Set, but I think it's quite common that the resulting container type should not be the same as the original container type. For example, it's unclear that calling map() on a Set should result in a Set, and it's certainly not correct that map()ing a String results in another String, or that map()ing a Map always results in another Map.)

Now, map() and filter() have their uses, I suppose, but in fact they're not the usual way to do mapping and filtering in Ceylon. We would really write the above code like this:

print(string(for (c in "Hello Word") if (c.uppercase) c)); //prints "HW"
print(string(for (c in "Hello Word") c.uppercased)); //prints "HELLO WORLD"

Likewise, Iterable has the methods any() and every(), but it's still usually more convenient and idiomatic to use comprehensions:

value allLowercase = every(for (c in "Hello Word") c.lowercase); //false
value someUppercase = any(for (c in "Hello Word") c.uppercase); //true

More operations of Iterable

However, there are some really useful methods of Iterable. First, find() and findLast():

value char = "Hello Word".find((Character c) c>`l`); //`o`

We can write this using a comprehension, but to be honest in this case it's slightly less ergonomic:

value char = first(for (c in "Hello World") if (c>`l`) c);

Next, sorted():

value sorted = "Hello World".sorted(byIncreasing((Character c) c.uppercased)); 
        //" deHllloorW"

Finally, fold():

value string = "Hello World".fold("", 
        (String s, Character c) 
            s.empty then c.string 
                    else s + c.string + " ");
        //"H e l l o  W o r l d"

value list = "Hello World".fold({}, 
        (Character[] seq, Character c) 
            c.letter then append(seq,c) 
                     else seq); 
        //{ H, e, l, l, o, W, o, r, l, d }

There's also two very useful attributes declared by Iterable. coalesced produces an iterable object containing the non-null elements:

value letters = { "Hello World".map((Character c) c.letter 
        then c.uppercased).coalesced... };
        //{ H, E, L, L, O, W, O, R, L, D }

The indexed attribute produces an iterable object containing the elements indexed by their position in the stream:

value entries = { "Hello World".indexed... }; 
        //{ 0->H, 1->e, 2->l, 3->l, 4->o, 5-> , 6->W, 7->o, 8->r, 9->l, 10->d }

It's quite interesting to see the declaration of these operations. For example:

shared default Iterable<Element&Object> coalesced {
    return elements(for (e in this) if (exists e) e);
}

Notice how the use of the intersection type Element&Object eliminates the need for a type parameter with a lower bound type constraint, which would be required to write down the signature of this operation in most other languages. Indeed, we use the same trick for the operations union() and intersection() of Set, as you can see here.

Set union and intersection

We let you write the union of two Sets as s|t in Ceylon, and the intersection of two Sets as s&t. Now check this out:

Set<String> strings = ... ;
Set<Integer> ints = ... ;
value stringsAndInts = strings|ints; //type Set<String|Integer>
value stringsAndInts = strings&ints; //type Set<Bottom>, since Integer&String is an empty type

That is, the type of the union of a Set<X> with a Set<Y> is Set<X|Y> and the type of the intersection of a Set<X> with a Set<Y> is Set<X&Y>. Cool, huh?

By the way, we just added similar methods withLeading() and withTrailing() to List:

value floatsAndInts = { 1, 2, 3.0 }; //type Sequence<Integer|Float>
value stuff = floatsAndInts.withTrailing("hello", "world"); //type Sequence<Integer|Float|String>

(These operations didn't make it into M3.1.)

min() and max()

Well, here's something else that's cool. Ceylon distinguishes between empty and nonempty sequences within the type system. An empty sequence is of type Empty, a List<Bottom>. A non-empty sequence is a Sequence<Element>,a List<Element>. (There's even a convenient if (nonempty) construct for narrowing a sequence to a nonempty sequence without too much ceremony.)

This lets us do something pretty cool with the signature of min() and max().

value nothingToMax = max({}); //type Nothing
value somethingToMax = max({0.0, 1.0, -1.0}); //type Float
List<Character> chars = "hello";
value maybeSomethingToMax = max(chars); //type Character?

That is, according to its signature, the max() function returns:

  • null when passed an empty sequence,
  • a non-null value when passed a nonempty sequence, or
  • a possibly-null value when passed an iterable object that we don't know is empty or nonempty.

It's important to point out that this is not some special feature built in as a special case in the type system. Nor is it in any way related to overloading. Indeed, until a couple of weeks ago I didn't even realize that this was possible. It's rather a demonstration of how expressive our type system is: in this case, the combination of intersection types, union types, and principal instantiation inheritance of generic types lets us express something within the type system that might seem almost magical. The real magic is in the declaration of the language module type ContainerWithFirstElement.