Generics

This is the ninth part of the Tour of Ceylon. The previous leg covered intersection types, union types, and enumerated types. In this part we're looking at generic types.

Inheritance and subtyping are a powerful tool for abstracting over types. But this tool has its limitations. It can't help us express generic container types like collections. For this problem we need parameterized types. We've seen plenty of parameterized types already—for example, iterables, sequences, and tuples—but now let's explore a few more details.

Generic types

Programming with generic types is one of the most difficult parts of Java. That's still true, to some extent, in Ceylon. But because the Ceylon language and SDK were designed for generics from the ground up, Ceylon is able to alleviate the most painful aspects of Java's bolted-on-later model.

There are two kinds of things which may be generic:

a class, interface, or type alias, or

a function.

Type parameters

Just like in Java, only types and methods may declare type parameters. Also just like in Java, type parameters are listed before ordinary parameters, enclosed in angle brackets.

shared interface Iterator<out Element> { ... }

shared class Singleton<out Element>(Element element) extends Object() satisfies [Element+] given Element satisfies Object { ... }

shared Value sum<Value>({Value+} values) given Value satisfies Summable<Value> { ... }

shared <Key->Item>[] zip<Key,Item>({Key*} keys, {Item*} items) given Key satisfies Object given Item satisfies Object { ... }

As you can see, the convention in Ceylon is to use meaningful names for type parameters (in other languages the usual convention is to use single letter names).

Type arguments

Unlike Java, we always do need to specify type arguments in a type declaration (there are no raw types in Ceylon). The following will not compile:

value strings = {"hello", "world"}; Iterator it = strings.iterator(); //error: missing type argument to parameter Element of Iterable

Instead, we have to provide a type argument like this:

value strings = {"hello", "world"}; Iterator<String> it = strings.iterator();

On the other hand, it would be very annoying if we always had to specify all type arguments explicitly everywhere.

Type argument inference

Fortunately, we don't need to explicitly specify type arguments in most method invocations or class instantiations. We don't usually need to write:

Array<String> strings = Array<String> { "Hello", "World" }; {String|Integer*} things = interleave<String|Integer,Null>(strings, 0..2);

Instead, it's very often possible to infer the type arguments from the ordinary arguments.

value strings = Array { "Hello", "World" }; // type Array<String> value things = interleave(strings, 0..2); // type {String|Integer*}

The generic type argument inference algorithm is slightly involved, so you should refer to the language specification for a complete definition. But essentially what happens is that Ceylon infers a type argument by combining the types of corresponding arguments using union in the case of a covariant type parameter, or intersection in the case of a contravariant type parameter.

value points = Array { Polar(pi/4, 0.5), Cartesian(-1.0, 2.5) }; // type Array<Polar|Cartesian> value entries = zipEntries(1..points.size, points); // type {<Integer->Polar|Cartesian>*}

There's one more way to avoid having to write in type arguments explicitly.

Default type arguments

A type parameter may have a default argument.

shared interface Iterable<out Element, out Absent=Null> ...

If a type parameter has a default argument, we're allowed to leave out the type argument to that type parameter when we supply a type argument list. Therefore:

Iterable<String> means Iterable<String,Null> , and

means , and Iterable<> means Iterable<Anything,Null> .

Note that we needed to explicitly specify the pointy brackets in Iterable<> .

Gotcha!

Default type arguments are always ignored when type arguments are inferred.

Suppose I have a generic class with a default argument:

class Container<out Element=Anything>(Element* elements) {}

Let's instantiate this class without any arguments:

value container = Container();

You might imagine that this instantiation would produce a Container