Traits and Classes
This section describes traits and classes in Encore. These can also be thought of a reference capabilities, as, combined with an alias mode, they define what can be done with a reference.
Traits
Encore traits are building blocks for creating classes. Traits require fields and methods, and provide methods. When creating a class C from the traits T1 and T2, C must declare the fields that T1 and T2 require. If T1 requires a method m, m must be provided by T2.
Traits may specify their mode explicitly, or the mode may be
provided later. For example, here is a
local trait Box
:
local trait Box[t]
require var content : t
def set(v:t) : Unit
this.content = v
end
def get() : t
this.content
end
end
The Box
trait is parameterised over a type t
, requires a
mutable field content
of type t
, and provides setters and
getters for the content
field.
Omitting the mode of the Box
trait (i.e., removing the local
keyword above) requires the mode to be provided at composition
time, i.e., when the trait is combined with a class. Through this
delayed specification of the mode, we can use the same trait to
construct several kinds of classes. Below, we sketch how Box
can
be used to create both a LocalIntBox
class whose accesses are
restricted to the actor that creates it, and an ActorIntBox
whose interface is asynchronous and whose accesses are serialised
in a message queue.
class LocalIntBox : local Box[int] ...
class ActorIntBox : active Box[int] ...
Classes
Classes are blueprints for creating objects. Classes are
constructed from traits. Classes contain zero or more field
declarations, an optional, single constructor method called
init
, and zero or more method declarations.
A number of standard traits exist and provide methods for things like identity comparison and hash codes. They must be included in a class explicitly.
class Person : Id
var name : String
var age : int
def init(name:String, age:int) : Unit
this.name = name
this.age = age
end
end
A class can consist of many traits, e.g.:
class Person : (Id + Hash) * Named ...
The operators +
and *
control how traits may share data inside
a class. In the above specification of Person
, Id
and Hash
may share mutable state with each other, but not with Named
.
This means that an instance of this particular class can be
manipulated by two threads simultaneously, one accessing its Id +
Hash
interface only, and one accessing only through Named
.
Actors
An actor in Encore is an instance of a class with at least one
trait whose mode is active
. For example, we can create an actor
from the Box
trait above thus:
class ActorBox[t] : active Box[t]
var content : t
def init(v:t) : Unit
this.set(v)
end
end
Instantiating the ActorBox
class will create objects whose
interfaces are asynchronous and whose return values will be
futures. Thus, the following code is well-typed:
val ab = ActorBox(42) -- int type argument is inferred
var f = ab ! get() -- returns a Future[int]
An attempt at performing a synchronous call on an ActorBox
will
result in a compile-time error. Note that internal synchronous
calls are allowed, so the this.set(v)
inside the ActorBox
is allowed, and does not involve message passing.
Constructors and Object Construction
Each object can have at most one constructor, which is called init
and returns unit
. If a constructor exists, it must be called at
object creation time. Calling a constructor like a normal method is
a compile-time error. The following class has a constructor, and
a field with an initial value:
local class Person
var name : String
var age : int = 42
def init(name : String) : unit
this.name = name
end
end
Creating a new instance of Person
take this shape:
new Person("Constructor Argument")
This performs the following steps in precisely this order:
- The object is created with primitive var fields initialised
to their default values, i.e.,
0
forage
- Initial values are computed in the order they appear in
the class and assigned to their corresponding field, i.e.,
42
evaluates to42
and gets assigned toage
- The
init()
method is run
To avoid the constructor observing the object in an uninitialised state, method calls are disallowed in the constructor until all fields have been properly initialised. Local functions in a constructor will also not be able to access fields unless they have a default or initial value. The following example demonstrates a correct use of initialiser:
local class Example
val a : int
val b = 4711
var c : int
var d : String
var e : (int, int)
def init(s : String) : Unit
this.a = first()
this.c = second(this.c, this.b)
this.d = s
-- at this point, all fields have an OK value
this.foo()
where
fun first() = 42
fun second(x : int, y : int) = x + y + this.e.1
-- fun bad1() = this.d
-- fun bad2() = this.foo()
fun good(e : Example) = e.foo()
end
...
end
When executing Example("string")
, the following takes place:
- A new example object is created, with the following fields
initialised:
b
(4711
),c
(0
), ande
((0, 0)
) init()
is run, initialisinga
,c
andd
-- notably relying on boththis.b
andthis.c
which is allowed as they have both valid values at this point- After the
-- at this point...
comment, all fields are statically known to be initialised, making it OK to callthis.foo()
. Moving this call earlier would have been a compile error.
The local function bad1()
demonstrates that this.d
may not
be accessed inside a local function, because it is known whether
this.d
has been properly initialised before the call to this
function. bad2()
is disallowed for the same reason. Both these
functions would cause a compile error.
Note that good()
allows calling this.foo()
because it takes
an (implicitly fully initialised) Example
as input. Calling
good()
is only allowed after the -- at this point...
comment.
Note that the checks and other features described in this section have not yet been fully implemented.
Traits and Polymorphism
Building Java-style interfaces using traits is simple and involves
requiring methods. For example, we could define the equivalent of
the Java Iterator
interface thus:
trait Iterator[t]
require def next() : t
require def has_next() : bool
require def remove() : bool
end
Defining a list class that uses the iterator trait must then define those methods.