Note: information on this page refers to Ceylon 1.0, not to the current release.

Anonymous and member classes

This is the fifth stop in our Tour of Ceylon. In the previous leg we learned about inheritance and refinement. It's time to round out our introduction to object-oriented programming in Ceylon by learning about anonymous classes and member classes.

Anonymous classes

If a class has no parameters, it's often possible to use a shortcut declaration which defines a named instance of the class, without providing any actual name for the class itself. This is usually most useful when we're extending an abstract class or implementing an interface.

"The origin"
object origin extends Polar(0.0, 0.0) {
    description => "origin";
}

An anonymous class may extend an ordinary class and satisfy interfaces.

shared object consoleWriter satisfies Writer {
    formatter = StringFormatter();
    write(String string) => process.write(string);
}

The downside to an object declaration is that we can't write code that refers to the concrete type of origin or consoleWriter, only to the named instances.

You might be tempted to think of object declarations as defining singletons, but that's not quite right:

  • A toplevel object declaration does indeed define a singleton.
  • An object declaration nested inside a class defines an object per instance of the containing class.
  • An object declaration nested inside a method, getter, or setter results in a new object each time the method, getter, or setter is executed.

Let's see how this can be useful:

interface Subscription {
    shared formal void cancel();
}

Subscription register(Subscriber s) {
    subscribers.append(s);
    object subscription satisfies Subscription {
        shared actual void cancel() =>
            subscribers.remove(s);
    }
    return subscription;
}

Notice how this code example makes clever use of the fact that the nested object declaration receives a closure of the locals defined in the containing method declaration!

A different way to think about the difference between object and class is to think of a class as a parametrized object. (Of course, there's one big difference: a class declaration defines a named type that we can refer to in other parts of the program.) We'll see later that, analogously, Ceylon lets us think of a method as a parametrized attribute.

An object declaration can refine an attribute declared formal or default, as long as it is a subtype of the declared type of the attribute.

shared abstract class App() {
    shared formal OutputStream stream;
    ...
}

class ConsoleApp() extends App() {
    shared actual object stream
            satisfies OutputStream { ... }
    ...
}

However, an object may not itself be declared formal or default.

Member classes and member class refinement

You're probably used to the idea of an "inner" class in Java—a class declaration nested inside another class or method. Since Ceylon is a language with a recursive block structure, the idea of a nested class is more than natural. But in Ceylon, a non-abstract nested class is actually considered a member of the containing type. For example, BufferedReader defines the member class Buffer:

class BufferedReader(Reader reader)
        satisfies Reader {
    shared default class Buffer()
            satisfies List<Character> { ... }
    ...
}

The member class Buffer is annotated shared, so we can instantiate it like this:

BufferedReader br = BufferedReader(ExampleReader());
BufferedReader.Buffer b = br.Buffer();

Note that a nested type name must be qualified by the containing type name when used outside of the containing type.

The member class Buffer is also annotated default, so we can refine it in a subtype of BufferedReader:

class BufferedFileReader(File file)
        extends BufferedReader(FileReader(file)) {
    shared actual class Buffer()
            extends super.Buffer() { ... }
    ...     
}

That's right: Ceylon lets us "override" a member class defined by a supertype!

Note that BufferedFileReader.Buffer is a subclass of BufferedReader.Buffer.

Now the instantiation br.Buffer() above is a polymorphic operation! It might return an instance of BufferedFileReader.Buffer or an instance of BufferedReader.Buffer, depending upon whether br refers to a plain BufferedReader or to a BufferedFileReader. This is more than a cute trick. Polymorphic instantiation lets us eliminate the "factory method pattern" from our code.

It's even possible to define a formal member class of an abstract class. A formal member class can declare formal members.

abstract class BufferedReader(Reader reader)
        satisfies Reader {
    shared formal class Buffer() {
        shared formal Byte read();
    }
    ...
}

In this case, a concrete subclass of the abstract class must refine the formal member class.

shared class BufferedFileReader(File file)
        extends BufferedReader(FileReader(file)) {
    shared actual class Buffer()
             extends super.Buffer() {
         shared actual Byte read() {
             ...
         }
    }
    ...
}

Notice the difference between an abstract class and a formal member class. An abstract nested class may not be instantiated, and need not be refined by concrete subclasses of the containing class. A formal member class may be instantiated, and must be refined by every subclass of the containing class.

It's an interesting exercise to compare Ceylon's member class refinement with the functionality of Java dependency injection frameworks. Both mechanisms provide a means of abstracting the instantiation operation of a type. You can think of the subclass that refines a member type as filling the same role as a dependency configuration in a dependency injection framework.

There's more...

Member classes and member class refinement allows Ceylon to support type families. We're not going to discuss type families in this tour.

Next, we're going to meet iterable objects, along with sequences, Ceylon's take on the "array" type, and tuples.