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
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()
}
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)
}
}
}
Do let me know, if you have some other way of doing this!
Bye!
Top comments (0)