Help

I'm the creator of Hibernate, a popular object/relational persistence solution for Java, and Seam, an application framework for enterprise Java. I've also contributed to the Java Community Process standards as Red Hat representative for the EJB, JPA, JSF specifications and as spec lead of the CDI specification. At Red Hat, I'm currently working on Ceylon, a new programming language for the JVM.

I also post stuff on G+.

Location: Guanajuato, Mexico, cabrones!
Occupation: Fellow at JBoss, a Division of Red Hat
Archive 'Introduction to Ceylon'
My Books
Java Persistence with Hibernate
with Christian Bauer
November 2006
Manning Publications
841 pages (English), PDF ebook
Hibernate in Action
with Christian Bauer
August 2004
Manning Publications
408 pages (English), PDF ebook
23. May 2011, 22:18 CET, by Gavin King

This is the final 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 2/6/2011 to reflect changes to the way annotation constraints are defined. The comment thread reflects information in the first version of article.

Annotations

If you've made it this far into this series of articles, you've already seen lots of annotations. Annotations are so important in Ceylon that it's extremely difficult to write any code without using them. But we have not yet really explored what an annotation is.

Let's finally rectify that. The answer is simple: an annotation is a toplevel method that returns a subtype of ConstrainedAnnotation. Here's the definition of a some of our old friends:

shared Deprecated deprecated() {
    return Deprecated();
}
shared Description doc(String description) {
    return Description(description.normalize());
}
shared Authors by(String... authors) {
    return Authors( from (authors) select (String name) (name.normalize()) );
}

(Note that the third example uses the syntax introduced in this blog entry.)

