Note: information on this page refers to Ceylon 1.2, 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 if we ever need to write
code that refers to the concrete type of origin
or consoleWriter
, we
must use the very ugly syntax \Iorigin
or \IconsoleWriter
as the type
name.
Thus, you occasionally see the following pattern in Ceylon:
abstract class Unit() of unit {
//operations
}
object unit extends Unit() {}
If Unit
and unit
are toplevel declarations, then Unit
is called a
unit type, since it has exactly one instance.
Gotcha!
Later, we'll see that
it's usually better to write a class like Unit
as a class with a
value constructor:
class Unit {
shared new unit {}
//operations
}
And we'll also learn that object
declarations are really just a syntax
sugar for a value constructor after all.
Is an anonymous class a singleton?
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 {
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 parameterized 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 parameterized 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; //formal attribute
...
}
class ConsoleApp() extends App() {
shared actual object stream //object refines the attribute
satisfies OutputStream { ... }
...
}
However, an object
may not itself be declared formal
or default
.
Anonymous class expressions
We can even write an anonymous class "inline" in an expression, for example:
interface Subscription {
shared formal void cancel();
}
Subscription register(Subscriber s) {
subscribers.append(s);
return object satisfies Subscription {
cancel() => subscribers.remove(s);
};
}
Member classes and member class refinement
You're probably used to the idea of an "inner" or nested 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 polymorphic member of the containing type.
Let's start with the following 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.
Notice how, unlike in Java or C#, where polymorphism is only for methods, Ceylon features polymorphism for attributes, methods, and member classes.
Formal member classes
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() {
...
}
}
...
}
Even though abstract
classes and formal
classes seem like quite similar
concepts, there's a really big difference:
- 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.
Note that this difference explains why Ceylon has a special-purpose annotation
to distinguish a formal member. We can't use abstract
to declare a formal
member, since that would be ambiguous in the case of a nested 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.