Current section
Files
Jump to
Current section
Files
src/process/process.gleam
//// Working with typed processes in Gleam.
////
//// This module allows you to:
//// - spawn processes
//// - send and receive messages
//// - call processes, send a message and expect a reply.
//// - monitor processes
//// - link to processes
//// - supervise processes with the `set_supervisor` and `rest_for_one`
////
//// *This module is being developed concurrently with the Midas web framework.
//// It will be extracted before Midas reaches 1.0.
//// Help with this module is greatly appreciated.*
////
//// 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.
import gleam/result.{Option}
/// A value in milliseconds or infinity.
pub type Wait {
Infinity
Milliseconds(Int)
}
// WORKING WITH PROCESSES
/// Identifier for a process
/// This type is parameterised by the message type acceptable to that process.
pub external type Pid(m)
/// Reason for a process to terminate.
///
/// In most cases `Normal` is the correct value to return from a process's run function.
pub type ExitReason {
Normal
Kill
Killed
}
// https://erlang.org/doc/reference_manual/errors.html#exit_reasons
// Other types e.g. BadArith(Stack) can all be enumerated here.
// receive can only fail due to timeout so option is an acceptable response type
type Receive(m) =
fn(Wait) -> Option(m)
type Run(m) =
fn(Receive(m)) -> ExitReason
pub external fn spawn_link(Run(m)) -> Pid(m) =
"process_native" "spawn_link"
pub external fn send(Pid(m), m) -> m =
"erlang" "send"
pub external fn unsafe_self() -> Pid(m) =
"erlang" "self"
pub fn self(_: Receive(m)) -> Pid(m) {
unsafe_self()
}
// Start a run function with a reference to the type of error messages that the parent process can accept.
// fn (run: fn() -> a, wrap_exit: fn(a) -> Message(parent)) Needs to be in process library that the specific message mapped to will be exit.
// This can be typed because an untyped pid is only the result of a DOWN or EXIT Message
// TODO make exit reason Killed and have a special fn for kill
pub external fn exit(Pid(m), ExitReason) -> Bool =
"erlang" "exit"
// Add a clause that turned Killed into Kill
// Or just StopReason
// type Kill {
// Kill
// }
//
// external fn kill(Pid(m), Kill) -> Bool
// = "erlang" "exit"
//
// pub fn exit(pid, reason) {
// case reason {
// Killed -> kill(pid, Kill)
// _ -> do_exit(pid, reason)
// }
// }
// The return value of a run function should just be the exit reason of a process.
// Then a Down message can just be treated as a promise for the result of the computation
// erlang:is_process_alive
// WORKING WITH UNTYPED PROCESSES
pub external type BarePid
pub external fn bare(Pid(a)) -> BarePid =
"process_native" "identity"
// WORKING WITH MONITORS AND REFERENCES
pub external type Ref
pub type MonitorType {
Process
}
pub external fn monitor(MonitorType, Pid(m)) -> Ref =
"erlang" "monitor"
pub fn monitor_process(pid) {
monitor(Process, pid)
}
pub type DemonitorOptions {
Flush
}
pub external fn demonitor(Ref, List(DemonitorOptions)) -> Bool =
"erlang" "demonitor"
// PROCESS FLAGS, WARNING:
// Can change the messages expected in a receive function
pub type ProcessFlag {
TrapExit(Bool)
}
pub external fn process_flag(ProcessFlag) -> ProcessFlag =
"process_native" "process_flag"
// CALL PROCESS
pub type From(r) {
From(Ref, Pid(tuple(Ref, r)))
}
// Can check pid is self
// Need error because process could have terminated
// needs to be separate receive fn because we want to ignore exits and motiors from other pids
// Might be more expressive to "Gone/Slow", I can't have a separate Type that includes a Timout Branch
pub type CallError {
Timeout
// Slow
Gone
}
// Gone(ExitReason) To not conflict with Down types
pub external fn receive_reply(Ref, Wait) -> Result(r, CallError) =
"process_native" "receive_reply"
pub fn call(
pid: Pid(m),
constructor: fn(From(r)) -> m,
wait: Wait,
) -> Result(r, CallError) {
let reference = monitor_process(pid)
let from = From(reference, unsafe_self())
let _message = send(pid, constructor(from))
receive_reply(reference, wait)
}
pub fn reply(from: From(r), message: r) {
let From(reference, pid) = from
send(pid, tuple(reference, message))
}
// Proc lib needs a start and init_ack function
// Will need an external receive ack
pub type OK {
OK(OK)
}
pub external fn do_sleep(Int) -> OK =
"timer" "sleep"