Skip to content

Search content

From F# to Gleam > 8: From MailboxProcessor to BEAM Processes

3 min readFrom F# to GleamPart 8
Linked parent and counter processes exchange an Add message and a reply while the counter owns its state.

F#‘s MailboxProcessor is a useful bridge to Gleam on Erlang. Both can give one message loop ownership of a value, rather than letting several callers mutate it directly.

Table of contents

On this page

A Counter with an F# Mailbox

Here’s a counter that handles additions, reads, and a stop message:

type Message =
    | Add of int
    | Read of AsyncReplyChannel<int>
    | Stop

let counter =
    MailboxProcessor.Start(fun inbox ->
        let rec loop total = async {
            let! message = inbox.Receive()
            match message with
            | Add amount -> return! loop (total + amount)
            | Read reply ->
                reply.Reply total
                return! loop total
            | Stop -> return ()
        }
        loop 0)

counter.Post(Add 3)
let total = counter.PostAndReply(Read, timeout = 1000)
counter.Post Stop
assert (total = 3)scratch.fsx

The state is an argument to loop. Handling Add recurs with a new total. No shared mutable counter is needed.

A Subject Has an Owner

Add the Erlang support package to the project from part 1:

gleam add gleam_erlang

A Subject(message) is a typed address for messages. Any process can send to it, but only its owning process can receive from it. Create the counter’s inbox inside the child process, then send that inbox back to the parent through a separate startup subject.

This handshake is important. Creating an inbox in the parent and handing it to a child doesn’t transfer its ownership.

Translate the Message Loop

Replace src/learning_gleam.gleam with these Gleam blocks in order:

import gleam/erlang/process

pub type Message {
  Add(Int)
  Read(reply_to: process.Subject(Int))
  Stop
}

fn counter(inbox: process.Subject(Message), total: Int) -> Nil {
  case process.receive_forever(inbox) {
    Add(amount) -> counter(inbox, total + amount)
    Read(reply_to) -> {
      process.send(reply_to, total)
      counter(inbox, total)
    }
    Stop -> Nil
  }
}src/learning_gleam.gleam

receive_forever waits for the next message. The recursive calls are in tail position, so handling messages doesn’t keep growing the stack. The reply subject plays the part of F#‘s AsyncReplyChannel.

Try It

The entry point starts the process and checks one conversation:

pub fn main() -> Nil {
  let ready = process.new_subject()

  let _pid =
    process.spawn(fn() {
      let inbox = process.new_subject()
      process.send(ready, inbox)
      counter(inbox, 0)
    })

  let assert Ok(inbox) = process.receive(ready, within: 1000)
  let reply = process.new_subject()

  process.send(inbox, Add(3))
  process.send(inbox, Read(reply))

  let total = process.receive(reply, within: 1000)
  process.send(inbox, Stop)

  assert total == Ok(3)
}src/learning_gleam.gleam

Run gleam run. Then send another Add(4) before Read and change the expected result to Ok(7).

Messages sent from this one parent to the counter preserve their send order. Don’t infer a total ordering between unrelated senders. Also, send returning doesn’t acknowledge that the counter has processed the message. The reply does that for this read.

Failure Is Part of the Protocol

receive(..., within: 1000) waits up to 1,000 milliseconds and returns Error(Nil) on timeout. A timeout doesn’t cancel the work or prove it wasn’t performed. Retrying a state-changing request blindly can apply it twice.

The assertions above deliberately fail this short-lived example if startup or the reply fails. A service should handle those outcomes explicitly, monitor process lifetime, and define how clients recover. A subject isn’t durable storage, and the counter disappears when its process exits.

process.spawn creates a linked process. An abnormal exit normally propagates over that link, but a link doesn’t restart anything. For long-running services, use the actor and supervision APIs in gleam_otp rather than extending this raw loop into a home-made framework.

Further reading: gleam/erlang/process and gleam_otp.



Previous in series
From F# to Gleam > 7: Modules, Packages, and Tests
Next in series
From F# to Gleam > 9: Erlang, JavaScript, and Foreign Functions