Actors

Defining an actor

Only actors can hold values that change over time, such as the contents of a list or the text being typed into a field. An actor is defined as a class. Its fields declare the values it holds, and its methods are the messages it accepts.

Actor subclass: #Cart fields: (items : Array[Str]).

Cart class >> new   = Cart fields: { items: #[] }.
Cart >> add: item   = self with: { items: items copyWith: item }.
Cart >> count       = self reply: items size.

Cart holds an array of strings, items. When it receives add:, it returns a new state with one more item. When it receives count, it replies with the number of items.

A method is a message handler. A handler returns the next state. self with: { ... } returns a new state with the named fields replaced; the original state is unchanged. A handler that needs to reply uses self reply:.

There is no assignment to fields. To change state, a handler builds a new state with self with: and returns it. An actor's state changes only when a handler returns a new state.

tell and ask

c := Actor spawn: Cart new.

c tell add: '六角ボルト'.              "send; do not wait"
(c ask count) forced printNl.          "wait for the reply"

tell sends a message and does not wait for a reply. ask requests a reply and returns a Future. forced waits for the reply.

If a handler uses forced, the actor does not process its next message until the reply arrives. Other actors keep running. In a measurement, an actor that sent three requests taking 100ms each processed them at 104ms, 207ms, and 320ms, while another actor ran at 3ms, 70ms, and 136ms.

Messages arrive in the order they were sent. Messages that arrive while a handler is waiting are processed in order after the reply.

Not waiting for a reply

A handler can finish without waiting for a server response and receive the response later as a message to itself.

Screen >> load = (
  (Http get: 'orders.json') then: [:r | me ref tell loaded: r].
  self ).

Screen >> loaded: r = self with: { rows: ... }.

load returns immediately, so the screen keeps responding to input while the request is in flight. The response arrives as a loaded: message. WebSocket handlers are written the same way: the receive callback sends me ref tell arrived:.

A handler's return value cannot be discarded

A handler's return value is the actor's next state. Calling a handler like an ordinary method and discarding the result is a compile error.

Counter >> inc = self with: { count: count + 1 }.

Counter >> bump = self inc.              "allowed: inc's return value is bump's return value"

Counter >> reset = (
  self inc.                              "compile error: inc's return value is discarded"
  self with: { count: 0 } ).
"inc" on Counter is a handler, so what it answers is the next state — here
that answer is dropped. Put the send where this handler answers, or send it
as a message with `me ref tell inc`

Returning another handler's result directly, as bump does, is allowed. Calling a handler mid-way and discarding the result, as reset does, is not. To do that, send the message to yourself with me ref tell inc.

Without this check, self inc in reset would run without changing the state and without raising an error.

Ordinary methods on an actor class, such as helpers that build the view, are not restricted.

When a handler raises

There is no try. If a handler raises an error, the actor stops.

Boss >> start   = ( me monitor: worker. self ).
Boss >> down: d = self with: { log: log copyWith: d actor name , ' が止まった' }.

After me monitor: worker, the actor receives a down: message when worker stops. With link:, the actor stops when the other stops. A supervisor is an actor that receives down: and spawns a replacement.

Only the failing actor stops. Other actors and other regions of the page are not affected.

The DOM region the stopped actor was drawing stays as it was last drawn. Clicking a button in it does nothing, because the message has no actor to go to. So the runtime shows a notice in that region saying the actor has stopped. If the page has an element with id="lps-dead", its content is used as the notice. Otherwise a built-in notice is shown. The built-in notice carries its own styling, so it is visible even if the page's CSS has nothing for it.

The notice is shown above the region's previous content, which is left in place so that the user can see what they were doing when the error occurred.

The notice is shown only when an actor stops because of an error. It is not shown after me stop.

Switching class with become:

become: replaces the actor with an instance of another class. Later messages are handled by the new class's methods. Use it when the set of messages an actor accepts depends on its state.

Door >> open = self become: (OpenDoor fields: {}).

The type of the reference determines which messages the new class must accept.

A reference created with Actor spawn: has type ActorRef[Door]. Any message Door accepts can be sent through it, so the new class must accept all of them. Otherwise it is a compile error.

A reference created with Actor spawn:as: is typed by a protocol and accepts only that protocol's messages. The new class needs to implement only the protocol.

spawn:as: types the reference on the side that creates the child. ref as: P types a reference on the side that hands it over. If a child is in a separate module, it cannot name the parent's class, because the parent imports the child and naming the parent back would create an import cycle. So the parent passes me ref as: Owner. The child knows only the Owner protocol, and messages from the child to the parent are type-checked against it.

Desk >> hire: n = self with: {
  kids: kids copyWith: (me spawn: (Kid named: n owner: (me ref as: Owner))) }.

A reference typed only as ActorRef[a] has no handlers to check a message name against. A tell to it is a compile error.

nothing says which actor "nosuchThing:" is sent to, so it cannot be checked —
type the reference by a protocol: `ActorRef[P]`, made with `ref as: P` or `spawn:as:`

Deadlock detection

If two actors wait for each other's reply, neither can proceed. Neither has stopped with an error, so monitor: does not detect it.

Before sending, ask follows the chain of actors the receiver is waiting on. If the chain leads back to the sender, ask raises an error.

Pong#2 asked Ping#3, which is waiting on it — nobody can take a turn

Only actual deadlocks are detected. There are no false positives.

Deadlocks across Workers are also detected. An ask to another thread carries the addresses and names of the actors waiting behind it. The receiving thread raises an error if the target is in that list. The decision is made on the receiving thread without querying the other thread.

Far#1 asked Home#1, which is waiting on it — nobody can take a turn