Beruflich Dokumente
Kultur Dokumente
Source: http://relation.to/tag/Introduction+to+Ceylon
10/19/2011
1.
BASICS ........................................................................................................................ 5
..................................................................................................... 7 STRINGS AND STRING INTERPOLATION ................................................................................................. 8 DEALING WITH OBJECTS THAT AREN'T THERE .......................................................................................... 9 OPERATORS FOR HANDLING NULL VALUES .......................................................................................... 11 DEFAULTED PARAMETERS ............................................................................................................. 11 2. CLASSES AND MEMBERS............................................................................................ 12 CREATING YOUR OWN CLASSES ....................................................................................................... 12 HIDING IMPLEMENTATION DETAILS .................................................................................................. 13 ABSTRACTING STATE USING ATTRIBUTES ............................................................................................ 14 UNDERSTANDING OBJECT INITIALIZATION ........................................................................................... 15 INSTANTIATING CLASSES AND OVERLOADING THEIR INITIALIZER PARAMETERS ................................................. 16 3. INHERITANCE AND INTRODUCTION ........................................................................... 19 INHERITANCE AND REFINEMENT ...................................................................................................... 19 ABSTRACT CLASSES ..................................................................................................................... 20 INTERFACES AND MIXIN INHERITANCE ............................................................................................... 21 AMBIGUITIES IN MIXIN INHERITANCE ................................................................................................ 23 INTRODUCTION ......................................................................................................................... 25 INTRODUCTION COMPARED TO EXTENSION METHODS AND IMPLICIT TYPE CONVERSIONS .................................... 27 TYPE ALIASES ............................................................................................................................ 28 MEMBER CLASSES AND MEMBER CLASS REFINEMENT .............................................................................. 28 ANONYMOUS CLASSES ................................................................................................................. 30 4. SEQUENCES............................................................................................................... 33 SEQUENCE SYNTAX SUGAR ............................................................................................................ 33 ITERATING SEQUENCES................................................................................................................. 34 SEQUENCE AND ITS SUPERTYPES ...................................................................................................... 34 EMPTY SEQUENCES AND THE BOTTOM TYPE ........................................................................................ 37 SEQUENCE GOTCHAS FOR JAVA DEVELOPERS ........................................................................................ 38 5. UNIONS, ALGEBRAIC TYPES, AND TYPE INFERENCE .................................................... 41 NARROWING THE TYPE OF AN OBJECT REFERENCE .................................................................................. 41 MORE ABOUT UNION TYPES ........................................................................................................... 41 ENUMERATED SUBTYPES............................................................................................................... 42 VISITORS ................................................................................................................................. 43 TYPESAFE ENUMERATIONS ............................................................................................................ 45 TYPE INFERENCE ........................................................................................................................ 47 TYPE INFERENCE FOR SEQUENCE ENUMERATION EXPRESSIONS ................................................................... 48 6. GENERICS .................................................................................................................. 49 DEFINING GENERIC TYPES .............................................................................................................. 49 COVARIANCE AND CONTRAVARIANCE ................................................................................................ 50 GENERIC TYPE CONSTRAINTS .......................................................................................................... 53 7. ATTRIBUTES AND LOCALS, CONTROL STRUCTURES, AND IMPORTS ............................ 57 ATTRIBUTES AND LOCALS .............................................................................................................. 57
ADDING INLINE DOCUMENTATION
2|P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
VARIABLES............................................................................................................................... 58
................................................................................................................................. 58 CONTROL STRUCTURES................................................................................................................. 59 SEQUENCED PARAMETERS ............................................................................................................. 61 PACKAGES AND IMPORTS .............................................................................................................. 61 8. HIGHER ORDER FUNCTIONS....................................................................................... 63 FIRST CLASS AND HIGHER ORDER FUNCTIONS ....................................................................................... 63 REPRESENTING THE TYPE OF A FUNCTION ........................................................................................... 63 REPRESENTING THE TYPE OF A METHOD ............................................................................................. 64 DEFINING HIGHER ORDER FUNCTIONS ................................................................................................ 65 FUNCTION REFERENCES ................................................................................................................ 66 MORE ABOUT HIGHER-ORDER FUNCTIONS .......................................................................................... 67 9. NAMED ARGUMENTS AND THE BUILDER SYNTAX ...................................................... 72 NAMED ARGUMENTS .................................................................................................................. 72 DECLARATIVE OBJECT INSTANTIATION SYNTAX...................................................................................... 73 MORE ABOUT NAMED ARGUMENTS .................................................................................................. 74 DEFINING USER INTERFACES ........................................................................................................... 78 10. BASIC TYPES AND OPERATORS .................................................................................. 80 AN OVERVIEW OF THE LANGUAGE MODULE ......................................................................................... 80 EQUALITY AND IDENTITY ............................................................................................................... 81 OPERATOR POLYMORPHISM .......................................................................................................... 82 THE SLOTS INTERFACE .................................................................................................................. 83 NUMERIC TYPES......................................................................................................................... 84 NUMERIC LITERALS ..................................................................................................................... 84 NUMERIC WIDENING ................................................................................................................... 85 NUMERIC OPERATOR SEMANTICS..................................................................................................... 86 11. INITIALIZATION ......................................................................................................... 88 SELF REFERENCES AND OUTER INSTANCE REFERENCES ............................................................................. 88 MULTIPLE INHERITANCE AND LINEARIZATION ....................................................................................... 88 DEFINITE ASSIGNMENT AND DEFINITE INITIALIZATION ............................................................................. 89 CLASS BODIES ........................................................................................................................... 90 INITIALIZER SECTION ................................................................................................................... 91 DECLARATION SECTION ................................................................................................................ 93 CIRCULAR REFERENCES ................................................................................................................. 94 DEFINITE INITIALIZATION OF METHODS .............................................................................................. 94 DEFINITE RETURN ....................................................................................................................... 95 12. ANNOTATIONS AND INTERCEPTORS .......................................................................... 96 ANNOTATIONS .......................................................................................................................... 96 ANNOTATION ARGUMENTS ........................................................................................................... 96 ANNOTATION TYPES .................................................................................................................... 97 ANNOTATION CONSTRAINTS .......................................................................................................... 98 READING ANNOTATION VALUES AT RUNTIME ....................................................................................... 99 DEFINING ANNOTATIONS ............................................................................................................ 100 INTERCEPTORS ........................................................................................................................ 101
SETTERS
3|P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
CONCLUSION ..........................................................................................................................
102
1. Basics
About Ceylon
Ceylon is a new programming language, designed to execute on the Java Virtual Machine, currently under development by my team here at Red Hat. We're fans of Java and of the Java ecosystem, of its practical orientation, of its culture of openness, of its developer community, of its roots in the world of business computing, and of its ongoing commitment to portability. However, we recognize that the language and class libraries, designed more than 15 years ago, are no longer the best foundation for a range of today's business computing problems. Ceylon's design goals include:
to be easy to learn and understand for Java and C# developers, to eliminate some of Java's verbosity, while retaining its readability, to improve upon Java's typesafety, to provide a declarative syntax for expressing hierarchical information like user interface definition, externalized data, and system configuration, thereby eliminating Java's dependence upon XML, to support and encourage a more functional style of programming with immutable objects and higher-order functions, to provide great support for meta-programming, thus making it easier to write frameworks, and to provide built-in modularity. Above all, Ceylon is designed to make it easy and fun to get things done in large teams. The Ceylon compiler is not yet finished, so you can't write code in Ceylon yet. However, we would like to get the community involved in development of the language and SDK, so this serious of articles is a sneak preview for interested folks. Let's start from the beginning.
This method prints Hello, World! on the console. A toplevel method like this is just like a C function it belongs directly to the package that contains it, it's not a member of any specific type. You don't need a receiving object to invoke a toplevel method. Instead, you can just call it like this:
hello();
Ceylon doesn't have Java-style static methods, but you can think of toplevel methods as filling the same role. The problem with static methods is that they break the block structure of the language. Ceylon has a very strict block structure a nested block always has access to declarations in all containing blocks. This isn't the case with Java's static methods. This method is declared using the void keyword, which means that the method declaration doesn't specify a return value. When the method is executed, it calls another toplevel method named writeLine(). This method displays its parameter on the console. Along with the void keyword, there's also a type named Void, which is the return type of any void method, and also the root of the Ceylon type system.
doc "The root type, supertype of both Object (definite values) and Nothing (the null value)." shared abstract class Void() of Object | Nothing {}
It might seem strange that a void method has a return type. The justification for this is that all methods in Ceylon are functions. (But not necessarily pure functions like hello(), a Ceylon function can have sideeffects.) At a very abstract level, every method accepts arguments and returns a result. The type Void, loosely speaking, is a way of representing the notion of an unknown value of an unknown type. You can assign any value, including null, to Void, but then there's no way to get it back out again, or even discover what type of value it was. So, in theory, a void method has a return value, we just have no way of finding out anything about that value. This stuff probably doesn't sound very useful right now, but it will turn out to be useful when we discuss higher order functions and Ceylon's typesafe metamodel.
6|P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
Or like this:
//The classic Hello World program void hello() { writeLine("Hello, World!"); }
doc
doc "The classic Hello World program" void hello() { writeLine("Hello, World!"); }
The doc annotation contains documentation that is included in the output of the Ceylon documentation compiler. The documentation compiler will support several other annotations, including by, for specifying the author of a program element,see, for referring to related code elements, and throws, for alerting the user to exception types thrown by an executable program element.
doc "The classic Hello World program" by "Gavin" see (goodbye) throws (IOException) void hello() { writeLine("Hello, World!"); }
There's also a deprecated annotation for marking program elements that will be removed in a future version of the module. Notice that when an annotation argument is a literal, it doesn't need to be enclosed in parentheses.
7|P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
Note also that annotations like doc, by, see, and deprecated aren't keywords. They're just ordinary identifiers. The same is true for annotations which are part of the language definition: abstract, variable, shared, formal, actual, and friends. On the other hand, void is a keyword.
Here we can see two nice things about strings in Ceylon. The first is that we can split a string across multiple lines. That's especially useful when we're writing documentation in a doc annotation. The second is that we can interpolate expressions inside a string literal. Technically, a string with expressions in it isn't really a literal anymore it's considered a string template. A string template must begin and end in a string literal. The following is not legal syntax:
writeLine("Hello, this is Ceylon " process.languageVersion); //compile error!
Note that this isn't the only way to concatenate strings in Ceylon. Indeed, it's only useful when you're interpolating variables or expressions into an otherwise-constant string. The + operator you're probably used to is an alternative, and more flexible in many cases:
writeLine("Hello, this is Ceylon " + process.languageVersion + " running on Java " + process.javaVersion + "!");
However, don't make the mistake of thinking that these two approaches are always equivalent just because they result in the same output in this case.
8|P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
The + operator immediately evaluates its operand expressions and produces an immutable Stringobject. On the other hand, a string template is an expression of type Gettable<String>, which evaluates its interpolated expressions lazily. If you print the same string template object twice, you might see different output the second time around! In fact, a string template is a kind of closure an important concept we'll explore further in Part 2.
The process object has an attribute named arguments, which holds a Sequence of the program's command line arguments. The local name is initialized with the first of these arguments, if any. This local is declared to have type String?, to indicate that it may contain a null value. Then the if (exists ...) control structure is used to initialize the value of the non-null local named greeting, interpolating the value of name into the message string whenever name is not null. Finally, the message is printed to the console. Unlike Java, locals, parameters, and attributes that may contain null values must be explicitly declared as being of optionaltype. And unlike other languages with typesafe null value handling, an optional type in Ceylon is not an algebraic datatype thatwraps the definite value. Instead, Ceylon uses an ad-hoc union
9|P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
type. The syntax T|S represents the union type of T and S, a type which may contain a value of type T or of type S. An optional type is any type of form Nothing|X where X is the type of the definite value. Fortunately, Ceylon lets us abbreviate the union type Nothing|X to X?.
doc "The type of null." shared abstract class Nothing() of nothing extends Void() {}
The value null is the unique instance of type Nothing, but it's not an instance of Object. So there's simply no way to assignnull to a local that isn't of optional type. The compiler won't let you.
doc "Represents a null reference." object nothing extends Nothing() {} doc "Hides the concrete type of nothing." shared Nothing null = nothing;
Nor will the Ceylon compiler let you do anything dangerous with a value of type T? that is, anything that could cause aNullPointerException in Java without first checking that the value is not null using if (exists ... ). More formally, the if (exists ... ) construct lets us extract a value of type X from a value of type X?, thereby allowing us to invoke the members of X upon the value. In fact, it's not even possible to use the equality operator == with an expression of optional type. You can't write if (x==null)like you can in Java. This helps avoid the undesirable behavior of == in Java where x==y evaluates to true if x and y both evaluate to null. It's possible to declare the local
String greeting; if (exists String name = process.arguments.first) { greeting = "Hello, " name "!"; } else { greeting = "Hello, World!"; } writeLine(greeting); name
inside the
if (exists ... )
condition:
This is the preferred style most of the time, since we can't actually use anything useful outside of the if (exists ... ) construct.
Source: http://relation.to/tag/Introduction+to+Ceylon
name
for
10 | P a g e
The ? operator returns its first argument if the first argument is not null, or its second argument otherwise. Its a more convenient way to handle null values in simple cases. It can even be chained:
shared String greeting = "Hello, " + nickName?name?"World";
The related ?. operator lets us call operations on optional types, always returning an optional type:
shared String shoutedGreeting = "HELLO, " + name?.uppercase?"WORLD";
Defaulted parameters
A method parameter may specify a default value.
void hello(String name="World") { writeLine("Hello, " name "!"); }
Then we don't need to specify an argument to the parameter when we call the method:
hello(); //Hello, World! hello("JBoss"); //Hello, JBoss!
Defaulted parameters must be declared after all required parameters in the parameter list of a method. Ceylon also supports sequenced parameters (varargs), declared using the syntax T.... We'll come back to them after we discuss sequences and for loops.
11 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
operations called methods, state held by attributes, logic to initialize the state when the object is first created, and, sometimes, other nested types. Types (interfaces, classes, and type parameters) have names that begin with uppercase letters. Members (methods and attributes) and locals have names that begin with lowercase letters. This is the rule you're used to from Java. Unlike Java, the Ceylon compiler enforces these rules. If you try to write class hello or String Name, you'll get a compilation error. Our first version of the
Hello
doc "The greeting" shared String greeting; if (exists name) { greeting = "Hello, " name "!"; } else { greeting = "Hello, World!"; }
To understand this code completely, we're going to need to first explore Ceylon's approach to program element accessibility the shared annotation
12 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
above, then meet the concept of an attribute, and finally discuss how object initialization works in Ceylon.
cycles. From a software engineering point of view, any code element exposed to a different package or module is a dependency that must be managed. And a dependency is a dependency. It's not any less of a dependency if the code to which the program element is exposed is in a subclass of the type which contains the program element!
The Ceylon compiler forces us to specify a value of any simple attribute or local before making use of the simple attribute or local in an expression. Ceylon will never automatically initialize an attribute to any kind of default value or let code observe the value of an uninitialized attribute. This code results in an error at compile time:
Natural count; shared void inc() { count++; } //compile error
An attribute is a bit different to a Java field. It's an abstraction of the notion of a value. Some attributes are simple value holders like the one we've just seen; others are more like a getter method, or, sometimes, like a getter and setter method pair. Like methods, attributes are polymorphican attribute definition may be refined (overridden) by a subclass. We could rewrite the attribute
shared String greeting { if (exists name) { return "Hello, " name "!"; } else { return "Hello, World!"; } } greeting
as a getter:
Notice that the syntax of a getter declaration looks a lot like a method declaration with no parameter list.
14 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
Clients of a class never need to know whether the attribute they access holds state directly, or is a getter that derives its value from other attributes of the same object or other objects. In Ceylon, you don't need to go around declaring all your attributes private and wrapping them in getter and setter methods. Get out of that habit right now!
the parameters needed to instantiate the class the initializer parameters are declared directly after the name of the class, and code to initialize the new instance of the class the class initializer goes directly in the body of the class. Take a close look at the following code fragment:
String greeting; if (exists name) { greeting = "Hello, " name "!"; } else { greeting = "Hello, World!"; }
In Ceylon, this code could appear in the body of a class, where it would be declaring and specifying the value of an immutable attribute, or it could appear in the body of a method definition, where it would be declaring and specifying the value of an immutable local variable. That's not the case in Java, where initialization of fields looks very different to initialization of local variables! Thus the syntax of Ceylon is more regular than Java. Regularity makes a language easy to learn and easy to refactor. Now let's turn our attention to a different possible implementation of greeting:
class Hello(String? name) { shared String greeting { if (exists name) { return "Hello, " name "!"; } else { return "Hello, World!"; } }
15 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
... }
You might be wondering why we're allowed to use the parameter name inside the body of the getter of greeting. Doesn't the parameter go out of scope as soon as the initializer terminates? Well, that's true, but Ceylon is a language with a very strict block structure, and the scope of declarations is governed by that block structure. In this case, the scope of name is the whole body of the class, and the definition of greeting sits inside that scope, so greeting is permitted to access name. We've just met our first example of closure. We say that method and attribute definitions receive a closure of values defined in the class body to which they belong. That's just a fancy way of obfuscating the idea that greeting holds onto the value ofname, even after the initializer completes. In fact, one way to look at the whole notion of a class in Ceylon is to think of it as a function which returns a closure of its own local variables. This helps explain why the syntax of class declarations is so similar to the syntax of method declarations (a class declaration looks a lot like a method declaration where the return type and the name of the method are the same).
Our rewritten hello() method just creates a new instance of Hello, and invokes say(). Ceylon doesn't need a new keyword to know when you're instantiating a class. No, we don't know why Java needs it. You'll have to ask James. I suppose you're worried that if Ceylon classes don't have constructors, then they also can't have multiple constructors. Does that mean we can't overload the initialization parameter list of a class? I guess now's as good a time as any to break some more bad news: Ceylon doesn't support method overloading either! But, actually, this isn't as bad as it
16 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
sounds. The sad truth is that overloading is the source of various problems in Java, especially when generics come into play. And in Ceylon, we can emulate most non-evil uses of constructor or method overloading using:
defaulted parameters, to emulate the effect of overloading a method or class by arity (the number of parameters), sequenced parameters, i.e. varargs, and union types or enumerated type constraints, to emulate the effect of overloading a method or class by parameter type.
We're not going to get into all the details of these workarounds right now, but here's a quick example of each of the three techniques:
//defaulted parameter void print(String string = "\n") { writeLine(string); } //sequenced parameter void print(String... strings) { for (String string in strings) { writeLine(string); } } //union type void print(String|Named printable) { String string; switch (printable) case (is String) { string = printable; } case (is Named) { string = printable.name; } writeLine(string); }
Don't worry if you don't completely understand the third example just yet. Just think of it as a completely typesafe version of how you would write an overloaded operation in a dynamic language like Smalltalk, Python, or Ruby. (If you're really impatient, skip forward to the discussion of generic type constraints.) To be completely honest, there are some circumstances where this approach ends up slightly more awkward than Java-style overloading. But that's a small
17 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
price to pay for a language with clearer semantics, without nasty corner cases, that is ultimately more powerful. Let's overload Hello, and its
doc "A command line greeting" class Hello(String? name = process.args.first) { ... say()
doc "Print the greeting" shared void say(OutputStream stream = process.output) { stream.writeLine(greeting); } }
Our
hello()
18 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
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
19 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
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() {
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 nullunless 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"
20 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
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.
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 for Ceylon.
shared interface Writer {
21 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
Writer
interface
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();
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
22 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
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.
To fix this code, we'll factor out a formal declaration of the attribute 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; } }
name
to a
23 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
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; }
default
in both
User
and
Party
and
24 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
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.
25 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
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 SequenceListinterface provides concrete implementations of all methods of List that are not already implemented by Sequence. Now, to introduce List to is import the adapter:
Sequence
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 and is assignable to the introduced type. Again, according to the spec:
List,
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.
26 | P a g e
Source: http://relation.to/tag/Introduction+to+Ceylon
Otherwise, the operation is dispatched to the introduction that has the most-refined member defining the operation.
shared String concatenated { variable String concat = ""; for (String s in this) { concat+=s; } return concat; }
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
27 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
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>;
class BufferedReader(Reader reader) satisfies Reader { shared default class Buffer() satisfies List<Character> { ... } ... }
Buffer
is annotated
shared,
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 subtype of BufferedReader:
shared class BufferedFileReader(File file) extends BufferedReader(FileReader(file)) { shared actual class Buffer() extends super.Buffer() { ... } } default,
so we can refine it in a
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 ofBufferedFileReader.Buffer or an instance of BufferedReader.Buffer, depending upon whether br refers to a plainBufferedReader or a BufferedFileReader. This is more than a cute trick. Polymorphic instantiation lets us eliminate thefactory method pattern from our code. It's even possible to define a formal member class of an A formal member class can declare formal members.
abstract class BufferedReader(Reader reader) satisfies Reader { shared formal class Buffer() { shared formal Byte read(); } ... }
29 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
abstract
class.
abstract
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();
30 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
The downside to an object declaration is that we can't write code that refers to the concrete type of defaultHello orconsoleWriter, only to the named instances. You might be tempted to think of that's not quite right:
object
A toplevel
object
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
formal
or
default.
shared abstract class App() { shared formal OutputStream stream; ... } class ConsoleApp() extends App() {
31 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
However, an
object
formal
or
default.
32 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
4. Sequences
Some kind of array or list construct is a universal feature of all programming languages. The Ceylon language module defines support for sequence types. A sequence type is usually written X[] for some element type X. But this is really just an abbreviation for the union type Empty|Sequence<X>. The interface Sequence represents a sequence with at least one element. The type Empty represents an empty sequence with no elements. Some operations of the type Sequence aren't defined by Empty, so you can't call them if all you have is X[]. Therefore, we need the if (nonempty ... ) construct to gain access to these operations.
void printBounds(String[] strings) { if (nonempty strings) { //strings is a Sequence<String> writeLine(strings.first + ".." + strings.last); } else { writeLine("Empty"); } }
Note how this is just a continuation of the pattern established for null value handling.
{}
Empty.
However, unlike Java, all these syntactic constructs are pure abbreviations. The code above is exactly equivalent to the following de-sugared code:
Empty|Sequence<String> operators = Array("+", "-", "*", "/"); Nothing|String plus = operators.value(0); Empty|Sequence<String> multiplicative = operators.range(2,3);
33 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
Range
is also a subtype of
Sequence.
The following:
In fact, this is just a sneak preview of the fact that almost all operators in Ceylon are just sugar for method calls upon a type. We'll come back to this later, when we talk about operator polymorphism.
Iterating sequences
The Sequence interface extends a for loop:
for (String op in operators) { writeLine(op); } Iterable,
so we can iterate a
Sequence
using
for
for
If, for any reason, we need to use the index of each element of a sequence we can use a special variation of the for loop that is designed for iterating instances of Entries:
for (Natural i -> String op in entries(operators)) { writeLine($i + ": " + op); }
The entries() function returns an instance of the indexed elements of the sequence.
Entries<Natural,String>
containing
It's probably a good time to see some more advanced Ceylon code. What better place to find some than in the language module itself? Here's how the language module defines the type
shared interface Sequence<out Element> satisfies Correspondence<Natural, Element> & Iterable<Element> & Sized { Sequence:
doc "The index of the last element of the sequence." shared formal Natural lastIndex;
doc "The first element of the sequence." shared actual formal Element first;
doc "The rest of the sequence, without the first element." shared formal Element[] rest; shared actual Boolean empty { return false; }
doc "The last element of the sequence." shared default Element last { if (exists Element x = value(lastIndex)) { return x; } else { //actually never occurs if //the subtype is well-behaved return first; } } shared actual default Iterator<Element> iterator() { class SequenceIterator(Natural from) satisfies Iterator<Element> { shared actual Element? head { return value(from);
35 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
The most interesting operations are inherited from Correspondence, Iterable and Sized:
shared interface Correspondence<in Key, out Value> given Key satisfies Equality {
doc "Return the value defined for the given key." shared formal Value? value(Key key); } shared interface Iterable<out Element> satisfies Container {
doc "An iterator of values belonging to the container." shared formal Iterator<Element> iterator();
doc "The first object." shared default Element? first { return iterator().head; } } shared interface Sized satisfies Container {
doc "The number of values or entries belonging to the container." shared formal Natural size;
36 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
shared actual default Boolean empty { return size==0; } } shared interface Container {
shared actual Nothing head { return null; } shared actual Iterator<Bottom> tail { return this; } } shared interface Empty satisfies Correspondence<Natural, Bottom> & Iterable<Bottom> & Sized {
shared actual Natural size { return 0; } shared actual Boolean empty { return true; } shared actual Iterator<Bottom> iterator() { return emptyIterator; } shared actual Nothing value(Natural key) { return null; } shared actual Nothing first {
37 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
return null; } }
Bottom
represents:
the empty set, or equivalently the intersection of all types. Since the empty set is a subset of all other sets, Bottom is assignable to all other types. Why is this useful here? Well,Correspondence<Natural,Element> and Iterable<Element> are both covariant in the type parameter Element. So Empty is assignable to Correspondence<Natural,T> and Iterable<T> for any type T. That's why Empty doesn't need a type parameter. The following code is well-typed:
void printAll(String[] strings) { variable Iterator<String> i := strings.iterator(); while (exists String s = i.head) { writeLine(s); i := i.tail; } }
Since both Empty and Sequence<String> are subtypes of type String[] is also a subtype ofIterable<String>.
Iterable<String>,
the union
Another cool thing to notice here is the return type of the first and value() operations of Empty. You might have been expecting to see Bottom? here, since they override supertype members of type T?. But as we saw in Part 1, Bottom? is just an abbreviation for Nothing|Bottom. And Bottom is the empty set, so the union Bottom|T of Bottom with any other type T is just Titself. The Ceylon compiler is able to do all this reasoning automatically. So when it sees an Iterable<Bottom>, it knows that the operation first is of type Nothing, i.e. it is the value null. Cool, huh?
interface, it's not a mutable concrete type like an array. We can't set the value of an element:
String[] operators = .... ; operators[0] := "**"; //compile error
Furthermore, the index operation operators[i] returns an optional type String?, which results in quite different code idioms. To begin with, we don't iterate sequences by index like in C or Java. The following code does not compile:
for (Natural i in 0..operators.size-1) { String op = operators[i]; //compile error ... }
Here,
operators[i]
is a
String?,
String. for
Instead, if we need access to the index, we use the special form of above.
for (Natural i -> String op in entries(operators)) { ... }
shown
Likewise, we don't usually do an upfront check of an index against the sequence length:
if (i>operators.size-1) { throw IndexOutOfBoundException(); } else { return operators[i]; //compile error }
All this may take a little getting used to. But what's nice is that all the exact same idioms also apply to other kinds ofCorrespondence, including Entries and Maps.
40 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
The
switch
Object obj = ... ; switch(obj) case (is Hello) { obj.say(); } case (is Person) { stream.writeLine(obj.firstName); } else { stream.writeLine("Some miscellaneous thing"); }
These constructs protect us from inadvertantly writing code that would cause a ClassCastException in Java, just like if (exists ... ) protects us from writing code that would cause a NullPointerException.
We've seen a few examples of how ad-hoc union types are used in Ceylon. Let's just revisit the notion to make sure we completely understand it. When I declare the type of something using a union type X|Y, I'm saying that only expressions of type X and expressions of type Y are assignable to it. The type X|Y is a supertype of both X and Y. The following code is well-typed:
void print(String|Natural|Integer val) { ... } print("hello"); print(69); print(-1);
But what operations does a type like String|Natural|Integer have? What are its supertypes? Well, the answer is pretty intuitive: T is a supertype of X|Y if and only if it is a supertype of both X and Y. The Ceylon compiler determines this automatically. So the following code is also well-typed:
Natural|Integer i = ... ; Number num = i; String|Natural|Integer val = i; Object obj = val;
However,
num
is not assignable to
val,
since
Number
is not a supertype of
String.
Of course, it's very common to narrow an expression of union type using a switch statement. Usually, the Ceylon compiler forces us to write an else clause in a switch, to remind us that there might be additional cases which we have not handled. But if we exhaust all cases of a union type, the compiler will let us leave off the else clause.
void print(String|Natural|Integer val) { switch (val) case (is String) { writeLine(val); } case (is Natural) { writeLine("Natural: " + val); } case (is Integer) { writeLine("Integer: " + val); } }
Enumerated subtypes
Sometimes it's useful to be able to do the same kind of thing with the subtypes of an ordinary type. First, we need to explicitly enumerate the subtypes of the type using the of clause:
abstract class Hello() of DefaultHello | PersonalizedHello {
42 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
... }
(This makes Hello into Ceylon's version of what the functional programming community calls an algebraic data type.) Now the compiler won't let us declare additional subclasses of Hello, and so the union type DefaultHello|PersonalizedHellois exactly the same type as Hello. Therefore, we can write switch statements without an else clause:
Hello hello = ... ; switch (hello) case (is DefaultHello) { writeLine("What's your name?"); } case (is PersonalizedHello) { writeLine("Nice to hear from you again!"); }
Now, it's usually considered bad practice to write long switch statements that handle all subtypes of a type. It makes the code non-extensible. Adding a new subclass to Hello means breaking all the switch statements that exhaust its subtypes. In object-oriented code, we usually try to refactor constructs like this to use an abstract method of the superclass that is overridden as appropriate by subclasses. However, there are a class of problems where this kind of refactoring isn't appropriate. In most object-oriented languages, these problems are usually solved using the visitor pattern.
Visitors
Let's consider the following tree visitor implementation:
abstract class Node() { shared formal void accept(Visitor v); } class Leaf(Object val) extends Node() { shared Object value = val; shared actual void accept(Visitor v) { v.visitLeaf(this); } } class Branch(Node left, Node right) extends Node() { shared Node leftChild = left;
43 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
shared Node rightChild = right; shared actual void accept(Visitor v) { v.visitBranch(this); } } interface Visitor { shared formal void visitLeaf(Leaf l); shared formal void visitBranch(Branch b); }
We can create a method which prints out the tree by implementing the Visitor interface:
void print(Node node) { object printVisitor satisfies Visitor { shared actual void visitLeaf(Leaf l) { writeLine("Found a leaf: " l.value "!"); } shared actual void visitBranch(Branch b) { b.leftChild.accept(this); b.rightChild.accept(this); } } node.accept(printVisitor); }
Notice that the code of printVisitor looks just like a switch statement. It must explicitly enumerate all subtypes of Node. Itbreaks if we add a new subtype of Node to the Visitor interface. This is correct, and is the desired behavior. By break, I mean that the compiler lets us know that we have to update our code to handle the new subtype. In Ceylon, we can achieve the same effect, with less verbosity, by enumerating the subtypes of Node in its definition, and using a switch:
abstract class Node() of Leaf | Branch {} class Leaf(Object val) extends Node() { shared Object value = val; } class Branch(Node left, Node right) extends Node() { shared Node leftChild = left; shared Node rightChild = right; }
44 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
Our print() method is now much simpler, but still has the desired behavior of breaking when a new subtype of Node is added.
void print(Node node) { switch (node) case (is Leaf) { writeLine("Found a leaf: " node.value "!"); } case (is Branch) { print(node.leftChild); print(node.rightChild); } }
Typesafe enumerations
Ceylon doesn't have anything exactly like Java's emulate the effect using the of clause.
shared class Suit(String name) of hearts | diamonds | clubs | spades extends Case(name) {} enum
shared object hearts extends Suit("hearts") {} shared object diamonds extends Suit("diamonds") {} shared object clubs extends Suit("clubs") {} shared object spades extends Suit("spades") {}
We're allowed to use the names of extend the language module class Now we can exhaust all cases of
void print(Suit suit) { switch (suit)
object Case.
declarations in the
switch:
of
clause if they
Suit
in a
case (hearts) { writeLine("Heartzes"); } case (diamonds) { writeLine("Diamondzes"); } case (clubs) { writeLine("Clidubs"); } case (spades) { writeLine("Spidades"); } }
cases
cases, enum,
not
case (is...)
type cases.)
For a more practical example, let's see the definition of language module:
shared abstract class Boolean(String name) of true | false extends Case(name) {} shared object false extends Boolean("false") {} shared object true extends Boolean("true") {}
Boolean
from the
Comparable
Comparison:
doc "The result of a comparison between two Comparable objects." shared abstract class Comparison(String name) of larger | smaller | equal extends Case(name) {} doc "The receiving object is exactly equal to the given object." shared object equal extends Comparison("equal") {} doc "The receiving object is smaller than the given object." shared object smaller extends Comparison("smaller") {} doc "The receiving object is larger than the given object." shared object larger extends Comparison("larger") {}
Now, the
Comparable
interface itself:
shared interface Comparable<in Other> satisfies Equality given Other satisfies Comparable<Other> {
doc "The > operator." shared Boolean largerThan(Other other) { return compare(other)==larger; }
doc "The < operator." shared Boolean smallerThan(Other other) { return compare(other)==smaller; }
46 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
doc "The >= operator." shared Boolean asLargeAs(Other other) { return compare(other)!=smaller; }
doc "The <= operator." shared Boolean asSmallAs(Other other) { return compare(other)!=larger; } }
Type inference
So far, we've always been explicitly specifying the type of every declaration. I think this generally makes code, especially example code, much easier to read and understand. However, Ceylon does have the ability to infer the type of a locals or the return type of a local method. Just place the keywordlocal in place of the type declaration.
local hello = DefaultHello(); local operators = { "+", "-", "*", "/" }; local add(Natural x, Natural y) { return x+y; }
There are some restrictions applying to this feature. You can't use
local:
for declarations annotated for declarations annotated for methods with multiple to declare a parameter.
shared, formal,
statements, or
These restrictions mean that Ceylon's type inference rules are quite simple. Type inference is purely right-to-left and top-to-bottom. The type of any expression is already known without needing to look to any types declared to the left of the =specifier, or further down the block of statements.
local
47 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
local
returned
What type is inferred for sequence? You might answer: Sequence<X> where X is the common superclass or super-interface of all the element types. But that can't be right, since there might be more than one common supertype. The answer is that the inferred type is Sequence<X> where X is the union of all the element expression types. In this case, the type is Sequence<DefaultHello|String|Float>. Now, this works out nicely, because Sequence<T> is covariant in T. So the following code is well typed:
local sequence = { DefaultHello(), "Hello", 12.0 }; //type Sequence<DefaultHello|String|Float> Object[] objects = sequence; //type Empty|Sequence<Object>
What about sequences that contain of null from Part 1 was Nothing?
null?
local sequence = { null, "Hello", "World" }; //type Sequence<Nothing|String> String?[] strings = sequence; //type Empty|Sequence<Nothing|String> String? s = sequence[0]; //type Nothing|Nothing|String which is just Nothing|String
It's interesting just how useful union types turn out to be. Even if you only very rarely explicitly write code with any explicit union type declaration (and that's probably a good idea), they are still there, under the covers, helping the compiler solve some hairy, otherwise-ambiguous, typing problems.
48 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
6. Generics
Defining generic types
We've seen plenty of parameterized types in this series of articles, but now let's explore a few more details. Programming with generic types is one of the most difficult parts of Java. That's still true, to some extent, in Ceylon. But because the Ceylon language and SDK were designed for generics from the ground up, Ceylon is able to alleviate the most painful aspects of Java's bolted-on-later model. Just like in Java, only types and methods may declare type parameters. Also just like in Java, type parameters are listed before ordinary parameters, enclosed in angle brackets.
shared interface Iterator<out Element> { ... } class Array<Element>(Element... elements) satisfies Sequence<Element> { ... } shared Entries<Natural,Value> entries<Value>(Value... sequence) { ... }
As you can see, the convention in Ceylon is to use meaningful names for type parameters. Unlike Java, we always do need to specify type arguments in a type declaration (there are no raw types in Ceylon). The following will not compile:
Iterator it = ...; //error: missing type argument to parameter Element of Iterator
On the other hand, we shouldn't need to explicitly specify type arguments in most method invocations or class instantiations. In principle it's very often possible to infer the type arguments from the ordinary arguments. The following code should be possible, just like it is in Java:
Array<String> strings = Array("Hello", "World"); Entries<Natural,String> entries = entries(strings);
But we haven't yet figured out what exactly the type inference algorithm will be (probably something involving union types!) and so the Ceylon compiler currently requires that all type arguments be explicitly specified like this:
49 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
The root cause of very many problems when working with generic types in Java is type erasure. Generic type parameters and arguments are discarded by the compiler, and simply aren't available at runtime. So the following, perfectly sensible, code fragments just wouldn't compile in Java:
if (is List<Person> list) { ... } if (is Element obj) { ... }
(Where
Element
A major goal of Ceylon's type system is support for reified generics. Like Java, the Ceylon compiler performs erasure, discarding type parameters from the schema of the generic type. But unlike Java, type arguments are supposed to be reified (available at runtime). Of course, generic type arguments won't be checked for typesafety by the underlying virtual machine at runtime, but type arguments are at least available at runtime to code that wants to make use of them explicitly. So the code fragments above are supposed to compile and function as expected. You will even be able to use reflection to discover the type arguments of an instance of a generic type. The bad news is we haven't implemented this yet ;-) Finally, Ceylon eliminates one of the bits of Java generics that's really hard to get your head around: wildcard types. Wildcard types were Java's solution to the problem of covariance in a generic type system. Let's first explore the idea of covariance, and then see how covariance in Ceylon works.
Geek
is a subtype of
Person.
Reasonable.
//compiler error
This code is, frankly, perfectly reasonable taken at face value. Yet in both Java and Ceylon, this code results in a compiler error at the second line, where the Collection<Geek> is assigned to a Collection<Person>. Why? Well, because if we let the assignment through, the following code would also compile:
Collection<Geek> geeks = ... ; Collection<Person> people = geeks; people.add( Person("Fonzie") );
//compiler error
Geek!
Using big words, we say that Collection is nonvariant in Element. Or, when we're not trying to impress people with opaque terminology, we say that Collection both produces via the iterator() method and consumes via the add() method the type Element. Here's where Java goes off and dives down a rabbit hole, successfully using wildcards to squeeze a covariant or contravariant type out of a nonvariant type, but also succeeding in thoroughly confusing everybody. We're not going to follow Java down the hole. Instead, we're going to refactor pure consumer interface:
Collection
shared interface Producer<out Output> { shared formal Iterator<Output> iterator(); } shared interface Consumer<in Input> { shared formal void add(Input x); }
The out annotation specifies that Producer is covariant in Output; that it produces instances of Output, but never consumes instances of Output.
51 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
The in annotation specifies that Consumer is contravariant in Input; that it consumes instances of Input, but never produces instances of Input. The Ceylon compiler validates the schema of the type declaration to ensure that the variance annotations are satisfied. If you try to declare an add() method on Producer, a compilation error results. If you try to declare an iterate() method onConsumer, you get a similar compilation error. Now, let's see what that buys us:
Since Producer is covariant in its type parameter Output, and since Geek is a subtype of Person, Ceylon lets you assign Producer<Geek> to Producer<Person>. Furthermore, since Consumer is contravariant in its type parameter since Geek is a subtype of Person, Ceylon lets you assign Consumer<Person> to Consumer<Geek>. We can define our
Collection Input,
and
interface as a mixin of
Producer
with
Consumer.
Notice that Collection remains nonvariant in Element. If we tried to add a variance annotation to Element in Collection, a compile time error would result. Now, the following code finally compiles:
Collection<Geek> geeks = ... ; Producer<Person> people = geeks; for (Person person in people) { ... }
Which matches our original intuition. The following code also compiles:
Collection<Person> people = ... ; Consumer<Geek> geekConsumer = people; geekConsumer.add( Geek("James") );
Person!
There's two additional things that follow from the definition of covariance and contravariance:
Producer<Void>
is a supertype of
Producer<T>
Source: http://relation.to/tag/Introduction+to+Ceylon
Consumer<Bottom>
is a supertype of
Consumer<T>
These invariants can be very helpful if you need to abstract over all Producers or all Consumers. (Note, however, that if Producerdeclared upper bound type constraints on Output, then Producer<Void> would not be a legal type.) You're unlikely to spend much time writing your own collection classes, since the Ceylon SDK has a powerful collections framework built in. But you'll still appreciate Ceylon's approach to covariance as a user of the built-in collection types. The collections framework defines two interfaces for each basic kind of collection. For example, there's an interface List<Element>which represents a read-only view of a list, and is covariant in Element, and OpenList<Element>, which represents a mutable list, and is nonvariant in Element.
A type argument to
Element
must be a subtype of
Equality.
Set<String> set = Set("C", "Java", "Ceylon"); //ok Set<String?> set = Set("C", "Java", "Ceylon", null); //compile error
53 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
In Ceylon, a generic type parameter is considered a proper type, so a type constraint looks a lot like a class or interface declaration. This is another way in which Ceylon is more regular than some other C-like languages. An upper bound lets us call methods and attributes of the bound, but it doesn't let us instantiate new instances of Element. Once we implement reified generics, we'll be able to add a new kind of type constraint to Ceylon. An initialization parameter specification lets us actually instantiate the type parameter.
shared class Factory<out Result>() given Result(String s) { shared Result produce(String string) { return Result(string); } }
Factory
A third kind of type constraint is an enumerated type bound, which constrains the type argument to be one of an enumerated list of types. It lets us write an exhaustive switch on the type parameter:
Value sqrt<Value>(Value x) given Value of Float | Decimal { switch (Value) case (satisfies Float) { return sqrtFloat(x); } case (satisfies Decimal) { return sqrtDecimal(x); } }
This is one of the workarounds we mentioned earlier for Ceylon's lack of overloading. Finally, the fourth kind of type constraint, which is much less common, and which most people find much more confusing, is a lower bound. A lower bound is the opposite of an upper bound. It says that a type parameter is a supertype
54 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
of some other type. There's only really one situation where this is useful. Consider adding a union() operation to our Set interface. We might try the following:
shared class Set<out Element>(Element... elements) given Element satisfies Equality { ...
//compile error
This doesn't compile because we can't use the covariant type parameter T in the type declaration of a method parameter. The following declaration would compile:
shared class Set<out Element>(Element... elements) given Element satisfies Equality { ...
But, unfortunately, we get back a Set<Object> no matter what kind of set we pass in. A lower bound is the solution to our dilemma:
shared class Set<out Element>(Element... elements) given Element satisfies Equality { ...
shared Set<UnionElement> union(Set<UnionElement> set) given UnionElement abstracts Element { return ... } }
With type inference, the compiler chooses an appropriate type argument to UnionElement for the given argument to union():
55 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
Set<String> strings = Set("abc", "xyz") ; Set<String> moreStrings = Set("foo", "bar", "baz"); Set<String> allTheStrings = strings.union(moreStrings); Set<Decimal> decimals = Set(1.2.decimal, 3.67.decimal) ; Set<Float> floats = Set(0.33, 22.0, 6.4); Set<Number> allTheNumbers = decimals.union(floats); Set<Hello> hellos = Set( DefaultHello(), PersonalizedHello(name) ); Set<Object> objects = Set("Gavin", 12, true); Set<Object> allTheObjects = hellos.union(objects);
56 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
Counter:
count
is an attribute:
This might seem a bit strange at first, but it's really just how closure works. The same behavior applies to locals inside a method. Methods can't declare shared members, but they can return an object that captures a local:
interface Counter { shared formal Natural inc(); } Counter createCounter() { variable Natural count := 0; object counter satisfies Counter { shared actual Natural inc() { return ++count; } } return counter;
57 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
Even though we'll continue to use the words local and attribute, keep in mind that there's no really strong distinction between the terms. Any named value might be captured by some other declaration in the same containing scope. (I'm still searching for a really good word to collectively describe attributes and locals.)
Variables
Ceylon encourages you to use immutable attributes as much as possible. An immutable attribute has its value specified when the object is initialized, and is never reassigned.
class Reference<Value>(Value x) { shared Value value = x; }
If we want to be able to assign a value to a simple attribute or local we need to annotate it variable:
class Reference<Value>(Value x) { shared variable Value value := x; }
Notice the use of := instead of = here. This is important! In Ceylon, specification of an immutable value is done using =. Assignment to a variable attribute or local is considered a different kind of thing, always performed using the := operator. The = specifier is not an operator, and can never appear inside an expression. It's just a punctuation character. The following code is not only wrong, but even fails to parse:
if (x=true) { ... } //compile error
Setters
If we want to make an attribute with a getter mutable, we need to define a matching setter. Usually this is only useful if you have some other internal attribute you're trying to set the value of indirectly.
58 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
Suppose our class has the following simple attributes, intended for internal consumption only, so un-shared:
variable String? firstName := null; variable String? lastName := null;
null.)
Then we can abstract the simple attribute using a second attribute defined as a getter/setter pair:
shared String fullName { return " ".join(coalesce(firstName,lastName)); } shared assign fullName { Iterator<String> tokens = fullName.tokens(); firstName := tokens.head; lastName := tokens.rest.head; }
A setter is identified by the keyword assign in place of a type declaration. (The type of the matching getter determines the type of the attribute.) Yes, this is a lot like a Java get/set method pair, though the syntax is significantly streamlined. But since Ceylon attributes are polymorphic, and since you can redefine a simple attribute as a getter or getter/setter pair without affecting clients that call the attribute, you don't need to write getters and setters unless you're doing something special with the value you're getting or setting.
Control structures
Ceylon has five built-in control structures. There's nothing much new here for Java or C# developers, so I'll just give a few quick examples without much additional commentary. However, one thing to be aware of is that Ceylon doesn't allow you to omit the braces in a control structure. The following doesn't parse:
if (x>100) bigNumber();
59 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
if/else
The switch/case statement eliminates C's much-criticized fall through behavior and irregular syntax:
switch (x<=>100) case (smaller) { littleNumber(); } case (equal) { oneHundred(); } case (larger) { bigNumber(); }
The for loop has an optional fail block, which is executed when the loop completes normally, rather than via a return orbreak statement. There's no Cstyle for.
Boolean minors; for (Person p in people) { if (p.age<18) { minors = true; break; } } fail { minors = false; }
The
while
and
do/while
variable local it = names.iterator(); while (exists String name = it.head) { writeLine(name); it:=it.tail; }
The
try/catch/finally
Source: http://relation.to/tag/Introduction+to+Ceylon
And
try
Sequenced parameters
A sequenced parameter of a method or class is declared using an ellipsis. There may be only one sequenced parameter for a method or class, and it must be the last parameter.
void print(String... strings) { ... }
strings
has type
String[].
A slightly more sophisticated example is the coalesce() method we saw above. coalesce() accepts X?[] and eliminates nulls, returning X[], for any type X. Its signature is:
shared Value[] coalesce<Value>(Value?... sequence) { ... }
Sequenced parameters turn out to be especially interesting when used in named argument lists for defining user interfaces or structured data.
location of the source file in which it is declared. A class named Hello in the package org.jboss.hellomust be defined in the file org/jboss/hello/Hello.ceylon. When a source file in one package refers to a toplevel program element in another package, it must explicitly import that program element. Ceylon, unlike Java, does not support the use of qualified names within the source file. We can't writeorg.jboss.hello.Hello in Ceylon. The syntax of the import statement is slightly different to Java. To import a program element, we write:
import org.jboss.hello { Hello }
We think renaming is a much cleaner solution than the use of qualified names.
62 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
63 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
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 following:
Callable
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 Resultrepresents 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:
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>
64 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
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>>
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.
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 Callabletype 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)) {
65 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
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);
above. It is
66 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
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 theaddObserver() method to accept a function as a parameter.
shared abstract class Component() { OpenList<Callable<Void,Event>> observers = OpenList<Callable<Void,Event>>();
shared void fire(Event event) { for (void observe(Event event) in observers) { observe(event); } }
67 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
Here we see the difference between the two ways of specifying a function type:
is more readable in parameter lists, where the name of the parameter, but
void observe(Event event) Callable<Void,Event>
observe
is
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) {
68 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
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() { ...
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() isCallable<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().
70 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
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.
71 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
(Remember, the last parameter is a sequenced parameter which accepts multiple arguments, just like a Java varargsparameter.) 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 };
72 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
};
73 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
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
void,
and
:=
assignment
Once you're used to Ceylon's more flexible syntax, these differences will usually stand out immediately.
Table
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
74 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
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, (anonymous class) declarations are most useful for specifying the value of a parameter whose type is an interface or abstract class, and
object
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 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; } } };
75 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
Table
as follows:
Notice that we've specified the value of the parameter named 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; } } }
content
using the
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 minilanguage.)
76 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
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 {
77 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
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)) { ... } }
Body body { Div { cssClass = "greeting"; Hello( request.parameters["name"] ).greeting }, Div { cssClass = "footer"; "Powered by Ceylon" } } };
79 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
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
80 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
Ceylon type system can also represent some more exotic types, for example Nothing, which is the type ofnull. Therefore, Ceylon's Object has a superclass, named Void, which we already met in Part 1. All Ceylon types are assignable toVoid. 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 represent well-defined values extend Object, including:
Void.
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.
return false; } }
defines a default implementation of the interface Equality, which is very similar to the equals() andhashCode() methods defined by java.lang.Object.
IdentifiableObject shared interface Equality {
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
82 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
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 namedsmallerThan(), 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 a method named equals().
x==y
Equality,
which has
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
operator,
+
and
operators,
supports the other basic arithmetic operators, supports the comparison operators, and
Sequence
Comparable
Correspondence Boolean
Operator polymorphism is a little more flexible than you might imagine. Here's a quick example of this.
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 write things like permissions&(read|execute).
Slots,
allowing us to
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 Integer Float Whole
represents floating point approximations to the real numbers, represents arbitrary-precision signed integers, and represents arbitrary-precision and arbitrary-scale decimals.
Decimal
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 for Floats:
Naturals,
and literals
84 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
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
and
always widen
Natural
to
Integer:
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.
85 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
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);
to to
or
Decimal
or
Decimal
to to
Decimal Decimal
But these conversions aren't defined by special-case rules in the language specification.
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> { ... }
86 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
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 but that Float can only be widened toDecimal. So we can infer that the expression -1 * 0.4 is of type Float.
Decimal,
Therefore, the definition of a numeric operator like * can be represented, completely within the type system, in terms ofNumeric 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.
87 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
11. Initialization
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; } } }
this, super,
and
outer,
which we'll
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 isnever 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.
88 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
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 beeven 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.
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.
89 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
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
90 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
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";
91 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
String greeting; if (exists name) { greeting = greetingForTime + ", " + name; } else { greeting = greetingForTime; } //declaration section:
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:
this,
outer,
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
92 | P a g e
Source: http://relation.to/tag/Introduction+to+Ceylon
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
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 specialsuper 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
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.
93 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
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; } } }
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!"; } }
95 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
(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:
96 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
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; } }
97 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
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 argumentAttribute<Bottom,String> would constrain the annotation to occur only at program elements that declare an attribute of typeString. Here's a couple of examples I copied and pasted straight from the language spec:
shared interface Scope of request | session | application
98 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
satisfies OptionalAnnotation<Scope,Type<Object>> {} shared class Todo(String text) satisfies OptionalAnnotation<Todo,Annotated> { shared actual String string = text; }
doc
annotation of the
Person
class, we write:
Note that the expression Person returns the metamodel object for the class Person, an instance of ConcreteClass<Person>. To determine if the method write:
stop()
of a class named
Thread
is deprecated, we can
Note that the expression Thread.stop returns the metamodel object for the method stop() of Thread, an instance ofMethod<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:
99 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
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; }
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 metaannotation 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; }
transactional
using a
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 {
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,R esult,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();
101 | P a g e Source: http://relation.to/tag/Introduction+to+Ceylon
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!
13. Resources
http://blog.talawah.net/2011/04/gavin-king-unviels-red-hats-top-secret.html. http://www.infoq.com/news/2011/04/ceylon. http://relation.to/tag/Ceylon. http://relation.to/Tutorials/IntroductionToCeylon. http://en.wikipedia.org/wiki/Ceylon_%28disambiguation%29.