This is the second installment in a series of articles introducing the Ceylon language. Note that some features of the language may change before the final release.
Creating your own classes
Ceylon is an object oriented language, so we usually write most of our code in classes. A class is a type that packages:
- 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 class has a single attribute and a single method:
doc "A personalized greeting"
class Hello(String? name) {
doc "The greeting"
shared String greeting;
if (exists name) {
greeting = "Hello, " name "!";
}
else {
greeting = "Hello, World!";
}
doc "Print the greeting"
shared void say(OutputStream stream) {
stream.writeLine(greeting);
}
}
To understand this code completely, we're going to need to first explore Ceylon's approach to program element accessibility — the shared annotation above, then meet the concept of an attribute, and finally discuss how object initialization works in Ceylon.
Hiding implementation details
In Java and C#, a class controls the accessibility of its members using visibility modifier annotations, allowing the class to hide its internal implementation from other code. The visibility modifiers select between pre-defined, definite visibility levels like public, protected, package private, and private. Ceylon provides just one annotation for access control. The key difference is that Ceylon's shared annotation does not represent a single definite scope. Rather, its meaning is contextual, relative to the program element at which it appears. The shared annotation is in some cases more flexible, in certain cases less flexible, but almost always simpler and easier to use than the approach taken by Java and C#. And it's a far better fit to a language like Ceylon with a regular, recursive block structure.
Members of a class are hidden from code outside the body of the class by default — only members explicitly annotated shared are visible to other toplevel types or methods, other compilation units, other packages, or other modules. A shared member is visible to any code to which the class itself is visible.
And, of course, a class itself may be hidden from other code. By default, a toplevel class is hidden from code outside the package in which the class is defined — only toplevel classes explicitly annotated shared are visible to other packages or modules. A shared toplevel class is visible to any code to which the package containing the class is visible.
Finally, a package may be hidden from packages in other modules. In fact, packages are hidden from code outside the module to which the package belongs by default — only explicitly shared packages are visible to other modules.
It's not possible to create a shared toplevel class with package-private members. Members of a shared toplevel class must be either shared — in which case they're visible outside the package containing the class, or un-shared — in which case they're only visible to the class itself. Package-private functionality must be defined in un-shared (package-private) toplevel classes or interfaces. Likewise, a shared package can't contain a module-private toplevel class. Module-private toplevel classes must belong to unshared (module-private) packages.
Ceylon doesn't have anything like Java's protected. The purpose of visibility rules and access control is to limit dependencies between code that is developed by different teams, maintained by different developers, or has different release 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!
Abstracting state using attributes
The attribute greeting is a simple attribute, the closest thing Ceylon has to a Java field. Its value is specified immediately after it is declared. Usually we can declare and specify the value of an attribute in a single line of code.
shared String greeting = "Hello, " name "!";
shared Natural months = years * 12;
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 polymorphic—an attribute definition may be refined (overridden) by a subclass.
We could rewrite the attribute greeting as a getter:
shared String greeting {
if (exists name) {
return "Hello, " name "!";
}
else {
return "Hello, World!";
}
}
Notice that the syntax of a getter declaration looks a lot like a method declaration with no parameter list.
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!
Understanding object initialization
In Ceylon, classes don't have constructors. Instead:
- 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!";
}
}
...
}
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 of name, 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).
Instantiating classes and overloading their initializer parameters
Oops, I got so excited about attributes and closure and null value handling that I forgot to show you the code that uses Hello!
doc "Print a personalized greeting"
void hello() {
Hello(process.args.first).say(process.output);
}
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 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 price to pay for a language with clearer semantics, without nasty corner cases, that is ultimately more powerful.
Let's overload
Hello, and its say() method, using defaulted parameters:
doc "A command line greeting"
class Hello(String? name = process.args.first) {
...
doc "Print the greeting"
shared void say(OutputStream stream = process.output) {
stream.writeLine(greeting);
}
}
Our hello() method is now looking really simple:
doc "Print a personalized greeting"
void hello() {
Hello().say();
}
There's more...
In Part 3, we'll explore inheritance and refinement (overriding).
Look like you guys need help with the SPAM. Why don't you look at Mollom.com for Spam filter? (It's java in the backend :)
Ok, my question is on this code:
name?.uppercase?
so, if there is no 'name' defined or null, what is '.uppercase'? would that give you a nasty NPE :)
No, the point of the ?. operator is that it is a nullsafe invocation. The pseudo-code definition of ?. is:
It accepts an optional type and returns an optional type.
Personally I dislike the syntax of the languages. Looks as ugly as JavaFX Script IMHO.
language...
Seeing "shared" being used very often in your examples (and comparing with the usage of "public" in other languages) I'd like to suggest making "shared" the default and non-shared to be annotated (probably with "private" or some other annotation),
i.e. instead of
class Hello {
String greeting = "Hello";
shared void say() {
writeLine(greeting);
}
}
you would have
class Hello {
private String greeting = "Hello";
void say() {
writeLine(greeting);
}
}
Actually the language is quite visually similar to JavaFX Script, but without all the colons. Ceylon sticks stubbornly with Integer i instead of var i:Integer, and uses = instead of : in named argument lists. Also Ceylon uses {x, y, z} to construct sequences, whereas JavaFX uses [x, y, z]. But there is indeed a lot of similarity. Of course, Ceylon is a more powerful, more general-purpose language.
We're following a general principle that for features like access control and mutability control, the default should always be the most restrictive option. Otherwise, the reader of the code never knows if a variable is mutable by intent or by accident, or if a member is shared by intent or by omission.
To see why this is right, just look at how useless final and package private visibility turned out to be in Java. When you see a declaration without an access modifier in Java, you never know if it was really intended to be package private, or if the author just accidentally forgot to add a private modifier. If private were the default, and package private were only accessible via the modifier package, then this problem would not occur. You could be sure that the thing was package private by intent. Likewise, most developers just leave final off, to save the effort. But if final were the default, they would be forced to explicitly annotated variables variable. The idea is to have the compiler impose the kind of discipline on you that you're very bad at imposing on yourself. Then the end result is that you end up with a lot more information in the code when you come back to look at it later.
You certainly have a point there: although I've been using thoroughly (I think :-) I did forget more often than I'd like to (even though I'm a big fan of pure funcional programming and like to use Haskell for many things...)
Thanks for sharing your reasoning!
One question: How are you going to interoperate with Java (i.e. use the existing code) if Ceylon does not support overloads? Thank you.
In some cases, it's possible to map the syntax of a Java overloaded method to something that is legal in Ceylon. For example, if we have void print(String s) and void print(Named n), we'll be able to pretend there's a single method void print(String|Named o). Actually, depending upon how far you want to go with this, there's really a lot of cases which can be handled this way if you support a construct called GADT (Generalized Algebraic Data Type), which you may be familiar with from Haskell.
But it may simply be easier to directly hack in some support for overloading into the compiler, especially for Java interoperation, with certain restrictions (for example, no type inference).
And remember, for really difficult cases you always have the option of wrapping the Java API in un-overloaded Java. Which I think will be the approach at first.
Why a new keyword shared instead of the familiar public? Seems like one more thing for Java developers to learn.
Because shared isn't the same as public. Ceylon's visibility model is quite different to Java's private/package/protected/public model. It would be much more confusing for people coming from Java to change the meaning of something that they already think they understand.
To me they seem similar enough, public/shared meaning visible outside the class/package. As a Java programmer myself, I don't see the confusion, and would prefer public.
shared sometimes means visible-everywhere (public), sometimes means module-private, sometimes means package-private, sometimes means private-to-outer-class. It's meaning is contextual, To me, that's pretty different. I'm sure you'll get used to seeing the different word.
But doesn't public in Java work the same way? A public field in a non-public class in Java is also not accessible outside the package, or at least I hope not.
Well, you're sorta right. It's actually a little interesting. The following class passes the typechecker in Java:
class Foo { public String hello = "hello"; }You can even write a public static method that returns Foo:
public Test { public static Foo getFoo() { return new Foo(); } }However, you do actually get an error at the callsite if you try to do the following from a different package:
The error says:
i.e. you've violated the visibility restrictions on Foo, not on hello. So in effect you're correct, but more be side-effect ;-)
I don't think these semantics are at all clear, frankly.
Ceylon does things a bit differently. The semantics are simply different. This is again not an error:
class Foo() { shared String hello = "hello"; }(It's not an error, since the visibility of shared in this context is whatever scope contains Foo, i.e. the package.)
But Ceylon will give you an error when you try to define the following toplevel method:
shared Foo getFoo() { return Foo(); }The error says:
I think that's a lot better.
Continuing the discussion, here's an example where you're assertion is just plain wrong. Let's start with the same Foo above:
class Foo { public String hello = "hello"; }Now let's give it a public subclass:
public class Bar extends Foo {}Now, in another package, I can write:
Without any error. So hello truly is public.
Now, in Ceylon, let's do the same:
class Foo() { shared String hello = "hello"; }class Bar() extends Foo() {}Now this line in another package will cause an error, since hello is package private:
Well, at least it is supposed to. I just tried this out and realized that this currently accepted by the typechecker because of a bug. I'll fix the bug today :-)
where your asserion
Why do you think it's a side-effect? It seems to me it's working exactly as designed.
I respectfully disagree. I re-read that error message a few times, and I'm still not sure what it's saying :-(
I get this error trying to compile:
javac test2/Test2.java test2/Test2.java:10: hello in test1.Foo is defined in an inaccessible class or interface String s = Test.getFoo().hello; ^ 1 errorSo to me they seem to work the same way, and the error message makes much more sense to me.
This also seems to me to be working exactly as designed. Because Bar is public, its public fields are now accessible outside the package.
class Bar() or shared class Bar()?
So I gather you want to prevent subclasses from exposing package-private attributes in base classes. I suppose that's another attempt to make Ceylon safer, and in keeping with the design goal of the language.
However, I would still say that shared is so substantially similar to public, that you should consider just using public and document the difference. Because people are going to say anyway.
If I annotate method or field as public inability to use it from different package is very strange.