Of course, we can define our own annotations. (That's the whole point!)

shared Scope scope(Scope s) { return s; }
shared Todo todo(String text) { return Todo(text); }

Since annotations are methods, annotation names always begin with a lowercase letter.

Annotation arguments

When we specify an annotation with a non-empty parameter list at a program element, we need to specify arguments for the parameters of the annotation. Just like with a normal method invocation, we have the choice between a positional argument list or a named argument list. We could write:

doc ("The Hello World program")

or:

doc { description="The Hello World program"; }

Likewise, we could write:

by ("Gavin", "Stephane", "Emmanuel")

or:

by { "Gavin", "Stephane", "Emmanuel" }

But with annotations whose arguments are all literal values, we have a third option. We can completely eliminate the punctuation, and just list the literal values.

doc "The Hello World program"
by "Gavin" 
   "Stephane" 
   "Emmanuel"

As a special case of this, if the annotation has no arguments, we can just write the annotation name and leave it at that. We do this all the time with annotations like shared, formal, default, actual, abstract, deprecated, and variable.

Annotation types

The return type of an annotation is called the annotation type. Multiple methods may produce the same annotation type. An annotation type must be a subtype of ConstrainedAnnotation:

doc "An annotation. This interface encodes
     constraints upon the annotation in its
     type arguments."
shared interface ConstrainedAnnotation<out Value, out Values, in ProgramElement>
        of OptionalAnnotation<Value,ProgramElement> | SequencedAnnotation<Value,ProgramElement>
        satisfies Annotation<Value>
        given Value satisfies Annotation<Value>
        given ProgramElement satisfies Annotated {
    shared Boolean occurs(Annotated programElement) {
        return programElement is ProgramElement;
    }
}

The type arguments of this interface express constraints upon how annotations which return the annotation type occur. The first type parameter, Value, is simply the annotation type itself.

Annotation constraints

The second type parameter, Values, governs how many different annotations of given program element may return the annotation type. Notice that ConstrainedAnnotation has an of clause telling us that there are only two direct subtypes. So any annotation type must be a subtype of one of these two interfaces:

  • If an annotation type is a suptype of OptionalAnnotation, at most one annotation of a given program element may be of this annotation type, or, otherwise
  • if an annotation type is a suptype of SequencedAnnotation, more than one annotation of a given program element may be of this annotation type.
doc "An annotation that may occur at most once at 
     a single program element."
shared interface OptionalAnnotation<out Value, in ProgramElement>
        satisfies ConstrainedAnnotation<Value,Value?,ProgramElement>
        given Value satisfies Annotation<Value>
        given ProgramElement satisfies Annotated {}
doc "An annotation that may occur multiple times at 
     a single program element."
shared interface SequencedAnnotation<out Value, in ProgramElement>
        satisfies ConstrainedAnnotation<Value,Value[],ProgramElement>
        given Value satisfies Annotation<Value>
        given ProgramElement satisfies Annotated {}

Finally, the third type parameter, ProgramElement, of ConstrainedAnnotation constrains the kinds of program elements at which the annotation can occur. The argument to ProgramElement must be a metamodel type. So the argument Type<Number> would constrain the annotation to occur only at program elements that declare a subtype of Number. The argument Attribute<Bottom,String> would constrain the annotation to occur only at program elements that declare an attribute of type String.

Here's a couple of examples I copied and pasted straight from the language spec:

shared interface Scope
        of request | session | application
        satisfies OptionalAnnotation<Scope,Type<Object>> {}
shared class Todo(String text)
        satisfies SequencedAnnotation<Todo,Annotated> {
    shared actual String string = text;
}

Reading annotation values at runtime

Annotation values may be obtained by calling the toplevel method annotations() defined in the language module.

shared Values annotations<Value,Values,ProgramElement>(
               Type<ConstrainedAnnotation<Value,Values,ProgramElement>> annotationType,
               ProgramElement programElement)
           given Value satisfies ConstrainedAnnotation<Value,Values,ProgramElement>
           given ProgramElement satisfies Annotated { ... }

So to obtain the value of the doc annotation of the Person class, we write:

String? description = annotations(Description, Person)?.description;

Note that the expression Person returns the metamodel object for the class Person, an instance of ConcreteClass<Person>.

To determine if the method stop() of a class named Thread is deprecated, we can write:

Boolean deprecated = annotations(Deprecated, Thread.stop) exists;

Note that the expression Thread.stop returns the metamodel object for the method stop() of Thread, an instance of Method<Thread,Void>.

Here's two more examples, to make sure you get the idea:

Scope scope = annotations(Scope, Person) ? request;
Todo[] todos = annotations(Todo, method);

Yeah, everything's set up so that annotations() returns Scope? for the optional annotation type Scope, and Todo[] for the sequenced annotation type Todo. Nice, huh?

Of course, it's much more common to work with annotations in generic code, so you're more likely to be writing code like this:

Entry<Attribute<Bottom,Object?>,String>[] attributeColumnNames(Class<Object> clazz) {
	return from (clazz.members(Attribute<Bottom,Object?>))
	        select (Attribute<Bottom,Object?> att) (att->columnName(att));
}

String columnName(Attribute<Bottom,Object?> member) {
    return annotations(Column, member)?.name ? member.name;
}

As you can see, Ceylon annotations are framework-developer-heaven.

Defining annotations

We've seen plenty of examples of annotations built into Ceylon. Application developers don't often define their own annotations, but framework developers do this all the time. Let's see how we could define an annotation for declarative transaction management in Ceylon.

Transactional transactional(Boolean requiresNew = false) {
    return Transactional(requiresNew);
}

This method simply produces an instance of the class Transactional that will be attached to the metamodel of an annotated method or attribute. The meta-annotation specifies that the annotation may be applied to methods and attributes, and may occur at most once on any member.

shared class Transactional(Boolean requiresNew) 
        satisfies OptionalAnnotation<Transactional,Member<Bottom,Void>> {
    shared Boolean requiresNew = requiresNew;
}

Now we can apply our annotation to a method of any class.

shared class OrderManager() {
    shared transactional void createOrder(Order order) { ... }
    ...
}

We could specify an explicit argument to the parameter of transactional using a positional argument list:

shared transactional (true) 
void createOrder(Order order) { ... }

Alternatively, we could use a named argument list:

shared transactional { requiresNew=true; }
void createOrder(Order order) { ... }

We won't need to use reflection in our example, since Ceylon's module architecture includes special built-in support for using annotations to add interceptors to methods and attributes.

Interceptors

An interceptor allows frameworks to react to events like method invocations, class instantiations, or attribute evaluations. We don't need to write any special annotation scanning code to make use of interceptors. Ceylon handles this for us at class-loading time.

All we need to do is have our Transactional class implement the interfaces MethodAnnotation and AttributeAnnotation:

shared class Transactional(Boolean requiresNew)
        satisfies OptionalAnnotation<Transactional,Member<Bottom,Void>> &
                  MethodAnnotation & AttributeAnnotation {
        
    shared Boolean requiresNew = requiresNew;
    
    doc "This method is called whenever Ceylon loads a class with a method
         annotated |transactional|. It registers a transaction management
         interceptor for the method."
    shared actual void onDefineMethod<Instance,Result,Argument...>(OpenMethod<Instance,Result,Argument...> method) {
        method.intercept()
                onInvoke(Instance instance, Result proceed(Argument... args), Argument... args) {
            if (currentTransaction.inProcess || !requiresNew) {
                return proceed(args);
            }
            else {
                currentTransaction.begin();
                try {
                    Result result = proceed(args);
                    currentTransaction.commit();
                    return result;
                }
                catch (Exception e) {
                    currentTransaction.rollback();
                    throw e;
                }
            }
        }
    }
    
    doc "This method is called whenever Ceylon loads a class with an attribute
         annotated |transactional|. It registers a transaction management
         interceptor for the attribute."
    shared actual void onDefineAttribute<Instance,Result>(OpenAttribute<Instance,Result> attribute) {
        attribute.intercept()
                onGet(Instance instance, Result proceed()) {
            if (currentTransaction.inProcess || !requiresNew) {
                return proceed();
            }
            else {
                currentTransaction.begin();
                try {
                    Result result = proceed();
                    currentTransaction.commit();
                    return result;
                }
                catch (Exception e) {
                    currentTransaction.rollback();
                    throw e;
                }
            }
        }
    }
    
}

The intercept() method registers the interceptor - a kind of callback method. Again, we're using the syntax discussed here.

Conclusion

I think this is probably a good place to end this series for now, since I've covered all of the bits of Ceylon that have been properly worked out at this stage. At some stage I'll go back and update / clean up these posts and perhaps even reorganize the material a bit. Thanks for following along, and please keep letting us know your feedback!

21. May 2011, 06:51 CET, by Gavin King

This is the eleventh 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 2/6/2011 to mention definite initialization of methods and on 28/5/2011 to reflect refinements to the language specification, and add new material dealing with self references, outer instance references, and circular references. The comment thread reflects information in the first version of article.

Self references and outer instance references

Ceylon features the keywords this and super, which refer to the current instance of a class — the receiving instance of an operation (method invocation, member class instantiation, or attribute evaluation/assignment), within the body of the definition of the operation. The semantics are exactly the same as what you're used to in Java. In particular, a reference to a member of super always refers to a member of a superclass. There is currently no syntax defined for references to a concrete member of a superinterface.

In addition to this and super, Ceylon features the keyword outer, which refers to the parent instance of the current instance of a nested class.

class Parent(String name) {
    shared String name = name;
    shared class Child(String name) {
        shared String name = outer.name + "/" + name;
        shared Parent parent { return outer; }
    }
}

There are some restrictions on the use of this, super, and outer, which we'll explore below.

Multiple inheritance and linearization

There's a good reason why super always refers to a superclass, and never to a superinterface.

Ceylon features a restricted kind of multiple inheritance often called mixin inheritance. Some languages with multiple inheritance or even mixin inheritance feature so-called depth-first member resolution or linearization where all supertypes of a class are arranged into a linear order. We believe that this model is arbitrary and fragile.

Ceylon doesn't perform any kind of linearization of supertypes. The order in which types appear in the satisfies clause is never significant. The only way one supertype can take precedence over another supertype is if the first supertype is a subtype of the second supertype. The only way a member of one supertype can take precedence over a member of another supertype is if the first member refines the second member.

In our view, there's no non-fragile basis for deciding that one type specializes another type unless the first type is explicitly defined to be a subtype of the second. There's no non-fragile basis for deciding that one operation is more specific than another operation unless the first operation is explicitly declared to refine the second.

For a similar reason, interfaces shouldn't be able to define initialization logic. There's no non-fragile way to define the ordering in which supertype initializers are executed in a multiple-inheritance model. This is the basic reason why interfaces are stateless in Ceylon.

(Note that these arguments are even stronger in the case of adapter introduction, where linearization or statefulness would be even more fragile.)

So Ceylon is more restrictive than some other languages here. But we think that this restriction makes a subtype less vulnerable to breakage due to changes in its supertypes.

Definite assignment and definite initialization

A really nice feature of Java is that the compiler checks that a local variable has definitely been assigned a value before allowing use of the local variable in an expression. So, for example, the following code compiles without error:

String greeting;
if (person==me) {
    greeting = "You're beautiful!";
}
else {
    greeting = "You're ugly!";
}
print(greeting);

But the following code results in an error at compile time:

String greeting;
if (person==me) {
    greeting = "You're beautiful!";
}
print(greeting);   //error: greeting not definitely initialized

Many (most?) languages don't perform this kind of static analysis, which means that use of an uninitialized variable results in an error at runtime instead of compile time.

Unfortunately, Java doesn't do this same kind of static analysis for instance variables, not even for final instance variables. Instead, an instance variable which is not assigned a value in the constructor is initialized to a default value (zero or null). Surprisingly, it's even possible to see this default value for a final instance variable that is eventually assigned a value by the constructor. Consider the following code:

//Java code that prints "null"
class Broken {
    final String greeting;
    
    Broken() {
        print();
        greeting = "Hello";
    }

    void print() {
        System.out.println(greeting);
    }

}
new Broken();

This behavior is bad enough in and of itself. But it would be even less acceptable in Ceylon, where most types don't have an acceptable default value. For example, consider the type Person. What would be an acceptable default value of this type? The value null certainly won't do, since it's not even an instance of Person. (It's an instance of Nothing, remember!) I suppose we could say that evaluation of an uninitialized instance variable always results in an immediate runtime exception, but this is really just our old friend NullPointerException creeping back in by the back door, and, well, it's Just Not How We Do Things Around Here.

