SagaSaga
Guide

Refs

Saga values are immutable by default. When you need controlled mutable state, the Ref effect provides typed mutable cells with swappable storage backends. Because mutation is an effect, the type system tracks it and you choose the implementation at the handler level.

The Ref Effect

import Std.Ref (MutRef, Ref, beam_ref)

fun count_evens : List Int -> Int needs {Ref}
count_evens xs = {
  let counter = new! 0
  List.iter (fun x ->
    if x % 2 == 0 then {
      let _ = modify! counter (fun n -> n + 1)
      ()
    } else ()
  ) xs
  get! counter
}

main () = {
  let result = count_evens [1, 2, 3, 4, 5, 6] with beam_ref
  dbg $"evens: {result}"
}

The four operations are:

OperationTypeDescription
new!a -> MutRef aCreate a new ref with an initial value
get!MutRef a -> aRead the current value
set!MutRef a -> a -> UnitOverwrite the value
modify!MutRef a -> (a -> a) -> aRead, apply a function, write back, return the new value

Handlers

Two handlers ship with the standard library:

beam_ref is backed by the process dictionary. Fast, requires no setup, and scoped to the current process. Refs are cleaned up automatically when the process exits. This is the default choice for single-process state.

ets_ref is backed by an ETS table. Refs are stored in shared memory, accessible across processes. Use this when multiple actors need to read or write the same ref.

import Std.Ref (beam_ref, ets_ref)

# Process-local, fast
run_app () with beam_ref

# Shared across processes
run_app () with ets_ref

Same code, different handler, different storage backend.

Example: Memoized Fibonacci

A ref holding a Dict makes a simple memoization cache:

import Std.Ref (MutRef, Ref, beam_ref)

fun fib_memo : MutRef (Dict Int Int) -> Int -> Int needs {Ref}
fib_memo cache n = {
  let memo = get! cache
  case Dict.get n memo {
    Just result -> result
    Nothing -> {
      let result =
        if n <= 1 then n
        else fib_memo cache (n - 1) + fib_memo cache (n - 2)
      let _ = modify! cache (fun m -> Dict.put n result m)
      result
    }
  }
}

fun fibonacci : Int -> Int needs {Ref}
fibonacci n = {
  let cache = new! (Dict.new () : Dict Int Int)
  fib_memo cache n
}

main () = {
  let result = fibonacci 30 with beam_ref
  dbg $"fib(30) = {result}"
}

Shared Refs Across Processes

With ets_ref, multiple actors can access the same ref. Here, worker processes each increment a shared counter:

import Std.Actor (Process, Actor, beam_actor)
import Std.Ref (MutRef, Ref, ets_ref)

fun worker : MutRef Int -> Int -> Pid Done -> Unit needs {Ref, Process}
worker counter n parent = {
  worker_loop counter n
  send! parent Done
}

fun worker_loop : MutRef Int -> Int -> Unit needs {Ref}
worker_loop _ 0 = ()
worker_loop counter n = {
  let _ = modify! counter (fun x -> x + 1)
  worker_loop counter (n - 1)
}

main () = {
  let counter = new! 0
  let me = self! ()

  let _ = spawn! (fun () -> worker counter 100 me)
  receive { Done -> () }

  let _ = spawn! (fun () -> worker counter 100 me)
  receive { Done -> () }

  let total = get! counter
  dbg $"total: {total}"
} with {ets_ref, beam_actor}

Note that ets_ref does not provide atomicity. For concurrent read-modify-write operations that need mutual exclusion, use AtomicRef from Std.AtomicRef, which adds a lock server for safe cross-process mutation.

When to Use Refs vs Recursion

Functional Saga code typically threads state through recursive function calls:

fun loop : Int -> Unit needs {Actor Msg}
loop count = receive {
  Increment -> loop (count + 1)
  Get caller -> {
    send! caller count
    loop count
  }
}

This is the idiomatic approach for actor state, and you should prefer it when the state naturally flows through a recursive loop.

Refs are useful when:

  • You need a mutable accumulator inside an otherwise non-recursive function (like count_evens above)
  • Multiple parts of a computation need to read and write the same cell without passing it through every function call
  • You need a cache (like the memoization example)
  • Multiple processes need shared mutable state (with ets_ref or atomic_ref)