gleam/otp/actor

This module provides the Actor abstraction, one of the most common building blocks of Gleam OTP programs.

An Actor is a process like any other BEAM process and can be used to hold state, execute code, and communicate with other processes by sending and receiving messages. The advantage of using the actor abstraction over a bare process is that it provides a single interface for commonly needed functionality, including support for the tracing and debugging features in OTP.

Gleam’s Actor is similar to Erlang’s gen_server and Elixir’s GenServer but differs in that it offers a fully typed interface. This different API is why Gleam uses the name “Actor” rather than some variation of “generic-server”.

Example

An Actor can be used to create a client-server interaction between an Actor (the server) and other processes (the clients). In this example we have an Actor that works as a stack, allowing clients to push and pop elements.

pub fn main() {
  // Start the actor with initial state of an empty list, and the
  // `handle_message` callback function (defined below).
  // We assert that it starts successfully.
  // 
  // In real-world Gleam OTP programs we would likely write a wrapper functions
  // called `start`, `push` `pop`, `shutdown` to start and interact with the
  // Actor. We are not doing that here for the sake of showing how the Actor 
  // API works.
  let assert Ok(actor) =
    actor.new([]) |> actor.on_message(handle_message) |> actor.start
  let subject = actor.data

  // We can send a message to the actor to push elements onto the stack.
  process.send(subject, Push("Joe"))
  process.send(subject, Push("Mike"))
  process.send(subject, Push("Robert"))

  // The `Push` message expects no response, these messages are sent purely for
  // the side effect of mutating the state held by the actor.
  //
  // We can also send the `Pop` message to take a value off of the actor's
  // stack. This message expects a response, so we use `process.call` to send a
  // message and wait until a reply is received.
  //
  // In this instance we are giving the actor 10 milliseconds to reply, if the
  // `call` function doesn't get a reply within this time it will panic and
  // crash the client process.
  let assert Ok("Robert") = process.call(subject, 10, Pop)
  let assert Ok("Mike") = process.call(subject, 10, Pop)
  let assert Ok("Joe") = process.call(subject, 10, Pop)

  // The stack is now empty, so if we pop again the actor replies with an error.
  let assert Error(Nil) = process.call(subject, 10, Pop)

  // Lastly, we can send a message to the actor asking it to shut down.
  process.send(subject, Shutdown)
}

Here is the code that is used to implement this actor:

// First step of implementing the stack Actor is to define the message type that
// it can receive.
//
// The type of the elements in the stack is not fixed so a type parameter
// is used for it instead of a concrete type such as `String` or `Int`.
pub type Message(element) {
  // The `Shutdown` message is used to tell the actor to stop.
  // It is the simplest message type, it contains no data.
  //
  // Most the time we don't define an API to shut down an actor, but in this
  // example we do to show how it can be done.
  Shutdown

  // The `Push` message is used to add a new element to the stack.
  // It contains the item to add, the type of which is the `element`
  // parameterised type.
  Push(push: element)

  // The `Pop` message is used to remove an element from the stack.
  // It contains a `Subject`, which is used to send the response back to the
  // message sender. In this case the reply is of type `Result(element, Nil)`.
  Pop(reply_with: Subject(Result(element, Nil)))
}

// The last part is to implement the `handle_message` callback function.
//
// This function is called by the Actor each for each message it receives.
// Actors are single threaded only does one thing at a time, so they handle
// messages sequentially and one at a time, in the order they are received.
//
// The function takes the message and the current state, and returns a data
// structure that indicates what to do next, along with the new state.
fn handle_message(
  stack: List(e),
  message: Message(e),
) -> actor.Next(List(e), Message(e)) {
  case message {
    // For the `Shutdown` message we return the `actor.stop` value, which causes
    // the actor to discard any remaining messages and stop.
    // We may chose to do some clean-up work here, but this actor doesn't need
    // to do this.
    Shutdown -> actor.stop()

    // For the `Push` message we add the new element to the stack and return
    // `actor.continue` with this new stack, causing the actor to process any
    // queued messages or wait for more.
    Push(value) -> {
      let new_state = [value, ..stack]
      actor.continue(new_state)
    }

    // For the `Pop` message we attempt to remove an element from the stack,
    // sending it or an error back to the caller, before continuing.
    Pop(client) -> {
      case stack {
        [] -> {
          // When the stack is empty we can't pop an element, so we send an
          // error back.
          process.send(client, Error(Nil))
          actor.continue([])
        }

        [first, ..rest] -> {
          // Otherwise we send the first element back and use the remaining
          // elements as the new state.
          process.send(client, Ok(first))
          actor.continue(rest)
        }
      }
    }
  }
}

Types

pub opaque type Builder(state, message, return)

A type returned from an actor’s initialiser, containing the actor state, a selector to receive messages using, and data to return to the parent.

