Encore in a Nutshell
Encore is a high-level object-oriented programming language with mixed imperative and functional flavour, aimed at implementing concurrent and parallel systems. The language is class-based and uses traits instead of inheritance.
Encore is an actor-based language, and every Encore program has at least one actor. Actors are lightweight concurrent objects which encapsulate one or more logical threads of control, and whose state is strongly isolated from all other actors. Encore support millions of actors, even on machines with just a few cores.
Actors communicate via asynchronous message sends. Messages may return futures, which are placeholders where the results of the computation is stored. Reading a future before it has been fulfilled will cause the reader to block until the value has been computed and stored in the future. An actor may perform synchronous calls on itself.
The internals of actors are constructed by passive objects, which are normal objects. By default, passive object have reference semantics. Method calls across passive objects are standard synchronous calls.
Encore primitive values are int
, uint
(unsigned int), float
and bool
.
All scalars are 64-bit values.
Here is the Encore Hello, world!-program:
active class Main
def main() : unit
print("Hello, world!")
end
end
Main
is a class that create actors, denoted by the keyword
active
at its start. Methods start with the keyword def
. The unit
type
is equivalent to void
from the C-family of languages. Statements
are separated by newlines. Encore is sensitive to indentation
because indentation should follow the control-flow of the program.
Changing indentation importantly never changes the semantics of
the program. As is visible from above, end
is used to end a method
and a class (and everything else) in Encore.
When run, this program spawns one actor from the Main
class and
sends it the main
message, causing the actor to respond by
running its main()
method, which prints a message to the
terminal. The following program contains three actors, Main,
Pong and Ping. Main creates the two others and tells Ping
to
send a pong
message to some Pong instance, passing itself in
as an argument.
active class Ping
def start(recv:Pong) : unit
recv ! pong(this)
end
def ping(sender:Pong) : unit
sender ! pong(this)
end
end
active class Pong
def pong(sender:Ping) : unit
sender ! ping(this)
end
end
active class Main
def main() : unit
val p1 = new Ping()
val p2 = new Pong()
p1 ! start(p2)
end
end
Next example is the classic threadring benchmark
implemented in Encore. It consists of two actor classes,
RingMember
and Main
. When the program starts, an instance of
Main
is spawned, and it immediately sent a main
message. This
prompts the creation of a ring, a cycle of 503 RingMember
instances.
active class RingMember
val id:int -- id is a constant for each ring member
var next:RingMember -- a handle to the next actor
def init(id:int) : unit -- constructor
this.id = id
end
def create_ring(id:int, ring_size:int, leader:RingMember) : unit
if id <= ring_size then
this.next = new RingMember(id)
this.next ! create_ring(id + 1, ring_size, leader)
else
this.next = leader
end
end
def send(hops:int) : unit
if hops > 0 then
this.next ! send(hops-1)
else
print(this.id)
end
end
end
active class Main
def main() : unit
val a = new RingMember(1)
a ! create_ring(2, 503, a)
a ! send(50 * 1000 * 1000)
end
end
Encore does not have a NULL
value. Instead, a Maybe
type is
used, whose possible values can be something, for example the
number 42, denoted Just(42)
(the type of which is Maybe[int]
),
or nothing, denoted Nothing
. For example, the signature of the
method to_i()
in the actor String
returns a Maybe[int]
to
account for the possibility of failure of converting the string to
an integer value.
def to_i() : Maybe[int]
To extract the value, we use pattern matching:
val str = "42"
val tmp = str.to_i()
match tmp with
case Just(num) => print("The number is: {}", num)
case Nothing => print("Failure in conversion")
end
Classes, Actors and Traits
Classes are constructed from traits. Traits require fields and methods and provide methods. Each trait has a mode which can be manifest (given at declaration) or given for each inclusion of a trait in a field.
Consider the Box trait, which implements a cell of type t
:
trait Box[t]
require var content : t
def set(v:t) : unit
this.content = v
end
def get() : t
this.content
end
end
To create an actor from the Box trait, we might do the following:
class ActiveBox[t] : active Box[t]
var content : t
end
By giving the Box trait the actor
mode, the Box interface
becomes asynchronous, meaning calls to set()
and get()
must
take place through message sends, and calls to get()
return a
Future[t]
value. An alternative syntax is the following, which specifies
the mode after inheriting the trait:
active class ActiveBox[t] : Box[t]
var content : t
end
In the future, the default mode, except in the above case, will be
subordinate
, which means an object which lives inside an actor and cannot
escape. A subordinate
box must now be declared as:
subord class PassiveBox[t] : Box[t]
var content : t
end
Note that while ActorBox
and PassiveBox
both were created from
the same trait, they are not assignment compatible, and their
interfaces are different. The ActorBox
's interface is
asynchronous and get()
returns future t
-value. The
PassiveBox
's interface is synchronous, and its get()
returns a
value of type t
.
The following possible use of an active box holding a subordinate passive box (of some value) is invalid, because a subordinate type cannot appear in the interface of an active type:
val box = new ActiveBox[PassiveBox[Any]]()