Actors

A screen, a row of a list, a reading on a monitoring wall, the point that talks to the network: each thing that holds state is written as an actor of its own. An actor holds only its own state and deals with other actors only by messages. There is no way to change another actor's state directly.

So only that actor can change its state. When a value goes wrong, there is one place to look.

A Message Nobody Handles Is Found Before It Runs

A message is a class, and the method that handles it is written with the class's name.

Object subclass: #Add fields: (n).
Add >> n = n.
Object subclass: #Get fields: ().

Actor subclass: #Counter fields: (count).
Counter >> Add m = self copyWith: { count: count + m n }.
Counter >> Get = ( self reply: count. self ).

c := Actor spawn: (Counter fields: { count: 0 }).
c tell: (Add fields: { n: 5 }).
((c ask: (Get fields: { })) await) displayNl.     "5"

Code that sends Counter a message it has no handler for is an error before the program starts. Inference knows which class the receiver is, so no type is written.

A handler answers the next state: it does not change the state, it answers a new value (copyWith:). Answering anything else is an error where it is written.

Sending and Waiting

Actor spawn: vmake an actor whose state is v
ref tell: msend m and do not wait
ref ask: msend m and take the answer later
f awaitwait for the answer
f awaitFor: mswait, giving up at the deadline
self reply: xgive the answer
OS sleep: mswait for a while

An actor handles its messages one at a time, in the order they arrived. Two of one actor's handlers never run at once, so there are no locks inside an actor.

Waiting Reads Top to Bottom

A handler that waits for an answer is written in one piece:

Actor subclass: #Client fields: (counter).
Client >> Go = (
    counter tell: (Add fields: { n: 5 }).
    n := (counter ask: (Get fields: { })) await.
    n displayNl.
    self).

While it waits on await, that actor stands still and every other actor runs on. When the answer arrives it carries on from where it stopped. There is no separate method to receive the answer and no function to call back later.

What Each State Accepts

Connected and disconnected, being typed and committed: when an actor's behaviour changes with its state, each state can be a class of its own.

Object subclass: #Open fields: ().
Object subclass: #Close fields: ().

Actor subclass: #Closed fields: ().
Closed class >> start = Closed fields: { }.
Closed >> Open = ('opening' displayNl. self become: (Opened fields: { visits: 1 })).
Closed >> Close = ('already closed' displayNl. self).

Actor subclass: #Opened fields: (visits).
Opened >> Open = ('already open' displayNl. self copyWith: { visits: visits + 1 }).
Opened >> Close = (('closing after ' , visits asString) displayNl. self become: (Closed fields: { })).

become: switches to another class. Each state's behaviour sits in its own class, so there is no code asking which state it is in and branching.

If the class switched to cannot handle a message the one before could, that is an error before the program runs. A sender can send safely without knowing which state the other is in.

An actor can also wait for a message of particular kinds (selective receive); the others are kept for later.

One Failing Leaves the Rest Running

If an actor fails partway through a handler, only that actor stops. The program and the other actors run on.

An actor watching it is told which actor stopped and why, and can make a new one or tell the user.

Actor subclass: #Boss fields: (lost).
Boss class >> start = Boss fields: { lost: 0 }.
Boss >> Down d = (
  ('lost a worker: ' , d reason) displayNl.
  self copyWith: { lost: lost + 1 }).
Boss >> Hire = (
  w := Actor spawn: (Worker fields: { name: 'a' }).
  me monitor: w.
  w tell: (Work fields: { }).
  w tell: (Boom fields: { }).
  self).

Whoever was waiting for an answer from the actor that stopped gets the wait back as a failure. Nobody is left waiting for an answer that will never come.

Running at Once, With No Thread or Lock Written

Heavy computation, and work that waits such as the network or a database, can go on another thread.

OS startWorkerDomains: 4.

a := Actor spawn: (Worker fields: { name: 'a' }) onDomain: 1.
b := Actor spawn: (Worker fields: { name: 'b' }) onDomain: 2.

Saying where is all it takes; the code that sends to it does not change. Values do not change, so nothing is copied when it goes to another thread, and nothing needs a lock. A thread with nothing to do takes over work from the others.

The actors that draw the screen run on the thread that has the window. Hand waiting work to an actor on another thread, and the screen keeps going while it waits.