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.