Help

This is the third installment in a series of articles introducing the Ceylon language. Note that some features of the language may change before the final release.

This article was updated on 28/5/2011 to reflect changes to the model for introductions and add material on ambiguities in mixin inheritance, and on 3/5/2011 to incorporate feedback on member class refinement. The comment thread reflects information in the first version of article.

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 Hello class from Part 2 into two classes, with two different implementations of greeting:

doc "A default greeting" 
class DefaultHello() {

    doc "The greeting" 
    shared default String greeting = "Hello, World!";
    
    doc "Print the greeting" 
    shared void say(OutputStream stream) {
        stream.writeLine(greeting);
    }
    
}

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, 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:

doc "A personalized greeting" 
class PersonalizedHello(String name) 
        extends DefaultHello() {
    
    doc "The personalized greeting" 
    shared actual String greeting {
        return "Hello, " name "!";
    }

}

Ceylon also forces us to declare that an attribute or method refines (overrides) an attribute or method of a superclass by annotating it actual. 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.

Abstract classes

There's one problem with what we've just seen. A personalized greeting is not really a kind of default greeting. This is a case for introducing an abstract superclass:

doc "A greeting" 
abstract class Hello() {
    
    doc "The (abstract) greeting" 
    shared formal String greeting;
    
    doc "Print the greeting" 
    shared void say(OutputStream stream) {
        stream.writeLine(greeting);
    }
    
}

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 slightly 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 simply assign a value to it in the subclass.

doc "A default greeting" 
class DefaultHello() extends Hello() {
    greeting = "Hello, World!";
}

Of course, we can also define an implementation for an inherited abstract attribute by refining it.

doc "A personalized greeting" 
class PersonalizedHello(String name) 
        extends Hello() {
    
    doc "The personalized greeting" 
    shared actual String greeting {
        return "Hello, " name "!";
    }
    
}

Note that there's no way to prevent a other code from extending a class in Ceylon. 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.

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.
  • It may not define simple attributes or initialization logic.

Notice that prohibiting simple attributes 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.

shared interface Writer { 

    shared formal Formatter formatter; 
    
    shared formal void write(String string);
    
    shared void writeLine(String string) { 
        write(string);
        write(process.newLine);
    }
    
    shared void writeFormattedLine(String formatString, Object... args) { 
        writeLine( formatter.format(formatString, args) );
    }
    
}

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

Note also that the call to writeLine() from writeFormattedLine() resolves to the instance method of Writer, which hides the toplevel method of the same name.

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

shared class ConsoleWriter() 
        satisfies Writer {
    
    formatter = StringFormatter();
    
    shared actual void write(String string) { 
        writeLine(string);
    }
    
}

The satisfies keyword 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 {
        return legalName;
    }
}

interface User {
    shared formal String userId;
    shared default String name {
        return userId;
    }
}

class Customer(String name, String email) 
        satisfies User & Party {
    legalName = name;
    userId = email;
    shared actual String name = name;    //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 {
        return legalName;
    }
}

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

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

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 {
        return legalName;
    }
}

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

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

To fix this code, name must be declared default in both User and Party and explicitly refined in Customer.

Introduction

Sometimes, especially when we're working with code from modules we don't have control over, we would like to mix an interface into a type that has already been defined in another module. For example, we might like to introduce the Ceylon collections module type List into the language module type Sequence, so that all Sequences support all operations of List. But the language module shouldn't have a dependency to the collections module, so we can't specify that interface Sequence satisfies List in the declaration of Sequence in the language module.

Instead, we can introduce the type Sequence in the code which uses the collections and language modules. The collections module already defines an interface called SequenceList for this purpose. Well, it doesn't yet, since we have not yet either implemented introductions or written the collections module, but it will soon!

doc "Decorator that introduces List to Sequence."
see (List,Sequence)
shared interface SequenceList<Element> 
        adapts Sequence<Element>
        satisfies List<Element> {
    
    shared actual default List<Element> sortedElements() {
    	//define the operation of List in
    	//terms of operations on Sequence
        return asList(sortSequence(this));
    }
    
    ...
    
}

The adapts clause makes SequenceList a special kind of interface called an adapter (in the terminology used by this book). According to the language spec:

The interface may not:
  • declare or inherit a member that refines a member of any adapted type, or
  • declare or inherit a formal or non-default actual member unless the member is inherited from an adapted type.

