Processprelude
type Pid
Processes, tasks and external commands.
Kex's concurrency is the BEAM's: lightweight processes that share nothing and communicate by message. There are three things here, and they answer different questions.
Task runs one piece of work somewhere else and gives you the answer back:
let a = Task.start do expensiveThing(1) end
let b = Task.start do expensiveThing(2) end
a.await ` b.await
serving plus Process.spawn gives a piece of state its own process, with typed calls into it:
record RateLimiter do
remaining : Integer
end
serving RateLimiter do
slot allowed? -> Reply<Bool> do
let allowed = @remaining > 0
new.remaining = allowed then @remaining - 1 else @remaining
return { new, reply: allowed }
end
end
let api = Process.spawn(RateLimiter { remaining: 2 })
api.allowed?() # => Ok(true)
Process.run and Process.stream run an external program.
Process.run("git", ["rev-parse", "HEAD"])
Backed by Kex.Intrinsic.Process and the BEAM runtime.
An opaque BEAM process identifier.
Obtained from `Process.self` or `Process.whereis+. Send it messages, link to it, monitor it, or ask whether it is still alive.
type Task<A>
A handle on work running in another process, with a result you can await.
Created by Task.start.
type Reference
A monitor reference, returned by monitor and passed back to demonitor.
type Process<A>
A typed process handle: like a Pid, but it remembers what kind of message the process accepts.
type ProcessExitReason
A process termination reason. This remains open because the BEAM permits any term as an exit reason; conventional values include :normal, :shutdown, and structured application errors.
Variants
Any
record Reply<A>
What a synchronous slot returns: an answer for the caller, and optionally a new state and a reason to stop.
reply answers the caller immediately. A slot may omit it only when it sends a deferred response with from.reply(...). new installs the next serving state; omitting it preserves the current state. stop terminates the server after applying the transition. Within a serving X block, the checker narrows new from Any? to X?.
slot allowed? -> Reply<Bool> do
let allowed = @remaining > 0
new.remaining = allowed then @remaining - 1 else @remaining
return { new, reply: allowed }
end
slot stop -> Reply<Integer> = { stop: :normal, reply: @remaining }
Fields
replyAnewAny?optionalstopProcessExitReason?optional
type From<A>
The identity of a pending caller, for a slot that answers later rather than immediately. Hand it reply when the answer is ready.
type CallError
Why a call into a server failed: it took too long, the process is gone, or it crashed handling the call.
Variants
TimeoutNoProcessCallFailed
record Server<X>
A running server and the default timeout for calls into it.
Returned by Process.spawn. Every slot declared in the type's serving block becomes a method on it, answering a Result.
Fields
processProcess<X>timeoutIntegeroptional
module Process
Spawning servers, running external commands, and the ambient process operations.
function spawn
Starts a process running the serving implementation attached to state's type, and returns a handle on it.
The state you pass is the server's initial state. Every slot in the serving block becomes a method on the returned Server, and each answers a Result: a call can time out or find the process gone.
spawn(state) : X -> Server<X>
Parameters
stateX- the server's initial state
Returns: Server<X> — a handle on the running server
Examples
let api = Process.spawn(RateLimiter { remaining: 2 })
api.allowed?() # => Ok(true)
api.allowed?() # => Ok(true)
api.allowed?() # => Ok(false)
api.stop()
function run
Runs an executable with an argument vector and captures its output.
No shell is involved, so nothing is glob-expanded or word-split and arguments containing spaces need no quoting. Output is captured as UTF-8 strings.
A non-zero exit status is still Ok: the program ran and said something, which is information, not a failure to run it. Error means the child could not be started at all.
run(command, args) : String -> [String] -> Result<ProcessResult, String>
Parameters
commandString- the executable to run
args[String]- its arguments, one per element
Returns: Result<ProcessResult, String> — the captured result, or why it could not start
Examples
match Process.run("echo", ["hello"]) do
Ok(r) => IO.printLine(r.stdout.trim) # prints: hello
Error(e) => IO.printError(e)
end
A non-zero status is still Ok
Process.run("false", []) # => Ok(ProcessResult { exitCode: 1, ... })
A command that does not exist is an Error
Process.run("no-such-command", []) # => Error("executable not found")
Reading a command's output as data
Process.run("git", ["rev-parse", "HEAD"])
.map { |r| r.stdout.trim }
.or("unknown")
function stream
Runs an executable with the CALLER's stdout and stderr, so its output appears as it is produced rather than in one block when it exits.
Answers the exit code; nothing is captured: that is the trade, and it is what a long-running child a person is watching needs (kexhq/kex#187).
run remains the one to use when the output is data to be READ.
stream(command, args) : String -> [String] -> Result<Integer, String>
Parameters
commandString- the executable to run
args[String]- its arguments, one per element
Returns: Result<Integer, String> — the exit code, or why it could not start
Examples
Watching a build as it runs
match Process.stream("make", ["-j8"]) do
Ok(0) => IO.printLine("build succeeded")
Ok(code) => IO.printError("build failed with ${code}")
Error(e) => IO.printError(e)
end
function exec
Runs an executable and returns just its exit code, discarding its output.
A command that could not be started answers 127, the shell's convention for "command not found". Use run when you need the output or want to tell a failed start from a failed run.
exec(command, args) : String -> [String] -> Integer
Parameters
commandString- the executable to run
args[String]- its arguments, one per element
Returns: Integer — the exit code, or 127 if it could not start
Examples
Process.exec("true", []) # => 0
Process.exec("false", []) # => 1
Process.exec("no-such-command", []) # => 127
Testing for a tool's presence
let haveGit = Process.exec("git", ["--version"]) == 0
function self
Returns the calling process's own Pid.
self() : Pid
Returns: Pid — this process's identifier
Examples
Telling another process where to answer
worker.send((Process.self, :ping))
function exit
Sends an exit signal carrying reason to pid.
:normal is the ordinary shutdown reason; :kill cannot be trapped.
exit(pid, reason) : Pid -> X -> Void
Parameters
pidPid- the process to signal
reasonX- the exit reason
Returns: Void —
Examples
Process.exit(worker, :shutdown)
function register
Registers pid under the atom name, so it can be found by name rather than by passing the Pid around.
register(pid, name) : Pid -> Atom -> Void
Parameters
pidPid- the process to register
nameAtom- the name to register it under
Returns: Void —
Examples
Process.register(Process.self, :main)
function whereis
Returns the Pid registered under name, or None when nothing is.
whereis(name) : Atom -> Pid?
Parameters
nameAtom- the registered name
Returns: Pid? — the process, or None
Examples
Process.whereis(:main) # => Just(pid)
Process.whereis(:not_there) # => None
Sending to a named process if it is there
Process.whereis(:logger).map { |pid| pid.send(message) }
record ProcessResult
What an external command left behind: its exit status and its output.
Returned inside Ok by Process.run, whatever the exit status.
Fields
exitCodeIntegerstdoutStringstderrString
make Pid
send
Sends msg to the process, unchanged, as a raw BEAM term.
Sending never blocks and never fails, even when the process is gone: that is the BEAM's model, not an oversight. Monitor the process when delivery matters.
send(msg) : X -> Void
Parameters
msgX- the message to send
Returns: Void —
Examples
worker.send(:stop)
worker.send(("job", 42))
sendFrom
Sends the conventional Erlang sender-bearing pair {Process.self, msg}, so the receiver knows where to answer.
sendFrom(msg) : X -> Void
Parameters
msgX- the message to send
Returns: Void —
Examples
server.sendFrom(:status) # the server receives (senderPid, :status)
make Process<X>
A spawned Process<X> is a typed process handle backed by the same runtime pid. It therefore supports the ordinary pid lifecycle operations without erasing its message type.
send
Sends msg to the process. Unlike Pid.send, the message type is checked.
send(msg) : X -> Void
Parameters
msgX- the message to send
Returns: Void —
sendFrom
Sends the sender-bearing pair {Process.self, msg}, with the message type checked.
sendFrom(msg) : X -> Void
Parameters
msgX- the message to send
Returns: Void —
make Server<X>
within
Returns the same server with a different default call timeout, in milliseconds.
The server is untouched: this is a new view of it, so one slow call can be given more room without changing anything for other callers.
within(timeout) : Integer -> Server<X>
Parameters
timeoutInteger- the call timeout in milliseconds
Returns: Server<X> — a view of the server with that timeout
Examples
Giving one call longer to answer
api.within(30000).rebuildIndex()
make From<X>
reply
Answers the pending call this value identifies.
A slot that cannot answer immediately, because it is waiting on something else: omits reply from its transition and calls this later instead.
reply(value) : X -> Void
Parameters
valueX- the answer to send
Returns: Void —
Examples
Answering after the work is done
Task.start do
from.reply(expensiveThing())
end
module Task
Running work in another process and collecting the answer.
function sleep
Suspends for an elapsed duration. Negative durations are treated as zero.
sleep(duration) : Duration -> Void
function start
Runs f in a new process and returns a handle on its result.
The block starts immediately, so starting several tasks and awaiting them afterwards is what makes them run at the same time.
start(f) : Block<X> -> Task
Parameters
fBlock<X>- the work to run
Returns: Task — a handle on the result
Examples
let t = Task.start do 1 + 1 end
t.await # => 2
Two pieces of work at once
let a = Task.start do slowThing("a") end
let b = Task.start do slowThing("b") end
(a.await, b.await)
function awaitAll
Waits for every task in tasks and returns their results, in the order the tasks were given.
Each result comes back wrapped in a Result, so one task failing does not cost you the others' answers. That differs from Task.await on a single task, which hands back the value itself.
awaitAll(tasks) : [Task] -> [X]
Parameters
tasks[Task]- the tasks to wait for
Returns: [X] — their results, in order
Examples
let tasks = [Task.start do 1 end, Task.start do 2 end]
Task.awaitAll(tasks) # => [Ok(1), Ok(2)]
Fanning work out over a list
let results = Task.awaitAll(paths.map { |p| Task.start do process(p) end })
make Task
await
Waits for the task's result, giving up after timeout milliseconds.
Answers None if the task has not finished in time. The task itself is not stopped.
await(timeout) : Integer -> X
Parameters
timeoutInteger- how long to wait, in milliseconds
Returns: X — the task's result, or None on timeout
Examples
let t = Task.start do slowThing() end
t.await(1000)
function worker
Wraps a spawn block into a worker spec for Supervisor.start.
worker : Block<Pid> -> (Atom, Block<Pid>)
Parameters
blockBlock<Pid>- the block that spawns the worker
Returns: (Atom, Block<Pid>) — the worker spec