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

Inheritance, refinement, and interfaces

This is the fourth leg of the Tour of Ceylon. In the previous part we looked at attributes, variables, setters, and control structures. In this section we're going to learn about inheritance and refinement (known as "overriding" in many other languages).

Inheritance is one of two ways Ceylon lets us abstract over types. (The other is generics, which we'll get to later in this tour.) Ceylon features a flavor of multiple inheritance called "mixin" inheritance. You might have heard or experienced that multiple inheritance is scary and complicated and, indeed, that's kinda true of multiple inheritance in C++. But mixin inheritance in Ceylon comes with certain restrictions that strike a good balance between power and harmlessness.

Inheritance and refinement

In object-oriented programming, we often replace conditionals (if, and especially switch) with subtyping. Indeed, according to some folks, this is what makes a program object-oriented. Let's try refactoring the Polar class from the previous leg of the tour into two classes, with two different implementations of description. Here's the superclass:

"A polar coordinate"
class Polar(Float angle, Float radius) {

    shared Polar rotate(Float rotation) =>
            Polar(angle+rotation, radius);

    shared Polar dilate(Float dilation) =>
            Polar(angle, radius*dilation);

    "The default description"
    shared default String description => 
            "(``radius``,``angle``)";

}

Notice that Ceylon forces us to declare attributes or methods that can be refined (overridden) by annotating them default.

Subclasses specify their superclass using the extends keyword (here's why), followed by the name of the superclass, followed by a list of arguments to be sent to the superclass initializer parameters. It looks just like an expression that instantiates the superclass:

"A polar coordinate with a label"
class LabeledPolar(Float angle, Float radius, String label)
        extends Polar(angle, radius) {

    "The labeled description"
    shared actual String description =>
            label + "-" + super.description;

}

Ceylon also forces us to declare that an attribute or method refines (overrides) an attribute or method of a superclass by annotating it actual (not "overrides" like Java). All this annotating stuff costs a few extra keystrokes, but it helps the compiler detect errors. We can't inadvertently refine a member or the superclass, or inadvertently fail to refine it.

Notice that Ceylon goes out of its way to repudiate the idea of "duck" typing or structural typing. If it walks() like a Duck, then it should be a subtype of Duck and must explicitly refine the definition of walk() in Duck. We don't believe that the name of a method or attribute alone is sufficient to identify its semantics. And, more importantly, structural typing doesn't work properly with tools.

Shortcut syntax for refinement

There's a more compact way to refine a default member of a superclass: simply specify its refined implementation using =>, like this:

"A polar coordinate with a label"
class LabeledPolar(Float angle, Float radius, String label)
        extends Polar(angle, radius) {

    description => label + ": " + super.description;

}

Or assign a value to it using =, like this:

"A polar coordinate with a label"
class LabeledPolar(Float angle, Float radius, String label)
        extends Polar(angle, radius) {

    description = "``label``: (``radius``,``angle``)";

}

You can refine any function or non-variable value using this streamlined syntax.

Note that this shortcut syntax does not allow annotations. If you need to add documentation or other annotations to the refining member, you must use the more verbose syntax.

Refining a member of Object

Our Polar class is an implicit subtype of the class Object in the package ceylon.language. If you take a look at this class, you'll see that it has a default attribute named string. It's common to refine this attribute to provide a developer-friendly representation of the object.

Polar is also a subtype of the interface Identifiable which defines default implementations of equals() and hash. We should definitely refine those:

"A polar coordinate"
class Polar(Float angle, Float radius) {

    // ...

    shared default String description => 
            "(``radius``,``angle``)";

    value azimuth => pi*(angle/pi).fractionalPart;

    shared actual Boolean equals(Object that) {
        if (is Polar that) {
            return azimuth==that.azimuth && 
                   radius==that.radius; 
        }
        else {
            return false;
        }
    }

    shared actual Integer hash => radius.hash;

    shared actual String string => description;

}

It's the first time we've seen this syntax:

if (is Polar that) { ... }

As you've probably guessed, if (is ... ) works just like if (exists ... ), testing and narrowing the type of a value. In this case it tests the type of that and narrows to Polar if that is indeed an instance of Polar. We'll come back to this construct later in the tour.

Using the shortcut syntax for refinement that we just met, we could abbreviate the above code like this:

"A polar coordinate"
class Polar(Float angle, Float radius) {

    // ...

    shared default String description => 
            "(``radius``,``angle``)";

    value azimuth => pi*(angle/pi).fractionalPart;

    shared actual Boolean equals(Object that) {
        if (is Polar that) {
            return azimuth==that.azimuth && 
                   radius==that.radius; 
        }
        else {
            return false;
        }
    }

    hash => radius.hash;

    string => description;

}

(But in this case, the shortcut syntax is perhaps not an improvement.)

Abstract classes

Now let's consider a much more interesting problem: abstracting over the polar and cartesian coordinate systems. Since a cartesian coordinate isn't just a special kind of polar coordinate, this is a case for introduction of an abstract superclass:

"A coordinate-system free abstraction of a 
 geometric point"
abstract class Point() {

     shared formal Polar polar;
     shared formal Cartesian cartesian;

     shared formal Point rotate(Float rotation);
     shared formal Point dilate(Float dilation);

}

Ceylon requires us to annotate abstract classes abstract, just like Java. This annotation specifies that a class cannot be instantiated, and can define abstract members. Like Java, Ceylon also requires us to annotate "abstract" members that don't specify an implementation. However, in this case, the required annotation is formal. The reason for having two different annotations, as we'll see later, is that nested classes may be either abstract or formal, and abstract nested classes are a bit different to formal member classes. A formal member class may be instantiated; an abstract class may not be.

Note that an attribute that is never initialized is always a formal attribute. Ceylon doesn't initialize attributes to zero or null unless you explicitly tell it to!

One way to define an implementation for an inherited abstract attribute is to use the shortcut refinement syntax we saw above.

"A polar coordinate"
class Polar(Float angle, Float radius) 
        extends Point() {

    polar => this;
    cartesian => Cartesian(radius*cos(angle), radius*sin(angle));

    rotate(Float rotation) => Polar(angle+rotation, radius);
    dilate(Float dilation) => Polar(angle, radius*dilation);

}

Alternatively, we can write it all out the long way.

import ceylon.math.float { sin, cos }

"A polar coordinate"
class Polar(Float angle, Float radius) 
        extends Point() {

    shared actual Polar polar => this;

    shared actual Cartesian cartesian =>
            Cartesian(radius*cos(angle), radius*sin(angle));

    shared actual Polar rotate(Float rotation) =>
            Polar(angle+rotation, radius);

    shared actual Polar dilate(Float dilation) =>
            Polar(angle, radius*dilation);

}

Notice that Ceylon, like Java, allows covariant refinement of member types. We were able to refine the return type of rotate() and dilate(), narrowing to Polar from the more general type declared by Point. But Ceylon doesn't currently support contravariant refinement of parameter types. You can't refine a method and widen a parameter type. (Someday we would love to fix this.)

Of course, you can't refine a member and widen the return type, or change to some arbitrary different type, since in that case the subclass would no longer be a subtype of the supertype. If you're going to refine the return type, you have to refine to a subtype.

Cartesian also covariantly refines rotate() and dilate(), but to a different return type:

import ceylon.math.float { atan } 

"A cartesian coordinate"
class Cartesian(Float x, Float y) 
        extends Point() {

    shared actual Polar polar => 
            Polar(atan(y/x), (x^2+y^2)^0.5); 

    shared actual Cartesian cartesian => this;

    shared actual Cartesian rotate(Float rotation) =>
            polar.rotate(rotation).cartesian;

    shared actual Cartesian dilate(Float dilation) =>
            Cartesian(x*dilation, y*dilation);

}

We usually don't try to prevent other code from extending a class (though there is a final annotation like in Java). Since only members explicitly declared as supporting refinement using either formal or default can be refined, a subtype can never break the implementation of a supertype. Unless the supertype was explicitly designed to be extended, a subtype can add members, but never change the behavior of inherited members.

Abstract classes are useful. But since interfaces in Ceylon are more powerful than interfaces in Java, it often makes more sense to use an interface instead of an abstract class.

Interfaces and "mixin" inheritance

From time to time we come across a case where a class needs to inherit functionality from more than one supertype. Java's inheritance model doesn't support this, since an interface can never define a member with a concrete implementation. Interfaces in Ceylon are a little more flexible:

  • An interface may define concrete methods, attribute getters, and attribute setters, but
  • it may not define references or initialization logic.

Notice that prohibiting references and initialization logic makes interfaces completely stateless. An interface can't hold references to other objects.

Let's take advantage of mixin inheritance to define a reusable Writer interface for Ceylon.

interface Writer {

    shared formal Formatter formatter;

    shared formal void write(String string);

    shared void writeLine(String string) {
        write(string);
        write("\n");
    }

    shared void writeFormattedLine(String format, Object* args) {
        writeLine( formatter.format(format, args) );
    }

}

Note that we can't define a concrete value for the formatter attribute, since an interface may not hold a reference to another object.

Now let's define a concrete implementation of this interface.

class ConsoleWriter() satisfies Writer {
    formatter = StringFormatter();
    write(String string) => print(string);
}

The satisfies keyword (not implements like Java) is used to specify that an interface extends another interface or that a class implements an interface. Unlike an extends declaration, a satisfies declaration does not specify arguments, since interfaces do not have parameters or initialization logic. Furthermore, the satisfies declaration can specify more than one interface.

Ceylon's approach to interfaces eliminates a common pattern in Java where a separate abstract class defines a default implementation of some of the members of an interface. In Ceylon, the default implementations can be specified by the interface itself. Even better, it's possible to add a new member to an interface without breaking existing implementations of the interface.

Ambiguities in mixin inheritance

It's illegal for a type to inherit two members with the same name, unless the two members both (directly or indirectly) refine a common member of a common supertype, and the inheriting type itself also refines the member to eliminate any ambiguity. The following results in a compilation error:

interface Party {
    shared formal String legalName;
    shared default String name => legalName;
}

interface User {
    shared formal String userId;
    shared default String name => userId;
}

class Customer(String customerName, String email)
        satisfies User & Party {
    shared actual String legalName = customerName;
    shared actual String userId = email;
    shared actual String name = customerName;    //error: refines two different members
}

To fix this code, we'll factor out a formal declaration of the attribute name to a common supertype. The following is legal:

interface Named {
    shared formal String name;
}

interface Party satisfies Named {
    shared formal String legalName;
    shared actual default String name => legalName;
}

interface User satisfies Named {
    shared formal String userId;
    shared actual default String name => userId;
}

class Customer(String customerName, String email)
        satisfies User & Party {
    shared actual String legalName = customerName;
    shared actual String userId = email;
    shared actual String name = customerName;
}

Oh, of course, the following is illegal:

interface Named {
    shared formal String name;
}

interface Party satisfies Named {
    shared formal String legalName;
    shared actual String name => legalName;
}

interface User satisfies Named {
    shared formal String userId;
    shared actual String name => userId;
}

class Customer(String customerName, String email)
        satisfies User & Party {    //error: inherits multiple definitions of name
    shared actual String legalName = customerName;
    shared actual String userId = email;
}

To fix this code, name must be declared default in both User and Party and explicitly refined in Customer. We can delegate to one of the super-interface implementations using the syntax (super of Party).name.

interface Named {
    shared formal String name;
}

interface Party satisfies Named {
    shared formal String legalName;
    shared actual default String name => legalName;
}

interface User satisfies Named {
    shared formal String userId;
    shared actual default String name => userId;
}

class Customer(String customerName, String email)
        satisfies User & Party {
    shared actual String legalName => customerName;
    shared actual String userId => email;
    shared actual String name => (super of Party).name;
}

The of operator performs a statically safe typecast. That is, a cast that is guaranteed to succeed at runtime. We'll meet other uses for it later, but here you can think of it as widening the type of the expression super from User&Party to Party, thus resolving the ambiguity as to which inherited definition of name should be called.

There's more...

Next, to finish off our discussion of object oriented programming in Ceylon, we're going to learn about anonymous classes and member classes.