Note: information on this page refers to Ceylon 1.0, not to the current release.
Iterables, sequences, and tuples
This is the sixth leg of the Tour of Ceylon. In the previous leg we covered anonymous classes and member classes. Now we're going to look at iterable objects, sequences, and tuples. These are examples of generic container objects. Don't worry, we'll come back to talk more about generics later.
Iterables
An iterable object is an object that produces a stream of values. Iterable
objects satisfy the interface
Iterable
.
Ceylon provides some syntax sugar for working with iterable objects:
- the type
Iterable<X,Null>
represents an iterable object that might not produce any values when it is iterated, and may be abbreviated{X*}
, and - the type
Iterable<X,Nothing>
represents an iterable object that always produces at least one value when it is iterated, and is usually abbreviated{X+}
.
We may construct an instance of Iterable
using braces:
{String+} words = { "hello", "world" };
{String+} moreWords = { "hola", "mundo", *words };
The prefix *
is called the spread operator. It "spreads" the values of an
iterable object. So moreWords
produces the values "hola", "mundo", "hello",
"world"
when iterated.
As we'll see later, the braces may even contain a comprehension, making them much more powerful than what you see here.
Iterable
is a subtype of the interface
Category
,
so we can use the in
operator to test if a value is produced by the
Iterable
.
if (exists char = text[i],
char in {',', '.', '!', '?', ';', ':'}) {
//...
}
"index must be between 1 and 100"
assert (index in 1..100);
The in
operator is just syntactic sugar for the method contains()
of
Category
.
Iterating using for
To iterate an instance of Iterable
, we can use a
for
loop:
for (word in moreWords) {
print(word);
}
(Note that in this context, the in
keyword isn't the operator we just met
above, it's just part of the syntax of the for
loop.)
If, for any reason, we need an index for each element produced by an iterable
object, we can use a special variation of the for
loop that is designed for
iterating Entry
s:
for (i -> word in entries(moreWords)) {
print("``i``: ``word``");
}
The entries()
function returns an instance of Entry<Integer,String>[]
containing the
indexed elements of the sequence. (The ->
is syntax sugar for the class
Entry
.)
It's often useful to be able to iterate two sequences at once. The
zip()
function comes in handy here:
for (name -> place in zip(names,places)) {
print(name + " @ " + place);
}
Sequences
Some kind of array or list construct is a universal feature of all programming
languages. The Ceylon language module defines support for sequence types via
the interfaces Sequential
,
Sequence
,
and Empty
.
Again, there is some syntax sugar associated with sequences:
- the type
Sequential<X>
represents a sequence that may be empty, and may be abbreviated[X*]
orX[]
, - the type
Sequence<X>
represents a nonempty sequence, and may be abbreviated[X+]
, and - the type
Empty
represents an empty sequence and is abbreviated[]
.
Some operations of the type Sequence
aren't defined by Sequential
, so you
can't call them if all you have is X[]
. Therefore, we need the
if (nonempty ... )
construct to gain access to these operations.
void printBounds(String[] strings) {
if (nonempty strings) {
//strings is of type [String+] here
print(strings.first + ".." + strings.last);
}
else {
print("Empty");
}
}
Notice how this is just a continuation of the
pattern established for
null
value handling. In fact, both these constructs are just syntactic
abbreviations for type narrowing:
-
if (nonempty strings)
is an abbreviation forif (is [String+] strings)
, just like -
if (exists name)
is an abbreviation forif (is Object name)
.
Sequence syntax sugar
There's lots more syntactic sugar for sequences. We can use a bunch of familiar Java-like syntax:
String[] operators = [ "+", "-", "*", "/" ];
String? plus = operators[0];
String[] multiplicative = operators[2..3];
Oh, and the expression []
evaluates to an instance of Empty
.
[] none = [];
However, unlike Java, all these syntactic constructs are pure abbreviations. The code above is exactly equivalent to the following de-sugared code:
Sequential<String> operators = ... ;
Null|String plus = operators.get(0);
Sequential<String> multiplicative = operators.span(2,3);
(We'll come back to what the list of values in brackets means in a minute!)
The Sequence
interface extends
Iterable
,
so we can iterate a Sequence
using a for
loop:
for (op in operators) {
print(op);
}
Ranges
A Range
is a kind of Sequence
. The following:
Character[] uppercaseLetters = 'A'..'Z';
Integer[] countDown = 10..0;
Is just sugar for:
Sequential<Character> uppercaseLetters = Range('A','Z');
Sequential<Integer> countDown = Range(10,0);
In fact, this is just a sneak preview of the fact that almost all operators in Ceylon are just sugar for method calls upon a type. We'll come back to this later, when we talk about operator polymorphism.
Ceylon doesn't need C-style for
loops. Instead, combine for
with the
range operator:
variable Integer fac=1;
for (n in 1..100) {
fac*=n;
print("Factorial ``n``! = ``fac``");
}
Sequence and its supertypes
It's probably a good time to see some more advanced Ceylon code. What better place to find some than in the language module itself?
You can find the API documentation and source code of
Sequence
online, or you can go to Navigate > Open Ceylon Declaration...
to view the
declaration of Sequential
directly inside Ceylon IDE.
The most important operations of Sequential
are inherited from
Correspondence
,
and Iterable
.
-
Correspondence
provides the capability to access elements of the sequence by index, and -
Iterable
provides the ability to iterate the elements of the sequence.
Now open the class Range
in the IDE, to see a concrete implementation of the Sequence
interface.
Empty sequences and the bottom type
Finally, check out the definition of
Empty
.
Notice that Empty
is declared to be a subtype of List<Nothing>
. This special
type Nothing
, often called the bottom type, represents:
- the empty set, or equivalently
- the intersection of all types.
Since the empty set is a subset of all other sets, Nothing
is assignable to
all other types. Why is this useful here? Well, Correspondence<Integer,Element>
and Iterable<Element>
are both covariant in the type parameter Element
. So
Empty
is assignable to Correspondence<Integer,T>
and Iterable<T>
for any
type T
. That's why Empty
doesn't need a type parameter.
Since there are no actual instances of Nothing
, if you ever see an attribute
or method of type Nothing
, you know for certain that it can't possibly ever
return a value. There is only one possible way that such an operation can
terminate: by throwing an exception.
Another cool thing to notice here is the return type of the
first
and
item()
operations
of Empty
. You might have been expecting to see Nothing?
here, since they
override supertype members of type T?
. But as we saw in the
first part of the Tour, Nothing?
is just an abbreviation for
Null|Nothing
. And Nothing
is the empty set, so the union Nothing|T
of
Nothing
with any other type T
is just T
itself.
The Ceylon compiler is able to do all this reasoning automatically. So when
it sees an Iterable<Nothing>
, it knows that the operation first
is of type
Null
, i.e. that its value is null
.
Cool, huh?
Sequence gotchas for Java developers
Superficially, a sequence type looks a lot like a Java array, but really it's
very, very different! First, of course, a sequence type Sequential<String>
is
an immutable interface, it's not a mutable concrete type like an array. We
can't set the value of an element:
String[] operators = .... ;
operators[0] = "^"; //compile error
Furthermore, the index operation operators[i]
returns an optional type
String?
, which results in quite different code idioms. To begin with, we
don't iterate sequences by index like in C or Java. The following code does
not compile:
for (i in 0..operators.size-1) {
String op = operators[i]; //compile error
// ...
}
Here, operators[i]
is a String?
, which is not directly assignable to
String
.
Instead, if we need access to the index, we use the special form of for
shown above.
for (i -> op in entries(operators)) {
// ...
}
Likewise, we don't usually do an upfront check of an index against the sequence length:
if (i>operators.size-1) {
throw IndexOutOfBoundException();
}
else {
return operators[i]; //compile error
}
Instead, we do the check after accessing the sequence element:
if (exists op = operators[i]) {
return op;
}
else {
throw IndexOutOfBoundException();
}
Indeed, this is a common use for assert
:
assert (exists op = operators[i]);
return op;
We especially don't ever need to write the following:
if (i>operators.size-1) {
return "";
}
else {
return operators[i]; //compile error
}
This is much cleaner:
return operators[i] else "";
All this may take a little getting used to. But what's nice is that all the
exact same idioms also apply to other kinds of Correspondence
, including
Map
s .
Tuples
A tuple is a linked list which captures the static type of each individual element in the list. For example:
[Float,Float,String] point = [0.0, 0.0, "origin"];
This tuple contains a two Float
s followed by a String
. That information
is captured in its static type, [Float,Float,String]
.
Each link of the list is an instance of the class
Tuple
.
If you really must know, the code above is syntax sugar for the following:
Tuple<Float|String,Float,Tuple<Float|String,Float,Tuple<String,String>>>
point = Tuple(0.0, Tuple(0.0, Tuple("origin", [])));
Therefore, we always use syntax sugar when working with tuples.
Tuple
extends Sequence
, so we can do all the usual kinds of sequency
things to a tuple, iterate it, and so on. As with sequences, we can access
a tuple element by index. But in the case of a tuple, Ceylon is able to
determine the type of the element when the index is a literal integer:
Float x = point[0];
Float y = point[1];
String label = point[2];
Null zippo = point[3];
A unterminated tuple is a tuple where the last link in the list is
a sequence, not an Empty
. For example:
String[] labels = ... ;
[Float,Float,String*] point = [0.0, 0.0, *labels];
This tuple contains two Float
s followed by an unknown number of String
s.
Now we can see that a sequence type like [String*]
or [String+]
can
be viewed as a degenerate tuple type!
There's more...
If you're interested, you can find a more in-depth discussion of tuples here.
Next up we'll explore some more details of the type system, starting with type aliases and type inference.