This is the first installment in a series of articles introducing the Ceylon language. Note that some features of the language may change before the final release.
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.
Writing a simple program
Here's a classic example program.
void hello() { writeLine("Hello, World!"); }
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 side-effects.)
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.
Adding inline documentation
It's usually a good idea to add some kind of documentation to important methods like hello(). One way we could do this is by using a C-style comment, either like this:
/* The classic Hello World program */ void hello() { writeLine("Hello, World!"); }
Or like this:
//The classic Hello World program void hello() { writeLine("Hello, World!"); }
But it's much better to use the doc annotation for comments that describe declarations.
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.
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.
Strings and string interpolation
The Hello World program — wildly popular as it is — provides for a rather limited and monotonous user experience. A more typical program would produce output that sometimes varies between different runs.
Let's ask our program to tell us a little more about itself.
doc "The Hello World program ... version 1.1!" void hello() { writeLine("Hello, this is Ceylon " process.languageVersion " running on Java " process.javaVersion "!"); }
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!
A quick fix is to terminate the template with an empty string literal:
writeLine("Hello, this is Ceylon " process.languageVersion "");
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. The + operator immediately evaluates its operand expressions and produces an immutable String object. 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.
Dealing with objects that aren't there
An even more exciting program would receive input and produce output that depends upon the input it receives. Of course, this places somewhat more demanding requirements upon the user, but the extra work does sometimes pay off!
Therefore, this improved version of the original Hello World program takes a name as input from the command line. We have to account for the case where nothing was specified at the command line, which gives us an opportunity to explore how null values are treated in Ceylon, which is quite different to what you're probably used to in Java or C#.
doc "Print a personalized greeting" void hello() { String? name = process.arguments.first; String greeting; if (exists name) { greeting = "Hello, " name "!"; } else { greeting = "Hello, World!"; } writeLine(greeting); }
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 optional type. And unlike other languages with typesafe null value handling, an optional type in Ceylon is not an algebraic datatype that wraps
the definite value. Instead, Ceylon uses an ad-hoc union 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 assign null 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 a NullPointerException 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 name inside the if (exists ... ) condition:
String greeting; if (exists String name = process.arguments.first) { greeting = "Hello, " name "!"; } else { greeting = "Hello, World!"; } writeLine(greeting);
This is the preferred style most of the time, since we can't actually use name for anything useful outside of the if (exists ... ) construct.
Operators for handling null values
While we're discussing null values, I should tell you about an even easier way to define greeting, using the ? operator. It's just a little extra syntax sugar to save some keystrokes.
shared String greeting = "Hello, " + name?"World";
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.
There's more...
In Part 2, we'll see how to define our own types: classes, interfaces, and objects.