The purpose of an adapter is to add a new supertype, called an introduced type, to an existing type, called the adapted type. The adapter doesn't change the original definition of the adapted type, and it doesn't affect the internal workings of an instance of the adapted type in any way. All it does is fill in the definitions of the missing operations. Here, the SequenceList interface provides concrete implementations of all methods of List that are not already implemented by Sequence.

Now, to introduce List to Sequence in a certain compilation unit, all we need to do is import the adapter:

import ceylon.collection { List, SequenceList }

...

//define a Sequence
Sequence<String> names = { "Gavin", "Emmanuel", "Andrew", "Ales" };

//call an operation of List on Sequence
List<String> sortedNames = names.sortedElements();

Note that the introduction is not visible outside the lexical scope of the import statement (the compilation unit). But within the compilation unit containing the import statement, every instance of of the adapted type Sequence now has all the attributes and methods of the introduced type List, and is assignable to the introduced type.

Again, according to the spec:

If, in a certain compilation unit, multiple introductions of a certain adapted type declare or inherit a member that refines a common member of a common supertype then either:
  • there must be a unique member from the set of members, called the most refined member, that refines all the other members, or
  • the adapted type must declare or inherit a member that refines all the members.
At runtime, an operation (method invocation, member class instantiation, or attribute evaluation) upon any type that is a subtype of all the adapted types is dispatched according to the following rule:
  • If the runtime type of the instance of the adapted type declares or inherits a member defining the operation, the operation is dispatched to the runtime type of the instance.
  • Otherwise, the operation is dispatched to the introduction that has the most-refined member defining the operation.

Introduction compared to extension methods and implicit type conversions

Introduction is Ceylon's way of extending a type after it's been defined. It's interesting to compare introduction to the following features of other languages:

  • extension methods, and
  • user-defined implicit type conversions.

Introduction is really just a much more powerful cousin of extension methods. From our point of view, an extension method introduces a member to a type, without actually introducing a new supertype. Indeed, a Ceylon adapter with no satisfies clause is actually a package of extension methods!

shared interface StringSequenceExtensions 
        adapts Sequence<String> {
    
    shared String concatenated {
        variable String concat = "";
        for (String s in this) {
            concat+=s;
        }
        return concat;
    }
    
    shared String join(String separator=", ") {
        ...
    }
    
}

On the other hand, introductions are less powerful than implicit type conversions. This is by design! In this case, less powerful means safer, more disciplined. The power of implicit type conversions comes partly from their ability to work around some of the designed-in limitations of the type system. But these limitations have a purpose! I'm especially thinking of the prohibitions against:

  • inheriting the same generic type twice, with different type arguments (in most languages), and
  • overloading (in Ceylon).

Implicit type conversions are an end-run around these restrictions, reintroducing the ambiguities that these restrictions exist to solve.

Furthermore, it's extremely difficult to imagine a language with implicit type conversions that preserve the following important properties of the type system:

  • transitivity of the assignability relationship,
  • covariance of generic types,
  • the semantics of the identity == operator, and
  • the ability to infer generic type arguments of an invocation or instantiation.

Finally, implicit type conversions work by having the compiler introduce hidden invocations of arbitrary user-written procedural code, code that could potentially have side-effects or make use of temporal state. Thus, the observable behavior of the program can depend upon precisely where and how the compiler introduces these magic calls.

Introductions are a kind of elegant compromise: more powerful than plain extension methods, safer than implicit type conversions. We think the beauty of this model is a major advantage of Ceylon over similar languages.

Type aliases

It's often useful to provide a shorter or more semantic name to an existing class or interface type, especially if the class or interface is a parameterized type. For this, we use a type alias, for example:

interface People = Set<Person>;

A class alias must declare its formal parameters:

shared class People(Person... people) = ArrayList<Person>;

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(reader); 
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:

shared 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 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.

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.

doc "A default greeting" 
object defaultHello extends Hello() {
    greeting = "Hello, World!";
}
shared object consoleWriter satisfies Writer {
        	
    formatter = StringFormatter();
    
    shared actual void write(String string) { 
        writeLine(string);
    }
    
}

The downside to an object declaration is that we can't write code that refers to the concrete type of defaultHello 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 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 an 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();
}
shared 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 Ceylon also lets us think of a method as a parametrized attribute.

An object declaration can refine an attribute declared formal or default.

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.

There's more...

Member classes and member class refinement allows Ceylon to support type families.

