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:

  1. The object is created with primitive var fields initialised to their default values, i.e., 0 for age
  2. Initial values are computed in the order they appear in the class and assigned to their corresponding field, i.e., 42 evaluates to 42 and gets assigned to age
  3. 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:

  1. A new example object is created, with the following fields initialised: b (4711), c (0), and e ((0, 0))
  2. init() is run, initialising a, c and d -- notably relying on both this.b and this.c which is allowed as they have both valid values at this point
  3. After the -- at this point... comment, all fields are statically known to be initialised, making it OK to call this.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.

results matching ""

    No results matching ""