The idea of a type system checking messages sent and received between processes, has bounced around the erlang(Elixir) community for a while. With Gleam I think we now have an opportunity to explore it.
I have written a module to work with processes in Gleam Below is the section on how to start processes as well as send and receive messages. I’m really interest in feedback particularly in the following areas.
- Is the API ergonomic/clear/easy to use?
- I’m curious how this API’s might be extended to the multi-node case? though I think there is value even without answering that question
- This library works at a lower level that GenServer, (I’m not yet trying to solve those rather build a solid abstraction at this level). Is it useful to work at the spawn/send level or do you think Gleam processes will only be useful once the abstraction level of Gen* has been reached?
- I think Monitors/Link/Supervisors are a second question and one that can be discussed separately in a later forum post
Gleam Processes
You will need these imports for the examples to work
import process/process
import process/process.{Normal, Infinity, From}
Starting and stopping processes
A process is started using process.spawn_link/1
.
The single argument is a function that accepts a receive call and returns an ExitReason
.
This is an example of the simplest process that immediately exits with reason Normal
let pid = process.spawn_link(fn(_receive) {
Normal
})
Sending and receiving messages
Here is a simple process that sums all the numbers sent to it.
fn loop(receive, state) {
let value = receive(Infinity)
loop(receive, state + value)
}
let pid = process.spawn_link(loop(_, 0))
process.send(pid, 1)
process.send(pid, 2)
By recursively calling loop this function runs forever.
The type of the receive function is parameters by the type of messages it accepts.
Because gleam infers the type of value to be an Int it is known that only integers can be sent to this pid.
The following call will not be allowed by the type checker.
process.send(pid, "0")
Normally a process will accept more message types than an Int,
This is handled by defining a single type with a constructor for each expected message type.
Calling a process
Send a message to a process an await a reply.
type Messages {
Reverse(From(String), String)
}
fn loop(receive) {
let Reverse(from, str) = receive(Infinity)
process.reply(from, string.reverse(str))
loop(receive)
}
let pid = process.spawn_link(loop(_))
let Ok(reversed) = process.call(pid, Reverse(_, "hello"))
The Reverse
message type includes a from reference that says callers accept a String
type as a response.
Receiving this message works the same way as before, but now we have a from value that we can send a reply to.
Sending a call message uses process.call/2
.
The first argument is a Pid.
The second argument is a function that takes a from reference and creates a message.
Having a constructor for the sent message allows the process library to create a from value.
Doing this allows the calling process to receive replies without having to specify them as part of their message contract.
Again Gleam ensures we can’t send a message that isn’t understood and we can’t reply with a type that isn’t expected.