If you're interested, here's some crazy ideas about how to generalize the notion of refinement to toplevel declarations.

In Part 4, we're going to meet sequences, Ceylon's take on the array type.

20 comments:
 
29. Apr 2011, 21:34 CET | Link

Regarding the member classes and their refinement: BufferedReader and BufferedFileReader from the example are in the same class hierarchy. The instantiation br.Buffer() becomes polymorphic which indeed is very cute but I would expect that polymorhic calls give results that can be used in polymorphic manner also, that is they share the same class hierarchy.

In that particular example I would expect that br.Buffer() returns some kind of buffer (in Java terms: instanceof Buffer) and not an ArrayList as in the example with alias.

I can image a situation that this would be useful IF the Buffer would not be shared - it would be used internally by the outer class.

However if the Buffer is shared it is accessible outside of the BufferReader so making it an alias to totally different class introduces some inconsistency (as for a statically typed language :-).

Could you provide an example where member class aliasing would be useful? I can't think of any from top of my head.

And a short side question: do we really need semicolons for single statement lines? :)

Ceylon looks very promising so far, keep working, can't wait to get the compiler :-)

 
30. Apr 2011, 03:25 CET | Link

Mateusz, that's very perceptive.

I think you're right. I'm not meaning for this to be something like a Scala abstract type member. But the syntax I've chosen makes it look like that, to the extent that I've almost confused myself about the semantics - I've described the reference to ArrayList as a type alias, which it's not really.

What I mean is, I think type alias refinement is a bit broken (I don't see how you can do it without breaking transitivity of assignability), and that's not what this is meant to be.

What I'm actually trying to capture is the idea that the call to br.Buffer() results in a call to ArrayList<Character>(), not that BufferedFileReader.Buffer is an alias of ArrayList<Character>(), and that therefore ArrayList<Character>() is a subtype of BufferedReader.Buffer.

A syntax that better captures what I'm really trying to get at would be something like this:

shared class BufferedFileReader(File file) 
        extends BufferedReader(FileReader(file)) {
    Buffer = ArrayList<Character>;
}

This would be by analogy with the syntax that is permitted for refining formal attributes and methods without refining their type. But the truth is, this still looks much too much like an alias definition for my liking.

Could you provide an example where member class aliasing would be useful? I can't think of any from top of my head.

The truth is, given the problem you've pointed out, I think we can easily live without this feature. The following (which we'll see when we talk about first-class functions) is more or less equivalent:

abstract class BufferedReader(Reader reader) 
        satisfies Reader { 
    shared formal List<Character> buffer();
    ...
}
shared class BufferedFileReader(File file) 
        extends BufferedReader(FileReader(file)) {
    buffer = ArrayList<Character>;
}

I think this version is much less open to interpretation, with the only downside being that you lose the sugar of buffer() looking visually like an instantiation.

I think I was trying to be much too clever here. Thanks for helping me see that.

Let me sleep on this, and if I still think you're right in the morning, I will remove this example from the blog, and use the following, less exotic example of a formal member class:

abstract class BufferedReader(Reader reader) 
        satisfies Reader { 
    shared formal class Buffer() {
        shared formal Byte read();
    }
    ...
}
shared class BufferedFileReader(File file) 
         extends BufferedReader(FileReader(file)) {
     shared actual class Buffer() 
             extends super.Buffer() {
         shared actual Byte read() {
             ...
         }
    }
}

This is more the kind of example you were expecting to see, right?

 
02. May 2011, 13:30 CET | Link

Yes, this example is more like what I expected to see. I believe that in a type safe language we should prevent situation in which expected type of a member class changes - I think it should always fulfil the IS-A relationship with super class, especially if this is one way of expressing dependency injection.

Otherwise you could just use composition or define a new member class in a subclass if you need a different type. Less confusing and less error prone I think.

Thank you for the explanation.

 
02. May 2011, 13:41 CET | Link
Yes, this example is more like what I expected to see.

It's actually what formal member classes are really meant to be. It was an oversight to not have an example of that in the original article. And I'm going to remove the by-reference style of member class definition from the language spec. It's way too open to misunderstanding, and is not even really very semantically consistent with what member class refinement is really all about. (And I'm not interested in adding Scala-style abstract type members to Ceylon.)

06. May 2011, 09:37 CET | Link
Francois Swiegers
An interface may define concrete methods, attribute getters, and attribute setters.

