Classes
This is the second step in our tour of the Ceylon language. In the previous leg you learned some of the basics of the syntax of Ceylon. In this leg we're going to learn how to define classes. A class is one kind of type, and what makes a type interesting is its members. In Ceylon, the members of a type are:
- methods (member functions),
- attributes (member values), and
- member classes.
Methods, attributes, and member classes are polymorphic, that is, their definitions may be refined by a subtype.
In this chapter we're going to focus on methods and attributes. We'll discuss member classes in the next chapter.
First, we need to know about one little restriction that's quite specific to Ceylon.
Identifier naming
The case of the first character of an identifier is significant:
- Type (interface, class, and type parameter) names must start with an initial capital letter.
- Function and value names start with an initial lowercase letter or underscore.
The Ceylon compiler is very fussy about this. You'll get a error if you write:
class hello() { ... } //compile error
or:
String Name = .... //compile error
There is a way to work around this restriction, which is mainly
useful when calling legacy Java code. You can "force" the compiler
to understand that an identifier is a type name by prefixing it with
\I
, or that it is a function or value name by prefixing it with
\i
. For example, \iRED
is considered an initial lowercase
identifier.
So the following declarations are acceptable, but definitely not recommended, except in the interop scenario:
class \Ihello() { ... } //OK, but not recommended
and:
String \iName = .... //OK, but not recommended
Creating your own class
Our first class is going to represent points in a polar coordinate system. Our class has two parameters, two methods, and an attribute.
"A polar coordinate"
class Polar(Float angle, Float radius) {
shared Polar rotate(Float rotation)
=> Polar(angle+rotation, radius);
shared Polar dilate(Float dilation)
=> Polar(angle, radius*dilation);
shared String description
= "(``radius``,``angle``)";
}
There's two things in particular to notice here:
The parameters used to instantiate a class are specified as part of the class declaration, right after the name of the class. This syntax is less verbose and more regular than Java, C#, or C++. We do have constructors in Ceylon, but we rarely need them, and they shouldn't be the first thing you reach for.
We make use of the parameters of a class anywhere within the body of the class. In Ceylon, we often don't need to define explicit members of the class to hold the parameter values. Instead, we can access the parameters
angle
andradius
directly from therotate()
anddilate()
methods, and from the expression which specifies the value ofdescription
.Logic to initialize the values of attributes of a class—it's initializer—is written directly in the body of the class.
Notice also that Ceylon doesn't have a new
keyword to indicate
instantiation, we just "invoke the class", writing:
Polar(angle, radius)
Hiding implementation details
Ceylon doesn't make a distinction between public
, protected
and
"default" visibility like Java does; here's why.
Instead, the language distinguishes between:
- program elements which are visible only inside the scope in which they are defined, and
- program elements which are visible wherever the thing they belong to (a type, package, or module) is visible.
By default, members of a class are hidden from code outside the body
of the class. By annotating a member with the shared
annotation,
we declare that the 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. Annotating a top level class with shared
makes it visible to any code to which the package containing the class
is visible.
Tip: using restricted
- a member declaration annotated
restricted shared
is accessible only within the package in which it is declared, even if the type it belongs to isshared
, - a member or toplevel declaration annotated
restricted(`module`) shared
is accessible only within the module in which it is declared, even if the package it belongs to isshared
, and - a declaration or package annotated
restricted(`module foo`, `module bar`) shared
is accessible only within the explicitly listed modules, and within the package in which it is declared.
Note that adding the restricted
annotation always narrows the
visibility of a shared
declaration. But adding additional modules as
arguments to restricted
widens the visibility of a restricted shared
declaration.
The modules listed as arguments to the restricted
annotation are
sometimes called "friend" modules.
Class attributes
An attribute is a member of a class that represents state. Very often, particularly in the very important case of an immutable class, the state of a class is derived from the arguments used to instantiate a class. Therefore, there is a close relationship between class parameters and attributes.
Exposing parameters as attributes
If we want to expose the angle
and radius
of our Polar
coordinate to other code, we'll need to define attributes of the class.
It's very common to assign parameters of a class directly to a shared
attribute of the class, so Ceylon provides a streamlined syntax for
this.
"A polar coordinate"
class Polar(angle, radius) {
shared Float angle;
shared Float radius;
shared Polar rotate(Float rotation)
=> Polar(angle+rotation, radius);
shared Polar dilate(Float dilation)
=> Polar(angle, radius*dilation);
shared String description
= "(``radius``,``angle``)";
}
Code that uses Polar
can access the attributes of the class using a
very convenient syntax.
Cartesian cartesian(Polar polar)
=> Cartesian(polar.radius*cos(polar.angle),
polar.radius*sin(polar.angle));
There's an even more compact way to write the code above, though it's often less readable:
"A polar coordinate"
class Polar(shared Float angle, shared Float radius) {
shared Polar rotate(Float rotation)
=> Polar(angle+rotation, radius);
shared Polar dilate(Float dilation)
=> Polar(angle, radius*dilation);
shared String description
= "(``radius``,``angle``)";
}
This illustrates an important feature of Ceylon: there is almost no essential difference, aside from syntax, between a parameter of a class, and a value declared in the body of the class.
Instead of declaring the attributes in the body of the class, we
simply annotated the parameters shared
. We encourage you to avoid
this shortcut when you have more than one or two parameters.
Initializing attributes
The attributes angle
and radius
are references, the closest
thing Ceylon has to a Java field. Usually we specify the value of a
reference when we declare it.
Float x = radius * sin(angle);
String greeting = "Hello, ``name``!";
Integer months = years * 12;
On the other hand, it's sometimes useful to separate declaration from assignment.
shared String description;
if (exists label) {
description = label;
}
else {
description = "(``radius``,``angle``)";
}
But if our class doesn't have constructors, where precisely should we put this code? We put it directly in the body of the class!
"A polar coordinate with an optional label"
class Polar(angle, radius, String? label) {
shared Float angle;
shared Float radius;
shared String description;
if (exists label) {
description = label;
}
else {
description = "(``radius``,``angle``)";
}
// ...
}
The Ceylon compiler forces you to specify a value of any reference before making use of the reference in an expression.
Integer count;
void inc() {
count++; //compile error
}
We'll learn more about this later in the tour.
Abstracting state using attributes
If you're used to writing JavaBeans, you can think of a reference as a combination of several things:
- a field,
- a getter, and, sometimes,
- a setter.
That's because not every value is a reference like the one we've just seen; others are more like a getter method, or, sometimes, like a getter and setter method pair.
We'll need to expose the equivalent cartesian coordinates of a
Polar
. Since the cartesian coordinates can be computed from the
polar coordinates, we don't need to define state-holding references.
Instead, we can define the attributes as getters.
import ceylon.math.float { sin, cos }
"A polar coordinate"
class Polar(angle, radius) {
shared Float angle;
shared Float radius;
shared Float x => radius * cos(angle);
shared Float y => radius * sin(angle);
// ...
}
Notice that the syntax of a getter declaration looks a lot like a method declaration with no parameter list.
So in what way are attributes "abstracting state"? Well, code that
uses Polar
never needs to know if an attribute is a reference or a
getter. Now that we know about getters, we could rewrite our
description
attribute as a getter, without affecting any code that
uses it.
"A polar coordinate, with an optional label"
class Polar(angle, radius, String? label) {
shared Float angle;
shared Float radius;
shared String description {
if (exists label) {
return label;
}
else {
return "(``radius``,``angle``)";
}
}
}
Avoiding static members
Ceylon does feature static members, just like Java, C#, or C++.
However, static
was added very late in the evolution of the
language, and is barely used in idiomatic Ceylon code. Instead of a
static
member, we usually:
- use a toplevel function or value declaration, or
- in the case where several "static" declarations need to share some
private stuff, regular members of a singleton
object
declaration, which we'll meet right at the start of the next chapter.
However, it's very common to see something that looks a whole lot like a reference to a static member, but isn't. This results in a minor gotcha for newcomers.
Gotcha!
The syntax Polar.radius
is legal in Ceylon, and we even call it a
static reference, but it does not usually mean what you think it
means!
Sure, if you're taking advantage of Ceylon's Java interop, you can call a static member of a Java class using this syntax, just like you would in Java:
import java.lang { Runtime }
Integer procs = Runtime.runtime.availableProcessors();
Or, alternatively, you could write the following, directly import
ing
the static member:
import java.lang { Runtime { runtime } }
Integer procs = runtime.availableProcessors();
But in regular Ceylon code, an expression like Polar.radius
is not
usually a reference to a static member of the class Polar
. We'll
come back to the question of what a "static reference" really is,
when we discuss higher-order functions.
Living without overloading
It's time for some bad news: Ceylon doesn't have method or constructor overloading (the truth is that overloading is the source of various problems in Java, especially when generics come into play). However we can emulate most non-harmful uses of constructor or method overloading using:
- defaulted parameters,
- variadic parameters (varargs), and
- union types or enumerated type constraints.
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 println(String line, String eol = "\n")
=> process.write(line + eol);
//variadic parameter
void printlns(String* lines) {
for (line in lines) {
println(line);
}
}
//union type
void printName(String|Named name) {
switch (name)
case (is String) {
println(name);
}
case (is Named) {
println(name.first + " " + name.last);
}
}
Don't worry if you don't completely understand the third example just yet, we'll come back to it later in the tour.
Let's make use of this idea to "overload" the "constructor" of Polar
.
"A polar coordinate with an optional label"
class Polar(angle, radius, String? label=null) {
shared Float angle;
shared Float radius;
shared String description {
if (exists label) {
return label;
}
else {
return "(``radius``,``angle``)";
}
}
// ...
}
Now we can create Polar
coordinates with or without labels:
Polar origin = Polar(0.0, 0.0, "origin");
Polar coord = Polar(r, theta);
Later, we'll learn about named arguments, which we often use to make instantiation expressions more readable, especially when the class has more than two parameters:
Polar origin = Polar { angle = 0.0; radius = 0.0; label = "origin"; };
Polar coord = Polar { radius = r; angle = theta; };
Finally, it's worth noting that very many uses cases for overloading in Java involve the use of primitive types, which we can't abstract over in Java's type system. In Ceylon, there are no primitive types, and so we can often use generics instead of overloading.
Gotcha!
Even with these "emulation" techniques, not every case of a legal overloaded Java method can be represented directly in Ceylon. In such situations it's necessary to exert a little more effort to come up with distinct names.
Tip: using named constructors instead of overloading
When we have multiple ways to create a class, and none of the above techniques for "emulating" overloading works out, we probably need to give the class one or more named constructors. We'll learn about constructors much later in this tour, because they're only rarely used.
Tip: overloading when compiling for the JVM
Finally, the Ceylon compiler actually does allow you to declare an
overloaded method or constructor when compiling a class
that is explicitly marked native("jvm")
. This feature is provided to
ease interoperation with native Java code.
There's more...
In the next chapter, we'll continue our investigation of attributes, and especially variable attributes. We'll also meet Ceylon's control structures.
(We'll wait until a later chapter to learn more about methods.)