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]]()

results matching ""

    No results matching ""