Coming from a Java background, the idea of interfaces with concrete methods is extremely fascinating. Can you please elaborate on how Ceylon handles the case where a class implements multiple interfaces that each contain the same concrete method signature?

06. May 2011, 14:09 CET | Link
Francois Swiegers

Apologies, the answer to my question is already in the article...

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.
 
06. May 2011, 17:24 CET | Link

Hello, I've got two questions:

If I have two BufferedReaders, br1 and br2, are br1.Buffer and br2.Buffer distinct types? That is, will the compiler complain if I use the types wrongly? And if so, is there a way to write a method which accepts a Buffer coming from any BufferedReader?

Secondly, I suspect the inner classes may accept constructor parameters, but of course each extending class must have the same parameter list?

Adam

06. May 2011, 17:33 CET | Link
Coming from a Java background, the idea of interfaces with concrete methods is extremely fascinating. Can you please elaborate on how Ceylon handles the case where a class implements multiple interfaces that each contain the same concrete method signature?
Apologies, the answer to my question is already in the article... 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.

At least, that's the answer for now, and is the easiest thing to implement on the JVM. It might be possible to relax this limitation by allowing member renaming in any lexical context where there are two members with the same name (that don't refine a common member of a common supertype). The syntax would be something like this:

import some.package { Supertype { local renamedMember = member } }

class Subtype() extends Supertype() {

    //refines Supertype.member
    shared actual Integer renamedMember { ... }

    //does NOT refine Supertype.member
    shared String member { ... }

}

I'm a fan of this idea, but:

  • it's probably difficult to implement on the JVM, and
  • I have very rarely encountered these kinds of situations in practice.
 
06. May 2011, 17:34 CET | Link
If I have two BufferedReaders, br1 and br2, are br1.Buffer and br2.Buffer distinct types? That is, will the compiler complain if I use the types wrongly? And if so, is there a way to write a method which accepts a Buffer coming from any BufferedReader?

No. They are the same type.

Secondly, I suspect the inner classes may accept constructor parameters, but of course each extending class must have the same parameter list?

Yes, of course.

 
06. May 2011, 17:52 CET | Link
Gavin King wrote on May 06, 2011 11:34:
If I have two BufferedReaders, br1 and br2, are br1.Buffer and br2.Buffer distinct types? That is, will the compiler complain if I use the types wrongly? And if so, is there a way to write a method which accepts a Buffer coming from any BufferedReader?
No. They are the same type.

I mean br1.Buffer is not a type. Only BufferedReader.Buffer is a type.

Ceylon doesn't have path-dependent typing.

06. May 2011, 18:19 CET | Link
Thorsten
Gavin King wrote on May 06, 2011 11:33:
Coming from a Java background, the idea of interfaces with concrete methods is extremely fascinating. Can you please elaborate on how Ceylon handles the case where a class implements multiple interfaces that each contain the same concrete method signature?
Apologies, the answer to my question is already in the article... 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.
At least, that's the answer for now, and is the easiest thing to implement on the JVM. It might be possible to relax this limitation by allowing member renaming in any lexical context where there are two members with the same name (that don't refine a common member of a common supertype). The syntax would be something like this:
import some.package { Supertype { local renamedMember = member } }

class Subtype() extends Supertype() {

    //refines Supertype.member
    shared actual Integer renamedMember { ... }

    //does NOT refine Supertype.member
    shared String member { ... }

}
I'm a fan of this idea, but:
  • it's probably difficult to implement on the JVM, and
  • I have very rarely encountered these kinds of situations in practice.

Hmm, I believe Eiffel is the only language that ever got this right. In Ceylon-like syntax:

import some.package { Supertype }

class Subtype() 
	extends Supertype() renaming member as renamedMember
{

    //refines Supertype.member
    shared actual Integer renamedMember { ... }

    //does NOT refine Supertype.member
    shared String member { ... }

}

Subtype sub = Subtype();
Supertype super = sub;

sub.member();		// will call Subtype.member()
sub.renamedMember();	// will call Subtype.renamedMember()
super.member();		// will call Subtype.renamedMember() !! This is where all others typically fail

This is static typing used to its full consequence. In Eiffel you can even inherit twice from the same class (the famous DoubleLink extending twice from Link: once for the forward link and once (renaming) for the backward link). I think that this is used quite regularly in the Eiffel library (the renaming - probably not the repeated inheritance).

06. May 2011, 19:13 CET | Link