Indeed, few object-oriented languages (i.e. none that I know of) perform the necessary static analysis to ensure definite initialization of instance variables, and I believe that this is perhaps one main reason why object-oriented languages have never featured typesafe handling of null values.

Class bodies

In order to make it possible for the compiler to guarantee definite initialization of attributes, Ceylon imposes some restrictions on the body of a class. (Remember that Ceylon doesn't have constructors!) Actually, to be completely fair, they're not really restrictions at all, at least not from one point of view, since you're actually allowed extra flexibility in the body of a class that you're not allowed in the body of method or attribute declarations! But compared to Java, there's some things you're not allowed to do.

First, we need to know that the compiler automatically divides the body of the class into two sections:

  1. First comes the initializer section, which contains a mix of declarations, statements and control structures. The initializer is executed every time the class is instantiated.
  2. Then comes the declaration section, which consists purely of declarations, similar to the body of an interface.

Now we're going to introduce some rules that apply to code that appears in each section. The purpose of these rules is to guarantee that an instance variable has had a value specified or assigned before its value is used in an expression.

But you don't need to actually explicitly think about these rules when you write code. Only very rarely will you need to think about the initializer section and declaration section in explicit terms. The compiler will let you know when you break the rules, and force you to fix your code.

Initializer section

The initializer section is responsible for initializing the state of the new instance of the class, before a reference to the new instance is available to clients. The declaration section contains members of the class which are only called after the instance has been fully initialized.

Consider the following example:

class Hello(String? name) {
    
    //initializer section:

    String greetingForTime {
        if (morning) {
            return "Good morning";
        }
        else if (afternoon) {
            return "Good afternoon";
        }
        else if (evening) {
            return "Good evening";
        }
        else {
            return "Hi";
        }
    }
    
    String greeting;
    if (exists name) {
        greeting = greetingForTime + ", " + name;
    }
    else {
        greeting = greetingForTime;
    }
    
    //declaration section:
    
    shared void say() {
        print(greeting);
    }
    
    default void print(String message) {
        writeLine(message);
    }
    
}

To prevent a reference to a new instance of the class leaking before the new instance has been completely initialized, the language spec defines the following terminology:

Within a class initializer, a self reference to the instance being initialized is either:
  • the expression this, unless contained in a nested class declaration, or
  • the expression outer, contained in a directly nested class declaration.

Now, according to the language spec:

A statement or declaration that appears within the initializer of a class may not:
  • evaluate attributes, invoke methods, or instantiate member classes that are declared later in the body of the class upon the instance that is being initialized, including upon a self reference to the instance being initialized.
  • pass a self reference to the instance being initialized as an argument of an instantiation or method invocation or as the value of an attribute assignment or specification.
  • return a self reference to the instance being initialized.
  • evaluate attributes, invoke methods, or instantiate member classes declared in the declaration section of a superclass of the instance being initialized, including upon a self reference to the instance being initialized.
  • invoke or evaluate a formal member of the instance being initialized, including upon a self reference to the instance being initialized.
  • invoke or evaluate a default member of the instance that is being initialized, except via the special super self reference.

Declaration section

The declaration section contains the definition of members that don't hold state, and that are never called until the instance to which they belong has been completely initialized.

According to the language spec:

[The declaration section] may not contain:
  • a statement or control structure, unless it is nested inside a method, attribute, nested class, or nested interface declaration,
  • a declaration with a specifier or initializer, unless it is nested inside a method, attribute, nested class, or nested interface declaration,
  • an object declaration with a non-empty initializer section, or
  • a specification or initialization statement for a member of the instance being initialized.
However, the declarations in this second section may freely use this and super, and may invoke any method, evaluate any attribute, or instantiate any member class of the class or its superclasses. Furthermore, the usual restriction that a declaration may only be used by code that appears later in the block containing the declaration is relaxed.

Note that the rules governing the declaration section of a class body are essentially the same rules governing the body of an interface. That makes sense, because interfaces don't have initialization logic — what interfaces and declaration sections have in common is statelessness.

Circular references

Unfortunately, these rules make it a little tricky to set up circular references between two objects without resort to non-variable attributes. This is a problem Ceylon has in common with functional languages, which also emphasize immutability. We can't write the following code in Ceylon:

abstract class Child(Parent p) {
    shared formal Parent parent = p;
}

class Parent() {
    shared Child child = Child(this); //compile error (this passed as argument in initializer section)
}

Eventually, Ceylon will probably need some specialized machinery for dealing with this problem, but for now, here is a partial solution:

abstract class Child() {
    shared formal Parent parent;
}

class Parent() {
    shared object child extends Child() {
        shared actual parent {
            return outer;
        }
    }    
}

Definite initialization of methods

Ceylon lets us separate the declaration of a method defined using a method reference from the actual specification statement that specifies the method reference.

Float x = ... ;
Float op(Float y);
switch (symbol)
case ("+") { op = x.plus; }
case ("-") { op = x.minus; }
case ("*") { op = x.times; }
case ("/") { op = x.divided; }

The rules for definite initialization of locals and attributes also apply to methods defined using a specification statement.

Definite return

While we're on the topic, it's worth noting that the Ceylon compiler, just like the Java compiler, also performs definite return checking, to ensure that a method or getter always has an explicitly specified return value. So, this code compiles without error:

String greeting {
    if (person==me) {
        return "You're beautiful!";
    }
    else {
        return "You're ugly!";
    }
}

But the following code results in an error at compile time:

String greeting {   //error: greeting does not definitely return
    if (person==me) {
        return "You're beautiful!";
    }
}

There's more...

In the Part 12, we're going to discuss annotations, and take a little peek at using the metamodel to build framework code.

05. May 2011, 05:41 CET, by Gavin King

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

An overview of the language module

The module ceylon.language contains classes and interfaces that are referred to in the language specification, other declarations they refer to, and a number of related declarations. Let's meet the main characters.

Just like Java, Ceylon has a class named Object.

shared abstract class Object() 
        extends Void() {
        
    doc "A developer-friendly string representing the instance."
    shared formal String string;
    
    doc "Determine if this object belongs to the given Category
         or is produced by the iterator of the given Iterable
         object."
    shared Boolean element(Category|Iterable<Equality> category) {
        switch (category)
        case (is Category) {
            return category.contains(this);
        }
        case (is Iterable<Equality>) {
            if (is Equality self = this) {
                for (Equality x in category) {
                    if (x==self) {
                        return true;
                    }
                }
                fail {
                    return false;
                }
            }
            else {
                return false;
            }
        }
    }
}

In Ceylon, Object isn't the root of the type system. An expression of type Object has a definite, well-defined, non-null value. As we've seen, the Ceylon type system can also represent some more exotic types, for example Nothing, which is the type of null.

Therefore, Ceylon's Object has a superclass, named Void, which we already met in Part 1. All Ceylon types are assignable to Void. Expressions of type Void aren't useful for very much, since Void has no members or operations. You can't even narrow an expression of type Void to a different type. The one useful thing you can do with Void is use it to represent the signature of a method when you don't care about the return type, since a method declared void is considered to have return type Void, as we saw in Part 8.

As we also saw in Part 1, the type Nothing directly extends Void. All types that represent well-defined values extend Object, including:

  • user-written classes,
  • all interfaces, and
  • the types that are considered primitive in Java, such as Integer, Float and Character.

Since an expression of type Object always evaluates to a definite, well-defined value, it's possible to obtain the runtime type of an Object, or narrow an expression of type Object to a more specific type.

Equality and identity

On the other hand, since Object is a supertype of types like Float which are passed by value at the level of the Java Virtual Machine, you can't use the === operator to test the identity of two values of type Object. Instead, there is a subclass of Object, named IdentifiableObject, which represents a type which is always passed by reference. The === operator accepts expressions of type IdentifiableObject. It's possible for a user-written class to directly extend Object, but most of the classes you write will be subclasses of IdentifiableObject. All classes with variable attributes must extend IdentifiableObject.

shared abstract class IdentifiableObject() 
        extends Object() 
        satisfies Equality {

    shared default actual Boolean equals(Equality that) {
        if (is IdentifiableObject that) {
            return this===that;
        }
        else {
            return false;
        }
    }
    
    shared default actual Integer hash {
        return identityHash(this);
    }
    
    shared default actual String string {
        ...
    }
        
}

IdentifiableObject defines a default implementation of the interface Equality, which is very similar to the equals() and hashCode() methods defined by java.lang.Object.

shared interface Equality {
    
    shared formal Boolean equals(Equality that);
    
    shared formal Integer hash;
    
}

Just like in Java, you can refine this default implementation in your own classes. This is the normal way to get a customized behavior for the == operator, the only constraint being, that for subtypes of IdentifiableObject, x===y should imply x==y — equality should be consistent with identity.

Occasionally that's not what we want. For example, for numeric types, I don't care whether a value is of class Natural, Integer, or Whole when comparing it to 0. Fortunately, numeric types extend Object directly, and are not subject to the additional constraints defined by IdentifiableObject.

Thus, Ceylon is able to capture within the type system much of the behavior that Java introduces by fiat special-case rules in the language definition.

Operator polymorphism

Ceylon discourages the creation of intriguing executable ASCII art. Therefore, true operator overloading is not supported by the language. Instead, almost every operator (every one except the primitive ., (), is, and := operators) is considered a shortcut way of writing some more complex expression involving other operators and ordinary method calls. For example, the < operator is defined in terms of the interface Comparable<Other>, which we met in Part 5, and which has a method named smallerThan(), which is in turn defined in terms of another method named compare().

x<y

means, by definition,

x.smallerThan(y)

The equality operator == is defined in terms of the interface Equality, which has a method named equals().

x==y

means, by definition,

x.equals(y)

Therefore, it's easy to customize operators like < and == with specific behavior for our own classes, just by implementing or refining methods like compare() and equals(). Thus, we say that operators are polymorphic in Ceylon.

Apart from Comparable and Equality, which provide the underlying definition of comparison and equality operators, the following interfaces are also important in the definition of Ceylon's polymorphic operators:

  • Summable supports the infix + operator,
  • Invertable supports the prefix + and - operators,
  • Numeric supports the other basic arithmetic operators,
  • Slots supports bitwise operators,
  • Comparable supports the comparison operators,
  • Correspondence and Sequence support indexing and subrange operators, and
  • Boolean is the basis of the logical operators.

Operator polymorphism is a little more flexible than you might imagine. Here's a quick example of this.

The Slots interface

The interface Slots is an abstraction of the idea of a set of slots which may each hold true or false. The bitwise operators &, |, and ~ are defined in terms of this interface. The most obvious subtype of Slots would be a Byte class, where the slots are the eight binary digits.

But the interface Set from the collections module also extends Slots. The slots of a Set are values which may or may not belong to the set. A slot holds true if the value it represents belongs to the Set. The practical value of this is to allow the use of the operator | for set union, the operator & for set intersection, and the infix ~ operator for set complement.

Set<Person> children = males|females ~ adults;

Yes, I realize that these aren't the traditional symbols representing these operations. But if you think carefully about the definition of these operations, I'm pretty sure you'll agree that these symbols are reasonable.

We could even define a Permission class that implements Slots, allowing us to write things like permissions&(read|execute).

Numeric types

As we've mentioned several times before, Ceylon doesn't have anything like Java's primitive types. The types that represent numeric values are just ordinary classes. Ceylon has fewer built-in numeric types than other C-like languages:

  • Natural represents the unsigned integers and zero,
  • Integer represents signed integers,
  • Float represents floating point approximations to the real numbers,
  • Whole represents arbitrary-precision signed integers, and
  • Decimal represents arbitrary-precision and arbitrary-scale decimals.

Natural, Integer and Float have 64-bit precision by default. Eventually, you'll be able to specify that a value has 32-bit precision by annotating it small. But note that this annotation is really just a hint that the compiler is free to ignore (and it currently does).

Numeric literals

There are only two kinds of numeric literals: literals for Naturals, and literals for Floats:

Natural one = 1;
Float oneHundredth = 0.01;
Float oneMillion = 1.0E+6;

The digits of a numeric literal may be grouped using underscores. If the digits are grouped, then groups must contain exactly three digits.

Natural twoMillionAndOne = 2_000_001;
Float pi = 3.141_592_654;

A very large or small numeric literals may be qualified by one of the standard SI unit prefixes: m, u, n, p, f, k, M, G, T, P.

Float red = 390.0n; //n (nano) means E-9
Float galaxyDiameter = 900.0P; //P (peta) means E15
Float hydrogenRadius = 25.0p; //p (pico) means E-12
Float usGovDebt = 14.33T; //T (tera) means E12
Float brainCellSize = 4.0u; //u (micro) means E-6
Natural deathsUnderCommunism = 94M; //M (mega) means E6

Numeric widening

I mentioned earlier that Ceylon doesn't have implicit type conversions, not even built-in conversions for numeric types. Assignment does not automatically widen (or narrow) numeric values. Instead, we need to call one of the operations (well, attributes, actually) defined by the interface Number.

Whole zero = 0.whole; //explicitly widen from Natural
Decimal half = 0.5.decimal; //explicitly widen from Float

Usefully, the unary prefix operators + and - always widen Natural to Integer:

Integer negativeOne = -1;
Integer three = +3;

You can use all the operators you're used to from other C-style languages with the numeric types. You can also use the ** operator to raise a number to a power:

Float diagonal = (length**2.0+width**2.0)**0.5;

Of course, if you want to use the increment ++ operator, decrement -- operator, or one of the compound assignment operators such as +=, you'll have to declare the value variable.

Since it's quite noisy to explicitly perform numeric widening in numeric expressions, the numeric operators automatically widen their operands, so we could write the expression above like this:

Float diagonal = (length**2+width**2)**(1.0/2);

The built-in widening conversions are the following:

  • Natural to Integer, Float, Whole, or Decimal
  • Integer to Float, Whole, or Decimal
  • Float to Decimal
  • Whole to Decimal

But these conversions aren't defined by special-case rules in the language specification.

Numeric operator semantics

Operators in Ceylon are, in principle, just abbreviations for some expression involving a method call. So the numeric types all implement the Numeric interface, refining the methods plus(), minus(), times(), divided() and power(), and the Invertable interface, refining inverse. The numeric operators are defined in terms of these methods of Numeric. The numeric types also implement the interface Castable, which enables the widening conversions we just mentioned.

shared interface Castable<in Types> {
    shared formal CastValue as<CastValue>()
        given CastValue satisfies Types;
}

The type parameter Types uses a special trick. The argument to Types should be the union of all types to which the implementing type is castable.

For example, simplifying slightly the definitions in the language module:

shared class Natural(...)
        extends Object()
        satisfies Castable<Natural|Integer|Float|Whole|Decimal> &
                  Numeric<Natural> &
                  Invertable<Integer> {
    ...
}
shared class Integer(...)
        extends Object()
        satisfies Castable<Integer|Float|Whole|Decimal> &
                  Numeric<Integer> & 
                  Invertable<Integer> {
    ...
}
shared class Float(...)
        extends Object()
        satisfies Castable<Float|Decimal> &
                  Numeric<Float> & 
                  Invertable<Float> {
    ...
}

These declarations tell us that Integer can be widened to Float, Whole, or Decimal, but that Float can only be widened to Decimal. So we can infer that the expression -1 * 0.4 is of type Float.

Therefore, the definition of a numeric operator like * can be represented, completely within the type system, in terms of Numeric and Castable:

Result product<Left,Right,Result>(Left x, Right y)
        given Result of Left|Right satisfies Numeric<Result>
        given Left satisfies Castable<Result> & Numeric<Left>
        given Right satisfies Castable<Result> & Numeric<Right> {
    return x.as<Result>().times(y.as<Result>());
}

Don't worry too much about the performance implications of all this — in practice, the compiler is permitted to optimize the types Natural, Integer, and Float down to the virtual machine's native numeric types.

The value of all this — apart from eliminating special cases in the language definition and type checker — is that a library can define its own specialized numeric types, without losing any of the nice language-level syntax support for numeric arithmetic and numeric widening conversions.

There's more...

If you're interested, you can check out a complete list of Ceylon's operators along with a discussion of their precedence.

In the Part 11 we're going to come back to the subject of object initialization, and deal with a subtle problem affecting languages like Java and C#.

02. May 2011, 01:13 CET, by Gavin King

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

Named arguments

Consider the following method:

void printf(OutputStream to, String format, Object... values) { ... }

(Remember, the last parameter is a sequenced parameter which accepts multiple arguments, just like a Java varargs parameter.)

We've seen lots of examples of invoking a method or instantiating a class using a familiar C-style syntax where arguments are delimited by in parentheses and separated by commas. Arguments are matched to parameters by their position in the list. Let's see just one more example, just in case:

printf(process, "Thanks, %s. You have been charged %.2f. Your confirmation number is %d.", 
        user.name, order.total, order.confimationNumber);

This works fine, I suppose. However, Ceylon provides an alternative method invocation protocol that is usually easier to read when there are more than one or two arguments:

printf { 
    to = process;
    format = "Thanks, %s. You have been charged %.2f. Your confirmation number is %d."; 
    user.name, order.total, order.confimationNumber
};

This invocation protocol is called a named argument list. We can recognize a named argument list by the use of braces as delimiters instead of parentheses. Notice that arguments are separated by semicolons, except for arguments to the sequenced parameter, which are separated by commas. We explicitly specify the name of each parameter, except for the sequenced parameter, whose arguments always appear at the end of the named parameter list. Note that it's also acceptable to call this method like this, passing a sequence to the named value parameter:

printf { 
    to = process;
    format = "Thanks, %s. You have been charged %.2f. Your confirmation number is %d."; 
    values = { user.name, order.total, order.confimationNumber };
};

We usually format named argument invocations across multiple lines.

Declarative object instantiation syntax

Named arguments are very commonly used for building graphs of objects. Therefore, Ceylon provides a special abbreviated syntax that simplifies the declaration of an attribute getter, named parameter, or method that builds an object by specifying named arguments to the class initializer.

We're allowed to abbreviate an attribute definition of the following form:

Payment payment = Payment { 
    method = user.paymentMethod;
    currency = order.currency; 
    amount = order.total;
};

or a named argument specification of this form:

payment = Payment { 
    method = user.paymentMethod; 
    currency = order.currency; 
    amount = order.total;
};

to the following more declarative (and less redundant) style:

Payment payment { 
    method = user.paymentMethod; 
    currency = order.currency; 
    amount = order.total;
}

We're even allowed to write a method of the following form:

Payment createPayment(Order order) { 
    return Payment {
        method = user.paymentMethod; 
        currency = order.currency; 
        amount = order.total;
    };
}

using the following abbreviated syntax:

Payment createPayment(Order order) { 
    method = user.paymentMethod; 
    currency = order.currency; 
    amount = order.total;
}

Perhaps you're worried that this looks like a method that assigns the values of three attributes of the declaring class, rather than a shortcut syntax for a named argument instantiation of the Payment class. And that's a very fair point. To a Java developer, that is what it looks like. There's two things that should alert you to what's really going on. The above method:

  • has no return statement, but it's not declared void, and
  • contains a list of = specification statements instead of := assignment expressions.

Once you're used to Ceylon's more flexible syntax, these differences will usually stand out immediately.

More about named arguments

The following classes define a data structure for building tables:

class Table(String title, Natural rows, Border border, Column... columns) { ... } 
class Column(String heading, Natural width, String content(Natural row)) { ... } 
class Border(Natural padding, Natural weight) { ... }

Of course, we could built a Table using positional argument lists:

String x(Natural row) { return row.string; }
String xSquared(Natural row) { return (row**2).string; }
Table table = Table("Squares", 5, Border(2,1), Column("x",10, x), Column("x**2",12, xSquared));

However, it's far more common to use named arguments to build a complex graph of objects. In this section we're going to meet some new features of named argument lists, that make it especially convenient to build object graphs.

First, note that the syntax we've already seen for specifying a named argument value looks exactly like the syntax for refining a formal attribute. If you think about it, taking into account that a method parameter may accept references to other methods, the whole problem of specifying values for named parameters starts to look a lot like the problem of refining abstract members. Therefore, Ceylon will let us reuse much of the member declaration syntax inside a named argument list. (But note that this has not yet been implemented in the compiler.)

It's legal to include the following constructs in a named argument list:

  • method declarations — specify the value of a parameter that accepts a function,
  • object (anonymous class) declarations — are most useful for specifying the value of a parameter whose type is an interface or abstract class, and
  • getter declarations — lets us compute the value of an argument inline.

This helps explain why named argument lists are delimited by braces: the fully general syntax for a named argument list is very, very close to the syntax for a class, method, or attribute body. Notice, again, how flexibility derives from language regularity.

So we could rewrite the code that builds a Table as follows:

Table table = Table { 
    title="Squares";
    rows=5;
    border = Border {
        padding=2;
        weight=1;
    };
    Column {
        heading="x";
        width=10;
        String content(Natural row) {
            return row.string;
        }
    }, 
    Column {
        heading="x**2";
        width=12;
        String content(Natural row) {
            return (row**2).string;
        }
    }
};

Notice that we've specified the value of the parameter named content using the usual syntax for declaring a method.

Even better, our example can be abbreviated like this:

Table table { 
    title="Squares";
    rows=5;
    Border border {
        padding=2;
        weight=1;
    } 
    Column {
        heading="x";
        width=10;
        String content(Natural row) {
            return row.string;
        }
    }, 
    Column {
        heading="x**2"; 
        width=10;
        String content(Natural row) {
            return (row**2).string;
        }
    }
}

Notice how we've transformed our code from a form which emphasized invocation into a form that emphasizes declaration of a hierarchical structure. Semantically, the two forms are equivalent. But in terms of readability, they are very different.

We could put the above totally declarative code in a file by itself and it would look like some kind of mini-language for defining tables. In fact, it's executable Ceylon code that may be validated for syntactic correctness by the Ceylon compiler and then compiled to Java bytecode. Even better, the Ceylon IDE (when it exists) will provide authoring support for our mini-language. In complete contrast to the DSL support in some dynamic languages, any Ceylon DSL is completely typesafe! You can think of the definition of the Table, Column and Border classes as defining the schema or grammar of the mini-language. (In fact, they are really defining the syntax tree for the mini-language.)

Now let's see an example of a named argument list with an inline getter declaration:

shared class Payment(PaymentMethod method, Currency currency, Float amount) { ... }
Payment payment { 
    method = user.paymentMethod; 
    currency = order.currency; 
    Float amount {
        variable Float total := 0.0; 
        for (Item item in order.items) {
            total += item.quantity * item.product.unitPrice; 
        }
        return total;
    }
}

Finally, here's an example of a named argument list with an inline object declaration:

shared interface Observable { 
    shared void addObserver(Observer<Bottom> observer) { ... }
}
shared interface Observer<in Event> { 
    shared formal on(Event event);
}
observable.addObserver { 
    object observer satisfies Observer<UpdateEvent> {
        shared actual void on(UpdateEvent e) { 
            writeLine("Update:" + e.string);
        }
    }
};

Note that Observer<T> is assignable to Observer<Bottom> for any type T, since Observer<T> is contravariant in its type parameter T. If this doesn't make sense, please read these two sections again. ;-)

Of course, as we saw in Part 8, a better way to solve this problem might be to eliminate the Observer interface and pass a method directly:

shared interface Observable { 
    shared void addObserver<Event>(void on(Event event)) { ... }
}
observable.addObserver {
    void on(UpdateEvent e) { 
    	writeLine("Update:" + e.string);
    }
};

A quick tangent here: note that we need a type parameter T of the method addObserver() here only because Ceylon inherits Java's limitation that function types are nonvariant in their parameter types. This is actually pretty unnatural. We should probably eventually come up with a workaround to make function types contravariant in their parameter types, allowing us to write:

shared interface Observable { 
    shared void addObserver(void on(Bottom event)) { ... }
}

Defining user interfaces

One of the first modules we're going to write for Ceylon will be a library for writing HTML templates in Ceylon. A fragment of static HTML would look something like this:

Html { 
    Head head {
        title = "Hello World"; 
        cssStyleSheet = 'hello.css';
    } 
    Body body {
        Div { 
            cssClass = "greeting"; 
            "Hello World"
        },
        Div {
            cssClass = "footer"; 
            "Powered by Ceylon"
        }
    }
}

A complete HTML template might look like this:

import ceylon.html { ... }

doc "A web page that displays a greeting" 
page '/hello.html' 
Html hello(Request request) {
    
    Head head { 
        title = "Hello World"; 
        cssStyleSheet = 'hello.css';
    }
    
    Body body { 
        Div {
            cssClass = "greeting"; 
            Hello( request.parameters["name"] ).greeting
        }, 
        Div {
            cssClass = "footer"; 
            "Powered by Ceylon"
        }
    }

};

There's more...

There's plenty of potential applications of this syntax aside from user interface definition. For example, Ceylon lets us use a named argument list to the specify arguments of a program element annotation. But we'll have to come back to the subject of annotations in a future installment. In Part 10 we're going to discuss some of the basic types from the language module, in particular numeric types, and introduce the idea of operator polymorphism.

01. May 2011, 17:40 CET, by Gavin King

This is the eighth 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 31/5/2011 to add information about partial application of methods.

First class and higher order functions

Ceylon isn't a functional language: it has variable attributes and so methods can have side effects. But Ceylon does let you use functions as values, which in some people's eyes makes the language a kind of hybrid. I'm not so sure about that. There's actually nothing at all new about having functions-as-values in an object oriented language — for example, Smalltalk, one of the first and still one of the cleanest object oriented languages, was built around this idea. (To my eyes, true functional programming is more about what you can't do — mutate values — than what you can do.) Anyway, Ceylon, like Smalltalk and a number of other object oriented languages, lets you treat a function as an object and pass it around the system.

In this installment, we're going to discuss Ceylon's support for first class and higher order functions. First class function support means the ability to treat a function as a value. A higher order function is a function that accepts other functions as arguments, or returns another function. It's clear that these two ideas go hand-in-hand, so I'll just talk about higher order function support from now on.

A quick disclaimer: none of the things in this installment have actually been implemented in the compiler yet.

Representing the type of a function

Ceylon is a (very) statically typed language. So if we're going to treat a function as a value, the very first question that arises is: what is the type of the function? We need a way to encode the return type and parameter types of a function into the type system. Remember that Ceylon doesn't have primitive types. A strong design principle is that every type should be representable within the type system as a class or interface declaration.

I suppose Ceylon could have gone down the road of some functional languages, and represented all functions with multiple parameters in curried form. So

Natural sum(Natural x, Natural y) { ... }

would just be an abbreviation of

Natural sum(Natural x)(Natural y) { ... }

i.e. a function with one parameter that returns another function with one parameter. Then we could have represented the type of the function like this:

Function<Natural,Function<Natural,Natural>>

But we've decided not to go down this path.

Some other languages have chosen to have a separate type for each function arity. So there's F0<R>, F1<R,P1>, F2<R,P1,P2>, F3<R,P1,P2,P3>, etc. But this solution feels kinda .... lame. Worse, it doesn't allow us to abstract over all function types, building up abstractions like Method and Class, etc. We're going to need to be able to do that kind of thing when we get to discussing the typesafe metamodel.

In Ceylon, a single type Callable abstracts all functions. It's declaration is the following:

shared interface Callable<out Result, Argument...> {}

The syntax P... is called a sequenced type parameter. By analogy with a sequenced parameter, which accepts zero or more values as arguments, a sequenced type parameter accepts zero or more types as arguments. The type parameter Result represents the return type of the function. The sequenced type parameter Argument... represents the parameter types of the function.

So the type of sum in Ceylon is:

Callable<Natural, Natural, Natural>

What about void functions? Well, remember that way back in Part 1 we said that the return type of a void function is Void. So the type of a function like print() is:

Callable<Void,String>

Representing the type of a method

Here we've been discussing first class functions. But in Ceylon all named declarations are first class. That is to say, they all have a reified metamodel representable within the type system. For example, we could represent the type of a method like this:

shared interface Method<out Result, in Instance, Argument...>
    satisfies Callable<Callable<Result,Argument...>, Instance> {}

Where Instance is the type that declares the method. So the type of the method iterator() of Iterable<String> would be:

Method<Iterator<String>, Iterable<String>>

And the type of the method compare() of Comparable<Natural> would be:

Method<Comparison,Comparable<Natural>,Natural>

Notice that we've declared a method to be a function that accepts a receiver object and returns a function. As a consequence of this, an alternative method invocation protocol is the following:

Iterable<String>.iterator(strings)();
Comparable<Natural>.compare(0)(num);

Don't worry if you can't make sense of that right now. And actually I'm skipping over some details here, that's not quite exactly how Method is defined. But we'll come back to this in a future installment. Let's get back to today's topic.

Defining higher order functions

We now have enough machinery to be able to write higher order functions. For example, we could create a repeat() function that repeatedly executes a function.

void repeat(Natural times, Callable<Void,Natural> perform) {
    for (Natural i in 1..times) {
        perform(i);
    }
}

And call it like this:

void print(Natural n) { writeLine(n); }
repeat(10, print);

Which would print the numbers 1 to 10 to the console.

There's one problem with this. In Ceylon, as we'll see later, we often call functions using named arguments, but the Callable type does not encode the names of the function parameters. So Ceylon has an alternative, more elegant, syntax for declaring a parameter of type Callable:

void repeat(Natural times, void perform(Natural n)) {
    for (Natural i in 1..times) {
        perform(i);
    }
}

I find this version also slightly more readable and more regular. This is the preferred syntax for defining higher-order functions.

Function references

When a name of a function appears without any arguments, like print does above, it's called a function reference. A function reference is the thing that really has the type Callable. In this case, print has the type Callable<Void,Natural>.

Now, remember how we said that Void is both the return type of a void method, and also the logical root of the type hierarchy? Well that's useful here, since it means that we can assign a function with a non-Void return type to any parameter which expects a void method:

Boolean attemptPrint(Natural n) { 
    try {
        writeLine(n);
        return true;
    }
    catch (Exception e) {
        return false;
    }
}
repeat(10, attemptPrint);

Another way we can produce a function reference is by partially applying a method to a receiver expression. For example, we could write the following:

class Hello(String name) {
    shared void say(Natural n) {
        writeLine("Hello, " name ", for the " n "th time!");
    }
}

repeat(10, Hello("Gavin").say);

Here the expression Hello("Gavin").say has the same type as print above. It is a Callable<Void,Natural>.

More about higher-order functions

Let's see a more practical example, which mixes both ways of representing a function type. Suppose we have some kind of user interface component which can be observed by other objects in the system. We could use something like Java's Observer/Observable pattern:

shared interface Observer { 
    shared formal void observe(Event event);
}
shared abstract class Component() {
    
    OpenList<Observer> observers = OpenList<Observer>();
    
    shared void addObserver(Observer o) { 
        observers.append(o); 
    }
    
    shared void fire(Event event) { 
        for (Observer o in observers) { 
            o.observe(event);
        } 
    }

}

But now all event observers have to implement the interface Observer, which has just one method. Why don't we cut out the interface, and let event observers just register a function object as their event listener? In the following code, we define the addObserver() method to accept a function as a parameter.

shared abstract class Component() {
    
    OpenList<Callable<Void,Event>> observers = OpenList<Callable<Void,Event>>();
    
    shared void addObserver(void observe(Event event)) { 
        observers.append(observe); 
    }
    
    shared void fire(Event event) { 
        for (void observe(Event event) in observers) { 
            observe(event);
        } 
    }

}

Here we see the difference between the two ways of specifying a function type:

  • void observe(Event event) is more readable in parameter lists, where observe is the name of the parameter, but
  • Callable<Void,Event> is useful as a generic type argument.

Now, any event observer can just pass a reference to one of its own methods to addObserver():

shared class Listener(Component component) {

    void onEvent(Event e) { 
        //respond to the event 
        ...
    } 
    
    component.addObserver(onEvent); 
    
    ...

}

When the name of a method appears in an expression without a list of arguments after it, it is a reference to the method, not an invocation of the method. Here, the expression onEvent is an expression of type Callable<Void,Event> that refers to the method onEvent().

If onEvent() were shared, we could even wire together the Component and Listener from some other code, to eliminate the dependency of Listener on Component:

shared class Listener() {

    shared void onEvent(Event e) { 
        //respond to the event 
        ...
    } 
    
    ...

}
void listen(Component component, Listener listener) {
    component.addObserver(listener.onEvent);
}

Here, the syntax listener.onEvent is a kind of partial application of the method onEvent(). It doesn't cause the onEvent() method to be executed (because we haven't supplied all the parameters yet). Rather, it results in a function that packages together the method reference onEvent and the method receiver listener.

It's also possible to declare a method that returns a function. A method that returns a function has multiple parameter lists. Let's consider adding the ability to remove observers from a Component. We could use a Subscription interface:

shared interface Subscription {
    shared void cancel();
}
shared abstract class Component() {
    
    ...
    
    shared Subscription addObserver(void observe(Event event)) { 
        observers.append(observe); 
        object subscription satisfies Subscription {
            shared actual void cancel() {
                observers.remove(observe);
            }
        }
        return subscription;
    }
    
    ...

}

But a simpler solution might be to just eliminate the interface and return the cancel() method directly:

shared abstract class Component() {
    
    ...
    
    shared void addObserver(void observe(Event event))() { 
        observers.append(observe); 
        void cancel() {
            observers.remove(observe);
        }
        return cancel;
    }
    
    ...

}

Note the second parameter list of addObserver().

Here, we define a method cancel() inside the body of the addObserver() method, and return a reference to the inner method from the outer method. The inner method cancel() can't be called directly from outside the body of the addObserver() method, since it is a block local declaration. But the reference to cancel() returned by addObserver() can be called by any code that obtains the reference.

Oh, in case you're wondering, the type of the method addObserver() is Callable<Callable<Void>,Component,Callable<Void,Event>>.

Notice that cancel() is able to use the parameter observe of addObserver(). We say that the inner method receives a closure of the non-variable locals and parameters of the outer method — just like a method of a class receives a closure of the class initialization parameters and locals of the class initializer. In general, any inner class, method, or attribute declaration always receives the closure of the members of the class, method, or attribute declaration in which it is enclosed. This is an example of how regular the language is.

We could invoke our method like this:

addObserver(onEvent)();

But if we were planning to use the method in this way, there would be no good reason for giving it two parameter lists. It's much more likely that we're planning to store or pass the reference to the inner method somewhere before invoking it.

void cancel() = addObserver(onEvent);
...
cancel();

The first line demonstrates how a method can be defined using a = specification statement, just like a simple attribute definition. The second line of code simply invokes the returned reference to cancel().

We've already seen how an attribute can be defined using a block of code. Now we see that a method can be defined using a specifier. So, if you like, you can start thinking of a method as an attribute of type Callable — an attribute with parameters. Or if you prefer, you can think of an attribute as member with zero parameter lists, and of a method as a member with one or more parameter lists. Either kind of member can be defined by reference, using =, or directly, by specifying a block of code to be executed.

Cool, huh? That's more regularity.

There's more...

As you've probably noticed, all the functions we've defined so far have been declared with a name, using a traditional C-like syntax. We still need to see what Ceylon has instead of anonymous functions (sometimes called lambda expressions) for making it easy to take advantage of functions like repeat() which define specialized control structures. But I've hit my word limit already. Instead, you can find a discussion here.

If you're interested to know more about programming with higher-order functions, you can read more about currying, uncurrying, and function composition.

In Part 9, we're finally going to talk about Ceylon's syntax for named argument lists and for defining user interfaces and structured data.

Showing 1 to 5 of 12 blog entries tagged 'Introduction to Ceylon'