DEV Community

Kir Axanov
Kir Axanov

Posted on • Edited on

Code. Gleam. Named actors

Hi!

DISCLAIMER

This is a workaround!
Totally not tested under any production load, you've been warned!

Problem

We cannot have a global state in such an FP language as Gleam.

So, when we want to send a message to some specific (singleton) actor, we need to pass the reference to it as an argument. Every. Time.

Workaround

Words

So. Erlang has the pool of named processes, which we can query to get a process by its assigned unique name...

But as of now it is not type-checked by Gleam:

There is no support for named processes. They are untyped global mutable variables which may be uninitialized, more research is needed to find a suitable type safe alternative.

And all we can get is a process ID (pid). Which is not what we need to send a message to our actor.

But! There is hope workaround! While we are waiting for proper implementation of named actors.

We can send our message as a string to any process by its pid! All we need is to... kill it!

Using abnormal exit we can send any string as an exit reason to our process. We trap[1][2] this exit in our singleton actor (as it is linked with the process we are killing) and parse received message from this string.

Diagrams

Actors and processes

Initialization steps

What happens under the hood

Code

Usage example:

import gleam/io
import gleam/erlang/process

import named_actor


pub fn main() {
  // Just echo the message
  let _ = named_actor.new("my_singleton_actor", fn(msg) {
    io.debug(msg)
    Nil
  })

  // Send a message to you actor only by its name! Waiting is optional, just for the demo.
  process.sleep(500)
  named_actor.send("my_singleton_actor", "YOLO!!!1!")

  // Send a new message, you got this.
  process.sleep(1500)
  named_actor.send("my_singleton_actor", "Hieee!!!")

  // Keep the main process running
  process.sleep_forever()
}
Enter fullscreen mode Exit fullscreen mode

The whole code in named_actor.gleam:

//// A workaround to have a named actor with disposable proxy process with abnormal exits.
//// This actor is implemented with a Nil-state.
import gleam/string
import gleam/function
import gleam/dynamic
import gleam/otp/actor
import gleam/erlang/atom.{ create_from_string as atom }
import gleam/erlang/process


//---------------------------------------------------------------------------//
// Constants.                                                                //
//---------------------------------------------------------------------------//
/// How much time is given to actor to start.
/// In milliseconds.
const init_timeout = 500

/// How much time to wait between tries of getting named process.
/// In milliseconds.
const send_timeout = 100


//---------------------------------------------------------------------------//
// Methods.                                                                  //
//---------------------------------------------------------------------------//
/// Creates an actor and a named proxy process to send messages to that actor.
pub fn new(
  name: String,
  handle_msg: fn(dynamic.Dynamic) -> Nil,
) -> Result(process.Subject(dynamic.Dynamic), actor.StartError) {
  actor.start_spec(actor.Spec(
    init: fn() { init_actor(name) },
    init_timeout:,
    loop: fn (msg: dynamic.Dynamic, state: Nil) {
      handle_msg(msg)
      actor.continue(state)
    },
  ))
}

fn init_actor(name: String) -> actor.InitResult(Nil, dynamic.Dynamic) {
  let subject = process.new_subject()

  let trap_proxy = fn(exit_msg: process.ExitMessage) -> dynamic.Dynamic {
    start_proxy(name)

    case exit_msg.reason {
      process.Abnormal(reason) -> {
        reason
        |> string.drop_left(10)  // cuts left from `Abnormal("MSG")`, where we need `MSG`
        |> string.drop_right(2)  // cuts right from `Abnormal("MSG")`, where we need `MSG`
        |> dynamic.from()
      }
      result -> dynamic.from(result)
    }
  }

  let selector = process.new_selector()
  |> process.selecting_trapped_exits(trap_proxy)
  |> process.selecting(subject, function.identity)

  process.trap_exits(True)
  start_proxy(name)

  actor.Ready(Nil, selector)
}

/// Starts a named process, which upon exiting sends the message to our actor.
fn start_proxy(name: String) -> Nil {
  let _ = process.start(process.sleep_forever, True)
  |> process.register(atom(name))
  Nil
}

/// Sends a message to the named actor (by killing it... that's rough).
/// Sending is done in a loop by a new process.
/// Loop does not end until succeeded.
/// Each iteration has a `sleep()` inside.
pub fn send(name: String, msg: String) -> Nil {
  process.start(fn() { send__loop(name, msg) }, True)
  Nil
}

fn send__loop(name: String, msg: String) -> Nil {
  case process.named(atom(name)) {
    Ok(pid) -> {
      case process.is_alive(pid) {
        True -> process.send_abnormal_exit(pid, msg)
        False -> {
          process.sleep(send_timeout)
          send__loop(name, msg)
        }
      }
    }
    _ -> {
      process.sleep(send_timeout)
      send__loop(name, msg)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Do let me know, if you have some other way of doing this!

Bye!

Top comments (0)