Yes, Eiffel is where I saw it first, but I believe I've seen at least one other language which takes this approach. The question for me with renaming is whether:

  • it should be something that is done by the subtype definition, as in Eiffel, and is carried around by the subtype, applying wherever the subtype is used, or
  • it's a pure lexically scoped thing, and that clients also have the responsibility to rename in any context where there would be an ambiguity.

I lean toward the second option, even though I recognize that it's less convenient for clients.

 
08. May 2011, 09:57 CET | Link

Thanks. Btw. maybe you could use e.g. http://intensedebate.com/ for comments, so that threads can be watched and interested people notified via e-mail. This would make following a discussion much easier.

Adam

 
09. May 2011, 02:31 CET | Link

Adam, I guess we'll have a public mailing list soon enough...

25. May 2011, 11:08 CET | Link
JJenks

Hi Gavin

Did you consider to add Design by Contract as in Eiffel?

25. May 2011, 17:19 CET | Link
Did you consider to add Design by Contract as in Eiffel?

Naw, never been much of a fan of this. Sort of a nice idea, but I think the practical value is pretty limited because:

  • there's no way to really obligate the developer to take advantage of it (this makes it very different to static typing), and
  • I think it's pretty difficult to come up with cases where your assertions are anything more than trivial restatements of the code they're supposed to be validating - in which case their independent value is, IMO, limited.

I note that Java's assert statement is almost never used (except by people writing unit tests, which is not actually what it was designed for), and that other post-Eiffel language designers have not been motivated to include design-by-contract facilities at the language level.

However, I think you can write a pretty nice design-by-contract facility as a library in Ceylon. You can write an assert() function that emulates the behavior of Java's assert statement (this is possible in Ceylon), and you can take advantage of Ceylon's built-in interception to automatically call a method of your class annotated checkInvariants after each invocation of an instance.

 
19. Aug 2011, 15:17 CET | Link
shared class ConsoleWriter()
        satisfies Writer {
     
    formatter = StringFormatter();
     
    shared actual void write(String string) {
        writeLine(string);
    }
     
}

why formatter is not actual?

 
09. Sep 2011, 19:26 CET | Link
ГОСТ

I found interesting blog post with idea to generate collections on the fly. http://julianhyde.blogspot.com/2011/06/roll-your-own-high-performance-java.html May be it would be interesting to prohibit to make new collection implementation traditionally and force users to use factory, described in this blog. First time this factory can return just traditional implementation of collections. Later some specific libraries implementations like troove or fastutil can be used. This approach can make Ceylon to be even faster than java.

// Initialize the factory when the program is loaded.
// Then the bytecode gets generated just once.
static final Factory factory =
  new FactoryBuilder()
    .list()
    .elementType(Integer.TYPE)
    .modifiable(false)
    .factory();

int[] ints = {1000, 1001, 1002};
List list = factory.createIntList(ints);
13. Nov 2011, 02:19 CET | Link

I wonder if Ceylon prevents the following which is valid in Java:

public class C {
    public void f() {}
}

public interface I {
    public void f() {}
}

public class SubCImpI
       extends C
       implements I {
    // no compile error: implements I.f(), because C.f() exists. But C.f() was never intended to implement I.f()!!
}

Here is my attempt at it in Ceylon:

shared class C {
    shared default f();     
}

shared interface I {
    shared formal f();     
}

public class SubCImpI
       extends C()
       satisfies I {
    // I think we should get an error, possibly something similar to that:
    // "error: refines two different members", keeping in mind this error message is not good enough here.
}

And what would be alternative if we couldn't easily rename the method from class C or interface I?

For example, changing C.f() or I.f() names because a lot of code could depend on C and I making it impractical to easily change the names.

13. Nov 2011, 12:25 CET | Link

@Djano: the error you will get is:

may not inherit two declarations with the same name that do not share a common supertype: .I::f and .C::f

(nb: I had to slightly change the syntax of the above line to have the blog editor accept it) just for completeness I'll include the corrected code below:

shared class C() {
    shared default void f() {}     
}

shared interface I {
    shared formal void f();     
}

shared class SubCImpI()
       extends C()
       satisfies I {
    // I think we should get an error, possibly something similar to that:
    // "error: refines two different members", keeping in mind this error message is not good enough here.
}
Post Comment
Name:
E-mail address (optional):
Homepage URL (optional):
Subject:
Help
Let me type some plain text, not markup
Enable live preview
Enter characters (ignore circles):