Use the initialised, selecting, and returning functions to construct this type.

pub opaque type Initialised(state, message, data)

The type used to indicate what to do after handling a message.

pub opaque type Next(state, message)
pub type StartError {
  InitTimeout
  InitFailed(String)
  InitExited(process.ExitReason)
}

Constructors

A convenience for the type returned when an actor process is started.

pub type StartResult(data) =
  Result(Started(data), StartError)

A value returned to the parent when their child actor successfully starts.

pub type Started(data) {
  Started(pid: process.Pid, data: data)
}

Constructors

  • Started(pid: process.Pid, data: data)

    Arguments

    pid

    The process identifier of the started actor. This can be used to monitor the actor, make it exit, or anything else you can do with a pid.

    data

    Data returned by the actor after it initialised. Commonly this will be a subject that it will receive messages from.

Values

pub fn call(
  subject: process.Subject(message),
  timeout: Int,
  make_message: fn(process.Subject(reply)) -> message,
) -> reply

Send a synchronous message and wait for a response from the receiving process.

If a reply is not received within the given timeout then the sender process crashes. If you wish to receive a Result rather than crashing see the process.try_call function.

This is a re-export of process.call, for the sake of convenience.

pub fn continue(state: state) -> Next(state, message)

Indicate the actor should continue, processing any waiting or future messages.

pub fn initialised(
  state: state,
) -> Initialised(state, message, Nil)

Takes the post-initialisation state of the actor. This state will be passed to the on_message callback each time a message is received.

pub fn named(
  builder: Builder(state, message, return),
  name: process.Name(message),
) -> Builder(state, message, return)

Provide a name for the actor to be registered with when started, enabling it to receive messages via a named subject. This is useful for making processes that can take over from an older one that has exited due to a failure, or to avoid passing subjects from receiver processes to sender processes.

If the name is already registered to another process then the actor will fail to start.

When this function is used the actor’s default subject will be a named subject using this name.

pub fn new(
  state: state,
) -> Builder(state, message, process.Subject(message))

Create a builder for an actor without a custom initialiser. The actor returns a subject to the parent that can be used to send messages to the actor.

If the actor has been given a name with the named function then the subject is a named subject.

If you wish to create an actor with some other initialisation logic that runs before it starts handling messages, see new_with_initialiser.

pub fn new_with_initialiser(
  timeout: Int,
  initialise: fn(process.Subject(message)) -> Result(
    Initialised(state, message, return),
    String,
  ),
) -> Builder(state, message, return)

Create a builder for an actor with a custom initialiser that runs before the start function returns to the parent, and before the actor starts handling messages.

The first argument is a number of milliseconds that the initialiser function is expected to return within. If it takes longer the initialiser is considered to have failed and the actor will be killed, and an error will be returned to the parent.

The actor’s default subject is passed to the initialiser function. You can chose to return it to the parent with returning, use it in some other way, or ignore it completely.

If a custom selector is given using the selecting function then this overwrites the default selector, which selects for the default subject, so you will need to add the subject to the custom selector yourself.

pub fn on_message(
  builder: Builder(state, message, return),
  handler: fn(state, message) -> Next(state, message),
) -> Builder(state, message, return)

Set the message handler for the actor. This callback function will be called each time the actor receives a message.

Actors handle messages sequentially, later messages being handled after the previous one has been handled.

pub fn returning(
  initialised: Initialised(state, message, old_return),
  return: return,
) -> Initialised(state, message, return)

Add the data to return to the parent process. This might be a subject that the actor will receive messages over.

pub fn selecting(
  initialised: Initialised(state, old_message, return),
  selector: process.Selector(message),
) -> Initialised(state, message, return)

Add a selector for the actor to receive messages with.

If a message is received by the actor but not selected for with the selector then the actor will discard it and log a warning.

pub fn send(subject: process.Subject(msg), msg: msg) -> Nil

Send a message over a given channel.

This is a re-export of process.send, for the sake of convenience.

pub fn start(
  builder: Builder(state, msg, return),
) -> Result(Started(return), StartError)

Start an actor from a given specification. If the actor’s init function returns an error or does not return within init_timeout then an error is returned.

If you do not need to specify the initialisation behaviour of your actor consider using the start function.

pub fn stop() -> Next(state, message)

Indicate the actor should stop and shut-down, handling no futher messages.

The reason for exiting is Normal.

pub fn stop_abnormal(reason: String) -> Next(state, message)

Indicate the actor is in a bad state and should shut down. It will not handle any new messages, and any linked processes will also exit abnormally.

The provided reason will be given and propagated.

pub fn with_selector(
  value: Next(state, message),
  selector: process.Selector(message),
) -> Next(state, message)

Provide a selector to change the messages that the actor is handling going forward. This replaces any selector that was previously given in the actor’s init callback, or in any previous Next value.

Search Document