# Saga Language Reference Saga is a statically typed functional language with algebraic effects and handlers, compiling to the BEAM (Erlang VM). This file concatenates the language guide and worked examples for use as LLM context. Source: https://saga-lang.org Generated: 2026-06-07 # Guide ## Language Tour *A quick overview of everything Saga offers, in one page.* Saga is a statically typed functional language that compiles to the BEAM. It has full type inference, pattern matching, and an effect system that gives you dependency injection, error handling, and testability as language features rather than library conventions. ### Functions and Types Functions are defined with optional type annotations. The compiler infers types when you leave them off: ```saga pub fun add : Int -> Int -> Int add x y = x + y double x = x * 2 # inferred as Int -> Int ``` Types are algebraic data types and records: ```saga type Shape = | Circle Float | Rect Float Float record User { name: String, age: Int, } ``` ### Pattern Matching `case` expressions are exhaustive. The compiler ensures you handle every possibility: ```saga area shape = case shape { Circle r -> 3.14159 * r * r Rect w h -> w * h } ``` Patterns also work in function definitions: ```saga fib 0 = 0 fib 1 = 1 fib n = fib (n - 1) + fib (n - 2) ``` ### Pipes The pipe operator threads values through a chain of functions, left to right: ```saga [1, 2, 3, 4, 5] |> List.filter (fun x -> x % 2 == 0) |> List.map (fun x -> x * 10) ``` ### Traits Traits define shared behavior across types, similar to interfaces: ```saga trait Show a { fun show : a -> String } impl Show for User { show user = $"{user.name} (age {user.age})" } ``` The compiler can derive common traits automatically: ```saga record Point { x: Int, y: Int } deriving (Show, Eq, Ord) ``` ### Effects and Handlers Effects are how Saga handles operations like I/O, errors, and database access. Think of them as swappable interfaces whose implementation is chosen by the caller: ```saga effect Log { fun log : String -> Unit } fun process : String -> Unit needs {Log} process input = log! $"processing: {input}" ``` A handler provides the implementation. The same code can run with different handlers: ```saga handler console for Log needs {Stdio} { log msg = { print! msg resume () } } handler silent for Log { log _ = resume () } # Production process "data" with console # Tests process "data" with silent ``` ### Error Handling No exceptions. The `Fail` effect handles recoverable errors, and handlers decide what happens: ```saga fun parse_age : String -> Int needs {Fail} parse_age s = case String.to_int s { Ok n -> n Err _ -> fail! "not a number" } # Convert to Result parse_age "25" with to_result # Ok 25 parse_age "abc" with to_result # Err "not a number" ``` ### Modules Files declare a module name and control visibility with `pub`: ```saga module Math pub fun abs : Int -> Int abs n when n < 0 = -n abs n = n ``` ```saga import Math Math.abs (-5) # 5 ``` ### Testing Tests are ordinary modules that export a `tests` function. Handler swapping gives you isolation with no mocking libraries: ```saga import Std.Test (Testing, test, assert_eq) pub fun tests : Unit -> Unit needs {Testing} tests () = { test "get_users returns results" (fun () -> { let result = { get_users () } with { query _ = resume ["alice", "bob"], } assert_eq result ["alice", "bob"] }) } ``` ### Concurrency Saga runs on the BEAM, so you get lightweight processes and message passing: ```saga import Std.Actor (Process, Actor, beam_actor) fun greeter : Unit -> Unit needs {Actor Msg} greeter () = receive { Hello name -> { dbg $"Hello, {name}!"; greeter () } Goodbye -> dbg "Bye!" } main () = { let pid = spawn! (fun () -> greeter ()) send! pid (Hello "Alice") send! pid Goodbye } with beam_actor ``` ### Why Does This Matter? Features are nice, but what do they buy you in practice? #### Testing without mocks A function that queries a database is testable with no mock library, no dependency injection container, and no patching imports. Just provide a different handler: ```saga pub fun get_users : Unit -> List String needs {Database} get_users () = query! "SELECT name FROM users" test "returns users" (fun () -> { let result = { get_users () } with { query _ = resume ["alice", "bob"], } assert_eq result ["alice", "bob"] }) ``` The function under test is unchanged. The test provides an inline handler that returns canned data. That's the entire setup. #### Swapping implementations Dev uses SQLite, production uses Postgres. Same application code, one choice at the entry point: ```saga main () = { let db = if env == "dev" then sqlite else postgres run_app () } with { db, console } ``` No interface hierarchies, no factory patterns, no configuration frameworks. #### Errors that propagate cleanly `fail!` propagates through any number of function calls without manual forwarding. One handler at the boundary catches everything: ```saga fun parse : String -> Config needs {Fail} fun validate : Config -> Config needs {Fail} fun load : Config -> App needs {Fail} fun start : String -> Result App String start input = { input |> parse |> validate |> load } with to_result ``` Three functions that can each fail. No nested `case` expressions, no error codes, no try/catch. If any step calls `fail!`, the handler at the top produces an `Err`. ### Next Steps The rest of the guide covers each feature in depth, starting with [Basics](/guide/basics). ## Basics *Bindings, primitive types, operators, blocks, strings, and pipes.* Saga's syntax draws from ML-family languages like Haskell, Elm, and OCaml, with curly braces for blocks. If you've used any of those, or even TypeScript, much of this will feel familiar. ### Comments Line comments start with `#`. Doc comments use `#@` and attach to the next definition. ```saga # This is a comment #@ This documents the function below pub fun add : Int -> Int -> Int add x y = x + y ``` ### Let Bindings Inside a function or block, `let` binds a name to a value: ```saga main () = { let x = 42 let name = "Saga" dbg name } ``` Bindings are immutable and cannot be modified once defined, however it is possible to shadow and reuse the binding: ```saga let x = 5 x = 6 # error! let x = 6 # ok ``` For situations where mutability is truly needed (e.g. for performance), it is possible to use the [Ref Effect](/guide/refs) with a BEAM handler to access the mutable process dictionary or ETS table. `let` can also destructure patterns inline: ```saga let (x, y) = compute_pair () let Point { x, y } = get_point () ``` Pattern destructuring is covered in depth in [Pattern Matching](/guide/pattern-matching). ### Primitive Types Saga has five primitive types: | Type | Examples | Notes | | -------- | --------------- | --------------------------- | | `Int` | `0`, `42`, `-7` | Arbitrary precision integer | | `Float` | `3.14`, `-0.5` | 64-bit IEEE 754 | | `String` | `"hello"`, `""` | UTF-8 | | `Bool` | `True`, `False` | | | `Unit` | `()` | The "nothing useful" type | `Unit` is a special "nothing" type, used with the constructor `()`. For functions with no inputs or that return no data, the Unit type can be used. Other types, such as lists, mutable vectors, and bitstrings can be found in the standard library. ### Operators #### Arithmetic ```saga 1 + 2 # 3 10 - 3 # 7 4 * 5 # 20 7 / 2 # 3 (integer division truncates) 7 % 2 # 1 (modulo, Int only) ``` For floats, division works as expected: `7.0 / 2.0` is `3.5`. #### Comparison ```saga 1 == 1 # True 1 != 2 # True 3 < 5 # True 3 > 5 # False 4 <= 4 # True 4 >= 5 # False ``` #### Logic ```saga True && False # False True || False # True ``` #### Negation Negative literals require parentheses when passed as function arguments: ```saga abs (-5) # fine abs -5 # parse error: looks like binary minus -x # fine as a standalone expression ``` ### Strings Concatenate strings with `<>`: ```saga "Hello, " <> "world" # "Hello, world" ``` #### Interpolation Prefix a string with `$` to enable interpolation. Use `{expr}` for holes: ```saga let name = "Dylan" $"Hello, {name}!" # "Hello, Dylan!" $"Items: {xs |> List.length}" # pipes work inside holes ``` Any expression works inside a hole. Non-string values are converted automatically via the `show` function: ```saga let count = 42 let ratio = 0.75 $"count: {count}, ratio: {ratio}" # "count: 42, ratio: 0.75" ``` To include a literal brace, escape it: ```saga $"Show \{ literal brace" ``` #### Multiline strings Triple-quoted strings allow literal newlines. Indentation is stripped based on the column of the closing `"""`: ```saga let sql = """ SELECT * FROM users WHERE age > 30 """ # "SELECT *\nFROM users\nWHERE age > 30" ``` Interpolation works in multiline strings too with `$"""..."""`. #### Raw strings Prefix with `@` to disable escape processing: ```saga @"hello\nworld" # literal backslash-n, not a newline ``` ### Blocks A block is a sequence of expressions inside `{ }`. The value of the block is its last expression: ```saga compute x = { let a = x * 2 let b = a + 1 b # this is the return value } ``` Blocks appear in function bodies, `if` branches, `case` arms, and handler bodies. Anywhere you need to sequence multiple steps before producing a value, use a block. ### If / Then / Else `if` is an expression, not a statement. Both branches must return the same type: ```saga abs n = if n < 0 then -n else n ``` For multi-step branches, use a block: ```saga describe n = if n > 0 then { let label = "positive" label <> ": " <> show n } else { "non-positive" } ``` ### Top-Level Definitions Module-level named values are just functions with no arguments. Unlike `let`, which lives inside a function body, these are declared at the top level of a module: ```saga pi = 3.14159 app_name = "my-app" max_retries = 5 origins = ["localhost", "example.com"] ``` A name like `pi` is a zero-arity function: referencing it evaluates its body. For a plain constant that's exactly what you want. But because every reference re-runs the body, a zero-arity definition whose body is expensive or performs effects runs that work *on every reference* — see [Functions](/guide/functions#zero-arity-functions) for when to reach for a `let` binding or a thunk instead. To export a top-level definition, mark it `pub`. As with any public function, a `pub` definition needs a type annotation: ```saga pub fun version : String version = "1.0.0" ``` ## Functions *Definitions, annotations, pub/private, lambdas, currying, and composition.* Functions are the primary building block in Saga. Everything is a function or a value computed from one. ### Defining Functions A function definition has two parts: an optional type annotation, and one or more equations that provide the implementation. ```saga # Annotated (required for pub) pub fun add : Int -> Int -> Int add x y = x + y # Private with annotation (optional) fun double : Int -> Int double x = x * 2 # Private, fully inferred triple x = x * 3 ``` The annotation and the equations are separate declarations. The annotation describes the type; the equation gives the body. For public functions the annotation is required, both to enforce a stable API and as documentation for anyone reading the code. ### Calling Functions: Currying and Partial Application If you're coming from most other languages, Saga's call syntax will look unusual at first. There are no parentheses around argument lists and no commas between arguments: ```saga add 1 2 # not add(1, 2) clamp n 0 100 # not clamp(n, 0, 100) ``` This is because all functions in Saga are _curried_. A function that takes two arguments is actually a function that takes one argument and returns another function that takes the second. `add 1 2` means "apply `add` to `1`, getting back a function, then apply that to `2`." The practical payoff is partial application: you can apply fewer arguments than a function expects, and get a new function back: ```saga pub fun add : Int -> Int -> Int add x y = x + y increment = add 1 # type: Int -> Int increment 3 # 4 ``` This composes naturally with higher-order functions. Rather than writing a lambda, partially apply: ```saga # These are equivalent List.map (fun x -> x + 1) [1, 2, 3] List.map (add 1) [1, 2, 3] ``` One thing to watch: parentheses are still used to group expressions, just not for argument lists. So `f (g x)` means "apply `f` to the result of `g x`", not a comma-separated argument list. ### Multiple Equations and Guards A function can have multiple equations, matched top to bottom: ```saga fun fib : Int -> Int fib 0 = 0 fib 1 = 1 fib n = fib (n - 1) + fib (n - 2) ``` Guards add a condition to an equation using `when`. The first equation whose pattern and guard both match is used: ```saga fizzbuzz n when n % 15 == 0 = "FizzBuzz" fizzbuzz n when n % 3 == 0 = "Fizz" fizzbuzz n when n % 5 == 0 = "Buzz" fizzbuzz n = show n ``` The compiler checks that the equations are exhaustive. Patterns and guards are covered in full in [Pattern Matching](/guide/pattern-matching). ### Zero-Arity Functions A function can take no arguments at all. A definition with no parameters is a zero-arity function, and its body runs **every time the name is referenced**: ```saga fun answer : Int answer = 42 config_path = "/etc/app.conf" # inferred, zero-arity ``` This is how module-level constants are written (see [Basics](/guide/basics#top-level-definitions)). For a pure value, "evaluated on every reference" is exactly what you want. But it has teeth when the body is expensive or effectful, because the work happens at each mention rather than once: ```saga # Runs the query EVERY time `all_users` is referenced — probably not intended fun all_users : List User needs {Database} all_users = query! "SELECT * FROM users" ``` When you want the work to happen once, or not until you ask for it, you have two options: - **Bind it with `let`.** The right-hand side is evaluated a single time and the result is bound to a name: ```saga let users = all_users # query runs once, here process users process users # reuses the same list, no second query ``` - **Keep it a thunk.** Give the function a `Unit` parameter (written `()`) so the name is just a value you can pass around, with the body running only when you apply `()`: ```saga fun load_users : Unit -> List User needs {Database} load_users () = query! "SELECT * FROM users" let later = load_users # nothing has run yet let users = load_users () # query runs here ``` A `Unit` parameter is the idiomatic way to delay a computation. It's why callbacks passed to handlers, `async!`, and the test framework all take `Unit -> a`: the wrapper lets them hand a computation around without triggering it until the handler decides to. ### Parameter Labels Type annotations can include parameter labels. These are documentation only and have no effect on how the function is called: ```saga pub fun clamp : (value: Int) -> (min: Int) -> (max: Int) -> Int clamp value min max = ... ``` The label appears in hover tooltips and docs but the call site is still `clamp x 0 100`. ### Lambdas Anonymous functions use `fun`: ```saga fun x -> x + 1 fun x y -> x + y ``` Lambdas are most commonly passed to higher-order functions: ```saga List.map (fun x -> x * 2) [1, 2, 3] List.filter (fun x -> x % 2 == 0) [1, 2, 3, 4] ``` ### Functions as Values Functions are first-class values. You can assign them to `let` bindings and pass them around like any other value: ```saga let double = fun x -> x * 2 let apply = fun f x -> f x apply double 5 # 10 ``` Named functions work the same way -- the name is just a reference to the function value: ```saga let transform = double # transform is now the same function as double transform 4 # 8 ``` This is what makes partial application and higher-order functions work: `add 1` is just a value of type `Int -> Int`, and you can store it, pass it, or call it whenever you like. ### Pipes The pipe operator `|>` threads a value through a chain of functions. The value on the left becomes the last argument to the function on the right. Instead of: ```saga dbg (show (add 1 2)) ``` Write: ```saga add 1 2 |> show |> dbg ``` Pipes read left to right and eliminate deeply nested parentheses. They work especially well with curried functions, since data can flow through a pipeline of partially-applied steps: ```saga [1, 2, 3, 4, 5] |> List.filter (fun x -> x % 2 == 0) |> List.map (fun x -> x * 10) |> List.foldl (fun acc x -> acc + x) 0 |> dbg ``` The backward pipe `<|` binds in the other direction, which can avoid parentheses when the argument is a complex expression: ```saga dbg <| add 1 2 # equivalent to: dbg (add 1 2) ``` ### Function Composition `>>` composes two functions into one. `f >> g` means "apply f, then apply g to the result": ```saga let process = parse >> validate >> save data |> process # same as: data |> parse |> validate |> save ``` Composition is useful for building reusable pipelines from smaller pieces, especially when you want to pass the pipeline itself as a value rather than applying it immediately. ## Types *ADTs, records, tuples, type parameters, opaque types, and dictionaries.* Saga has two main ways to define types: Algebraic Data Types (ADTs) for variants, and records for structured data. Both can be generic, and they compose naturally together. ### Algebraic Data Types An ADT defines a type as one of several variants. Each variant is either bare (no data) or carries one or more values: ```saga type Direction = North | South | East | West type Shape = | Circle Float | Rect Float Float | Point ``` Variant arguments can have optional labels for documentation. Labels have no effect on construction or pattern matching, they just make the definition clearer: ```saga type Shape = | Circle (radius: Float) | Rect (width: Float) (height: Float) | Point ``` Constructing a value uses the variant name followed by its arguments: ```saga let c = Circle 5.0 let r = Rect 3.0 4.0 ``` ### Records Records are named collections of fields: ```saga record Point { x: Int, y: Int, } let p = Point { x: 3, y: 4 } p.x # 3 ``` Since records have no mutable state, "updating" a record means creating a new one with some fields changed: ```saga let p2 = { p | x: 10 } # new Point, x changed, y unchanged ``` #### Anonymous record fields A record field can itself be an inline record type, without needing to define a separate named record: ```saga record User { id: Int, name: { first: String, last: String }, email: String, } let u = User { id: 1, name: { first: "Alice", last: "Smith" }, email: "alice@example.com", } ``` Dot access chains through the levels: ```saga u.name.first # "Alice" ``` Record update works at any level too: ```saga let u2 = { u | name: { u.name | last: "Jones" } } ``` ### ADTs with Record Variants A useful pattern is using named records as ADT variants. Instead of putting field definitions inside the ADT, define separate records and reference them: ```saga record Success { status: Int, body: String, } record ApiError { code: Int, message: String, } type ApiResponse = | Success | ApiError ``` Pattern matching then uses record syntax: ```saga describe resp = case resp { Success { status, body } -> $"OK {show status}" ApiError { code, message } -> $"Error {show code}: {message}" } ``` This keeps your data shapes reusable and lets you pattern match on fields by name rather than position. ### Tuples Tuples group a fixed number of values without defining a record. Any arity works, no predefinition needed: ```saga let point = (3.0, 4.0) let tagged = ("alice", 42) # (String, Int) -- different types are fine fun swap : (a, b) -> (b, a) swap (x, y) = (y, x) # tuple arguments can be destructured! ``` Tuples are useful for returning multiple values from a function, or for quick grouping where a named record would be overkill. ### Generic Types Types can take type parameters, written as lowercase letters after the type name: ```saga type Maybe a = | Just a | Nothing type Result a e = | Ok a | Err e ``` The parameter `a` stands in for any type. When you use `Just 42`, the compiler infers `Maybe Int`. When you use `Nothing`, the type is inferred from context. Records can be generic too: ```saga record Box a { value: a, } let b1 = Box { value: 42 } # Box Int let b2 = Box { value: "hello" } # Box String ``` #### Functions that never return Some functions can never actually return a value: `panic` crashes the program, `fail!` transfers control to a handler. These are typed as `-> a`, a free type variable that unifies with any expected type. This lets them appear in any expression position without a type mismatch: ```saga # Both branches must return the same type. # panic is typed -> a, so it unifies with Int here. safe_head xs = case xs { h :: _ -> h [] -> panic "empty list" } ``` You will see this in signatures like `fun fail : String -> a` and `fun panic : String -> a`. It is not a special type -- it is just an unconstrained type variable, which means the type checker is happy to accept it wherever any concrete type is expected. ### Built-in Types Saga's prelude provides several common generic types: | Type | Description | | ------------ | ---------------------------------------- | | `Maybe a` | An optional value: `Just a` or `Nothing` | | `Result a e` | Success or failure: `Ok a` or `Err e` | | `List a` | A linked list, with `[]` and `::` syntax | | `Dict k v` | An immutable key-value map | Lists have special literal syntax: ```saga let nums = [1, 2, 3] let nested = [[1, 2], [3, 4]] let prepend = 0 :: nums # [0, 1, 2, 3] ``` These types are covered in depth in the standard library reference. ### Type Aliases A type alias gives an existing type a new name. Aliases are structural - the compiler unfolds them before any type comparison, so `UserId` and `Int` are fully interchangeable in the example below: ```saga type alias UserId = Int fun next_id : UserId -> UserId next_id n = n + 1 let uid : UserId = 42 let n : Int = next_id uid # OK — UserId is just Int ``` Aliases can take parameters of their own, and they can wrap any type expression - including parameterized types and function types: ```saga type alias Bag a = List a type alias Decoded a = Result a String type alias Predicate a = a -> Bool ``` Pattern matching, constructor application, and trait resolution all see through aliases to the underlying type: ```saga fun handle : Decoded Int -> Int handle r = case r { Ok n -> n Err _ -> 0 } ``` `pub type alias` exports the alias from a module like any other type. #### Restrictions A few rules keep aliases predictable rather than half-clever: - **Aliases must be fully applied.** Writing `Bag` (a 1-arity alias) without an argument is rejected — the alias must always be saturated at every use site. - **Aliases cannot be recursive.** `type alias T = List T` is a cycle and is rejected at the alias declaration. - **All variables in the body must be declared in the alias's parameter list.** `type alias Foo = Maybe b` is rejected because `b` isn't a parameter - Saga doesn't implicitly quantify alias bodies. If you need a nominally-distinct type rather than structural aliasing, use a single-constructor ADT (`type UserId = UserId Int`) instead - or pair one with a [symbol-tagged](/guide/generic-deriving#symbols-beyond-derive) parameter to get many distinct roles backed by a single wrapper. ## Pattern Matching *Case expressions, guards, destructuring, list/record/string patterns, and exhaustiveness.* Pattern matching is how you inspect and destructure values in Saga. It works in `case` expressions, function equations, and `let` bindings. The compiler checks every match for exhaustiveness, so you can't silently miss a case. ### Case Expressions A `case` expression matches a value against a list of patterns and evaluates the first arm that matches: ```saga type Shape = | Circle Float | Rect Float Float | Point area shape = case shape { Circle r -> 3.14 * r * r Rect w h -> w * h Point -> 0.0 } ``` Each arm is `pattern -> expression`. Patterns are tried top to bottom; the first match wins. ### Variables and Wildcards A bare name matches any value and binds it for use in the branch. An underscore `_` also matches any value but discards it: ```saga describe shape = case shape { Circle r -> $"circle with radius {r}" Rect _ _ -> "rectangle" Point -> "point" } ``` A variable also works as a catch-all arm. Unlike `_`, it gives you the value to use in the branch: ```saga type Priority = Critical | High | Medium | Low label priority = case priority { Critical -> "page immediately" High -> "fix today" other -> $"queued ({other})" } ``` `other` matches `Medium` and `Low` and binds the actual value, so it can appear in the expression. Use `_` when you don't need it at all. ### Guards A `when` clause adds a condition to a pattern. The arm only fires if the pattern matches and the guard expression is true: ```saga classify n = case n { n when n < 0 -> "negative" n when n > 100 -> "large positive" 0 -> "zero" n -> "positive" } ``` Guards work the same way on function equations: ```saga fizzbuzz n when n % 15 == 0 = "FizzBuzz" fizzbuzz n when n % 3 == 0 = "Fizz" fizzbuzz n when n % 5 == 0 = "Buzz" fizzbuzz n = show n ``` Any pure expression works as a guard, including calls to other functions: ```saga is_valid_length s = String.length s > 0 && String.length s < 100 describe s = case s { s when is_valid_length s -> "valid: " <> s _ -> "invalid" } ``` ### List Patterns Lists match against their structure: ```saga summarize items = case items { [] -> "empty" [_] -> "one element" [_, _] -> "two elements" _ :: _ -> "head and tail elements" } ``` The `::` pattern separates the head from the tail. You can chain it to reach deeper into the list: ```saga first_two xs = case xs { x :: y :: _ -> show x <> " and " <> show y _ -> "fewer than two elements" } ``` Patterns compose, so you can nest them freely. A list of tuples, for example: ```saga case pairs { (a, b) :: rest -> $"first pair: {a}, {b}" [] -> "empty" } ``` ### Tuple Patterns Tuples match by position: ```saga fun swap : (a, b) -> (b, a) swap (x, y) = (y, x) ``` ```saga label result = case result { (True, value) -> "found: " <> show value (False, _) -> "not found" } ``` ### Record Patterns Records can be destructured in patterns and matched on individual fields. You only need to mention the fields you care about -- unmentioned fields are ignored: ```saga record Point { x: Int, y: Int } quadrant p = case p { Point { x, y } when x > 0 && y > 0 -> "Q1" Point { x, y } when x < 0 && y > 0 -> "Q2" Point { x, y } when x < 0 && y < 0 -> "Q3" Point { x, y } when x > 0 && y < 0 -> "Q4" _ -> "origin or axis" } ``` To bind a field under a different name, use `field: alias`: ```saga greet user = case user { User { name: n, email: e } -> $"Hello {n}, your email is {e}" } ``` When you only care about which variant matched and not its fields, an empty pattern works: ```saga is_authenticated session = case session { LoggedIn {} -> True Anonymous {} -> False } ``` ### String Patterns Strings match exactly, or you can split off a prefix using `<>`: ```saga classify_log msg = case msg { "[ERROR]: " <> detail -> "Error: " <> detail "[WARN]: " <> detail -> "Warning: " <> detail "[INFO]: " <> detail -> "Info: " <> detail _ -> "unknown: " <> msg } ``` The `<>` pattern always splits from the left -- you match a known prefix and capture the rest. A wildcard arm is required because strings are infinite; unlike ADTs, the compiler can't enumerate all possibilities. ### Or-Patterns Multiple patterns can share a single arm using `|`: ```saga is_shaped shape = case shape { Circle _ | Rect _ _ -> True Point -> False } ``` ### Patterns in Function Parameters Patterns can appear directly in function parameters, not just in `case`: ```saga is_big (Circle r) when r > 10.0 = True is_big (Rect w h) when w > 10.0 || h > 10.0 = True is_big _ = False ``` This is equivalent to a `case` inside the function body but reads more naturally when each variant has distinct behavior. ### Let Destructuring Patterns also work in `let` bindings: ```saga let (x, y) = compute_origin () let Point { x, y } = get_anchor () let h :: t = items ``` This is convenient for functions that return tuples: ```saga let (width, height) = measure img ``` If the pattern might not match (like `h :: t` on a potentially empty list), use `case` instead. ### Exhaustiveness The compiler verifies that every `case` expression covers all possible values. A missing arm is a compile error: ```saga # Error: missing Nothing case case opt { Just x -> x } # Fine case opt { Just x -> x Nothing -> 0 } ``` A wildcard or catch-all variable arm also satisfies the check: ```saga case opt { Just x -> x _ -> 0 # covers Nothing (and any future variants) } ``` This applies to function equations too. Exhaustiveness checking means you can't have a runtime pattern match failure -- if the code compiles, every case is handled. ## Control Flow *do...else for sequential pattern binding, and list comprehensions.* Beyond `if/else` and `case`, Saga has two more control flow tools that come up constantly in practice. ### do...else When you have several sequential operations that can each fail, nesting `case` expressions gets unwieldy fast: ```saga fun lookup_grade : Int -> Result String String lookup_grade id = case (find_user id) { Err e -> Err e Ok name -> case (find_score name) { Err e -> Err e Ok score -> case (grade score) { Err e -> Err e Ok g -> Ok g } } } ``` `do...else` flattens this. Each line binds a pattern; if the pattern does not match, control jumps to the `else` block. Unlike monadic bind in other languages, `do...else` is purely pattern matching and doesn't require a specific type like `Result` or `Maybe`. Any pattern works, on any type, in any combination: ```saga fun lookup_grade : Int -> Result String String lookup_grade id = do { Ok user <- find_user id True <- is_active user Ok score <- find_score user.name g <- grade score Ok g } else { Err e -> Err e } ``` The last line (without `<-`) is the final return expression if all previous arms were successful. The `do` and `else` blocks must return the same type, e.g. a `Result` or `Maybe` or other ADT. The success expression does not have to be `Ok`. Here the happy path returns an `Int` and the else arm returns `0`: ```saga fun score_or_zero : Int -> Int score_or_zero id = do { Ok name <- find_user id Ok score <- find_score name score } else { Err _ -> 0 } ``` The `else` block must be exhaustive: it has to cover every value that could arrive from a failed binding. The compiler checks this the same way it checks `case` expressions. When different steps can fail in different ways, list all the bail patterns together in one `else` block: ```saga fun first_active_grade : Unit -> Result String String first_active_grade () = do { Just users <- active_users () Just user <- List.head users Ok score <- find_score user.id Ok g <- grade score Ok g } else { Nothing -> Err "nothing found" Err e -> Err e } ``` ### List Comprehensions List comprehensions build new lists from existing ones. The syntax is `[expr | qualifiers]`: ```saga # Map [x * 2 | x <- xs] # Filter [x | x <- xs, x > 0] # Both [x * 2 | x <- xs, x > 3] ``` Multiple generators produce a cartesian product: ```saga [x + y | x <- [1, 2, 3], y <- [10, 20]] # [11, 21, 12, 22, 13, 23] ``` `let` bindings work inside comprehensions: ```saga [y | x <- xs, let y = transform x, y > threshold] ``` Comprehensions desugar in the parser to `flat_map`, `if/else`, and `let`. There is no special runtime support -- they are syntax sugar for code you could write by hand. The effect system provides another form of control flow through the `Fail` effect, covered in [Effects & Handlers](/guide/effects-and-handlers) and [Error Handling](/guide/error-handling). ## Traits *Type-driven dispatch with Show, Eq, Ord. where clauses, supertraits, and deriving.* Traits give you type-driven dispatch. You define a set of operations once, then implement them differently for each type. When you call `show x`, the compiler looks at the type of `x` and picks the right implementation automatically. ### Defining a Trait A trait declares one or more function signatures that types can implement: ```saga trait Show a { fun show : a -> String } trait Eq a { fun eq : a -> a -> Bool } ``` The type variable `a` stands for the implementing type. Any type that implements `Show` must provide a `show` function that converts it to a `String`. A method signature is pure unless you say otherwise. A method may also declare a `needs` row, which lets impls leave effects unhandled for the caller to provide — see [Effectful Trait Methods](/guide/advanced-effects#effectful-trait-methods). ### Implementing a Trait Use `impl` to provide the implementation for a specific type: ```saga record User { name: String, age: Int, } impl Show for User { show user = $"{user.name} (age {user.age})" } impl Eq for User { eq a b = a.name == b.name && a.age == b.age } ``` Now `show some_user` and `eq user_a user_b` work on `User` values. ### Default Methods A trait can supply a default body for any of its methods. Write the signature as usual, then on a following line repeat the method name with an implementation: ```saga trait Greeter a { fun name : a -> String fun greet : a -> String greet x = "Hello, " <> name x <> "!" } ``` Now an `impl` only has to provide the methods that don't have defaults. The defaulted ones come along for free, but can still be overridden when a type wants different behavior: ```saga record User { name: String } record Robot { model: String } impl Greeter for User { name u = u.name # greet is inherited from the default } impl Greeter for Robot { name r = r.model greet r = "BEEP BOOP. UNIT " <> r.model <> " ONLINE." } ``` A default body can call the trait's other methods — `greet` above calls `name`, which dispatches to whichever implementation the type provides. It can also reference anything in scope where the trait is defined: imports and top-level functions. Defaults are useful for convenience methods that can be expressed in terms of a smaller required core. An implementer writes just the essentials; the trait fills in the rest. ### `where` Clauses Functions can require that their type parameters implement certain traits using `where`: ```saga fun to_string : a -> String where {a: Show} to_string x = show x ``` Multiple bounds on the same type variable use `+`: ```saga fun print_if_equal : a -> a -> String where {a: Show + Eq} print_if_equal x y = if eq x y then show x else "not equal" ``` Bounds on multiple type variables are comma-separated: ```saga fun convert : a -> b -> String where {a: Show, b: Show + Eq} convert x y = show x <> " -> " <> show y ``` ### Supertraits A trait can require that implementing types also implement another trait: ```saga trait Ord a where {a: Eq} { fun compare : a -> a -> Ordering } ``` Any type that implements `Ord` must also implement `Eq`. This means functions with an `Ord` bound can use both `compare` and `eq` without listing both explicitly. ### Conditional Implementations An implementation can have its own `where` clause, making it available only when certain constraints are met: ```saga impl Show for List a where {a: Show} { show xs = "[" <> String.join ", " (List.map show xs) <> "]" } ``` This says: `List a` implements `Show`, but only when `a` itself implements `Show`. So `show [1, 2, 3]` works (because `Int` implements `Show`), but `show` on a list of opaque values that don't implement `Show` is a type error. ### `deriving` For common traits, the compiler can generate implementations automatically: ```saga record Point { x: Int, y: Int, } deriving (Show, Eq, Ord) type Color = Red | Green | Blue deriving (Show, Eq, Enum) ``` The available derives are: | Trait | What it generates | | ------- | ------------------------------------------------------ | | `Show` | String representation based on constructors and fields | | `Debug` | Detailed debug output with type names | | `Eq` | Structural equality | | `Ord` | Structural ordering (by field/variant order) | | `Enum` | Conversion to/from integers (bare ADT variants only) | `deriving` saves boilerplate for types where the obvious implementation is the right one. For anything custom, write an explicit `impl`. ### Looking Ahead: Effectful traits Traits select an implementation based on the type. When you write `show x`, the compiler resolves which `show` to call based on what `x` is. This is fixed at compile time. The next section introduces effects, which look similar but work differently: the caller chooses the implementation at runtime. A useful rule of thumb: if the behavior depends on _what the data is_, use a trait. If it depends on _where the code is running_, use an effect. The two are not mutually exclusive. A trait still selects its impl by type, but that impl can itself perform effects whose handler the caller chooses at runtime. [Effectful Trait Methods](/guide/advanced-effects#effectful-trait-methods) covers how the two compose. ## Effects & Handlers *Declaring effects, performing with !, writing handlers, resume, and abort.* An **effect** is a capability a function uses — logging, fetching from the network, error handling — without specifying how it's actually carried out. The function declares the effect it needs, and a **handler**, supplied by the caller, provides the implementation. If you've worked with dependency injection in other languages, the basic shape will feel familiar: - An **effect** is like an interface: a named set of operations. - A **handler** is a concrete implementation of that interface. - The caller of the operation chooses which implementation to use. With effects, dependency injection is just a pattern — and the compiler checks the wiring for you. You decide which handlers to use at each function call, or even at your application's entry point, and every operation is guaranteed a handler before the program runs. ### Handlers control the continuation Effects go further than DI in one important way. When a function calls an effect operation like `log!`, control doesn't just _call_ the handler — it hands the handler the code that would run next, as a continuation. The handler can decide what to do with it: - Resume it once: behaves like a normal function call with a return value - Don't resume it: the rest of the code never runs, like an early return or error handling - Resume it after a delay: retries, backoff, supervision - Resume it multiple times: backtracking, nondeterminism, generators This is one of the features that makes effects a powerful abstraction. The same mechanism that allows you to swap behavior in and out also expresses error handling, concurrency, supervision, and even constraint solvers. ### Why you'd reach for an effect - **Testing without mocks.** Provide a different handler in tests; the function under test is unchanged. - **Per-environment swapping.** Dev runs against SQLite, prod against Postgres, with one handler choice at `main`. - **Ambient context.** Per-request values like the current user, request ID, or feature flags can be set up once at the request boundary instead of threaded through every function argument. This subsumes the Reader monad and request-scoped DI containers — see [Ambient context](/examples/ambient-context) for a worked example. - **Non-local control flow.** Errors that propagate without manual forwarding. Backtracking search. Guaranteed cleanup. Retries. ### It's effects all the way down Because effects are this useful, Saga leans on them everywhere. The standard library exposes the file system, console, HTTP, time, randomness, BEAM concurrency, and even the testing framework as effects, and ships with the handlers you'd expect for each. Once you understand the model in this section, you understand the shape of the whole stdlib. This is why function signatures stay honest: any effect a function performs but doesn't handle internally appears in its `needs` clause. You can see at a glance which handlers it will require from its caller. ### What you get - **Signatures that tell the truth.** No hidden I/O, no surprise side effects. - **No monad transformers.** Effects compose by listing them in `needs`. No lifting between layers, no stack ordering. - **Test isolation is just handler swapping.** No mocking libraries, no patching imports. - **Patterns that need a dedicated library elsewhere are a few lines of user code here.** Scoped resources, async/await, and supervision all fall out of the same handler machinery. The rest of this section covers how to declare effects, write handlers, and wire them up. [Error Handling](/guide/error-handling), [Handler Patterns](/guide/handler-patterns), and [Advanced Effects](/guide/advanced-effects) build on this foundation. ### Declaring an effect An effect is a named group of operations, defined with signatures but no implementation: ```saga effect Log { fun log : String -> Unit } ``` This says "there is an operation called `log` that takes a `String` and returns `Unit`." It does not say what `log` actually does. That is the handler's job. Effects can have multiple operations: ```saga effect Http { fun get : Request -> Response fun post : Request -> Response } ``` ### Performing effects Effect operations are called with `!` at the call site: ```saga log! "server starting" let resp = get! health_check ``` The `!` marks the exact point where control may transfer to a handler. Pure function calls never get it. If you see a `!`, you know something beyond ordinary computation is happening. Only operations declared directly in an `effect` block use `!`. Calling a function that internally uses effects is a normal call: ```saga # log! uses the bang because it is an effect operation # process_request does not, even though it uses effects internally fun process_request : Request -> Response needs {Log, Http} process_request req = { log! "handling request" let data = get! req.url parse_response data } # Calling process_request is a regular call process_request my_request ``` #### The `needs` clause A function that performs effects without handling them itself must declare them with `needs`: ```saga fun greet : String -> Unit needs {Log} greet name = log! $"Hello, {name}!" ``` This tells callers (and the compiler) what handlers must be provided. If a function calls another function that needs an effect without handling it, that effect propagates to the caller's signature: ```saga # greet needs {Log}, and we don't handle it here, so run also needs {Log} fun run : Unit -> Unit needs {Log} run () = greet "world" ``` Effects propagate all the way up the call chain until someone provides a handler. In some instances, you may want effects to bubble all the way up to your `main` function, where you wire in their handlers: ```saga fun main : Unit -> Unit main () = { run_app () } with { console, http, postgres, to_result, } ``` ### Writing a handler A handler provides implementations for an effect's operations. The simplest form is a named handler declaration: ```saga handler console for Log needs {Stdio} { log msg = { print! msg resume () } } ``` Three things to notice: 1. The handler specifies which effect it implements with `for Log`. 2. Each arm implements one operation. The arm for `log` receives `msg` (the argument from the call site) and decides what to do with it. 3. This handler `needs {Stdio}` because its implementation calls `print!`. Handlers can require effects of their own, covered in more detail below. #### `resume`: continuing the computation `resume` is a keyword available inside any handler arm. It sends a value back to the point where the effect was performed, and the computation continues from there. In the `console` handler above, `resume ()` means: "the call to `log!` returns `Unit`, and the code after `log!` keeps running." Here is a more illustrative example: ```saga effect Env { fun get_env : String -> Maybe String } handler dev_env for Env { get_env "DATABASE_URL" = resume (Just "localhost:5432/dev") get_env "APP_SECRET" = resume (Just "dev-secret") get_env _ = resume Nothing } fun db_url : Unit -> String needs {Env} db_url () = case (get_env! "DATABASE_URL") { Just url -> url Nothing -> "localhost:5432/default" } ``` When `db_url` calls `get_env! "DATABASE_URL"`, control transfers to the `dev_env` handler. The handler pattern-matches on the key, and calls `resume (Just "localhost:5432/dev")`. At that point, `get_env!` returns `Just "localhost:5432/dev"` and `db_url` continues into the `case`. #### Not resuming: aborting the computation A handler does not have to call `resume`. If it doesn't, the computation is abandoned and the handler's return value becomes the result of the entire `with` block. The built-in `Fail` effect is the clearest example. It declares a single operation that never returns normally: ```saga effect Fail { fun fail : String -> a } fun safe_divide : Int -> Int -> Int needs {Fail} safe_divide x 0 = fail! "division by zero" safe_divide x y = x / y let result = { safe_divide 10 0 } with { fail reason = Err reason } # result is Err "division by zero" ``` When `fail!` is performed, the handler returns `Err reason` directly. Everything after `fail!` in the computation is skipped. This is how error handling works in Saga: there is no special syntax for exceptions or try/catch. `Fail` is just an effect, and aborting is just a handler that chooses not to resume. #### The `return` clause When a handler aborts, the failure path produces a value (like `Err reason` above). But what about the success path? By default, the success value passes through unchanged. A `return` clause lets the handler intercept it: ```saga handler to_result for Fail { fail reason = Err reason return value = Ok value } ``` Now both paths return the same type: ```saga safe_divide 10 0 with to_result # Err "division by zero" safe_divide 10 2 with to_result # Ok 5 ``` Note that `return` here is not the keyword you may know from other languages. Saga has no general `return` statement. In this context, `return` is a special clause in a handler that transforms the final value of a successful computation. It only appears inside handler definitions. Most handlers don't need it. It comes up when both the success and failure paths need to produce a unified type, as with `to_result`. More advanced uses of `return` are covered in [Handler Patterns](/guide/handler-patterns). ### Attaching handlers with `with` The `with` keyword connects a computation to its handler: ```saga # Single handler greet "world" with console # Multiple handlers in a block fun main : Unit -> Unit main () = { process_request my_request } with { console, http, } ``` The handler can be a named declaration, a local binding, or even **defined inline**: ```saga # Inline handler for a one-off case greet "world" with { log msg = { print! $"[LOG] {msg}" resume () } } ``` Everything between the opening `{` and the `} with` is the "handled computation." Any effect operations performed inside that block (or in functions called from it) are routed to the attached handlers. ### Handlers that use effects The `dev_env` handler above is pure: it returns hardcoded values and needs no effects of its own. But a production handler might need to reach out to a secrets manager. Declare those dependencies with `needs` on the handler: ```saga effect SecretStore { fun fetch_secret : String -> Maybe String } handler prod_env for Env needs {SecretStore} { get_env key = { let secret = fetch_secret! key resume secret } } ``` This handler implements `Env` by delegating to a `SecretStore` effect. When you attach it, `SecretStore` must also be handled somewhere in the chain: ```saga main () = { run_app () } with { prod_env, onepassword, } ``` A handler with no `needs` clause is pure, like `dev_env` from earlier. ### Effects compose naturally When a function needs multiple effects, it just lists them. There is no layering, no ordering to get right, and no special plumbing to combine them. If you've used monad transformers in other languages (building stacks like `ReaderT Config (ExceptT Error (StateT S IO))` with lifting between layers), this is the problem effects eliminate entirely: ```saga fun process : Request -> Response needs {Log, Http, Fail, Database} process req = { log! "start" let user = query! req.user_id let data = get! user.api_url if data == "" then fail! "empty response" else build_response data } ``` Inside the function body, there is no layering. You call `log!`, `query!`, `get!`, and `fail!` freely without worrying about which "layer" each belongs to. There is no lifting between effect layers. ```saga main () = { process my_request } with { console, http, to_result, postgres, } ``` Adding a new effect to a function is just adding it to `needs` and providing a handler. No restructuring, no type-level plumbing. A function with fewer effects always fits where more are allowed. If a caller expects `needs {Log, Http}`, a function that only needs `{Log}` works fine. A pure function (no `needs` at all) fits everywhere. ### Testing with effects Since handlers are swappable, testing is straightforward: use the same function with different handlers. No mock frameworks, no patching imports. ```saga handler mock_http for Http { get req = resume (Response 200 "{\"status\": \"ok\"}") post req = resume (Response 201 "created") } handler silent for Log { log msg = resume () } test "process_request returns valid response" (fun () -> { let resp = process_request test_req with { mock_http, silent, } assert_eq resp.status 200 }) ``` The function under test is unchanged. Only the handlers differ. ### Putting it all together Here is a complete example: declaring effects, writing functions that use them, implementing handlers, and wiring everything up: ```saga # Declare effects effect Log { fun log : String -> Unit } effect Fail { fun fail : String -> a } # Functions that use effects fun parse_age : String -> Int needs {Fail} parse_age input = case (String.to_int input) { Ok n -> n Err _ -> fail! $"not a number: {input}" } fun validate_age : Int -> Int needs {Fail} validate_age age = if age < 0 || age > 150 then fail! $"age out of range: {age}" else age fun process_input : String -> String needs {Log, Fail} process_input input = { log! $"processing: {input}" let age = parse_age input let valid = validate_age age $"valid age: {valid}" } # Handlers handler console for Log needs {Stdio} { log msg = { print! msg resume () } } handler to_result for Fail { fail reason = Err reason return value = Ok value } # Wire it up fun main : Unit -> Unit main () = { let result = process_input "25" with to_result case result { Ok msg -> print! msg Err err -> print! $"Error: {err}" } } with {console, stdio} ``` The next section covers all the ways handlers can be defined, composed, and combined. ## Error Handling *The Fail effect, to_result, Result vs Fail philosophy, panic, and catch_panic.* Saga has no exceptions. Error handling uses ordinary values (`Result`) and the effect system (`Fail`), and the two work together. ### The `Fail` Effect `Fail` indicates that something went wrong. The function calling `fail!` doesn't decide what happens next -- the handler does. ```saga fun find_user : Int -> User needs {Fail, Database} find_user id = case (db_lookup! id) { Just user -> user Nothing -> fail! $"user {id} not found" } ``` `find_user` says "I might fail" through its `needs {Fail}` clause, but it has no opinion about what failure means. That's up to whoever handles it. #### Aborting with `to_result` The most common handler for `Fail` aborts the computation and wraps the outcome in a `Result`: ```saga handler to_result for Fail { fail reason = Err reason return value = Ok value } let result = find_user 42 with to_result case result { Ok user -> $"found: {user.name}" Err msg -> $"error: {msg}" } ``` When `fail!` is called, the handler doesn't call `resume`, so the computation stops. The `return` clause wraps the success path in `Ok` so both paths produce the same `Result` type. #### Resuming with a default value A handler can also choose to recover and keep going. Since `fail!` returns `-> a`, the handler can resume with any value that fits: ```saga find_user 42 with { fail _ = resume default_user } ``` Here the computation doesn't abort. `fail!` returns `default_user` at the call site and execution continues as if nothing went wrong. The same effect, different handler, completely different behavior. ### `Result` vs `Fail` Both represent operations that can fail. `Result` is a value you pattern match on. `Fail` is intended for flow control: it transfers execution to a handler. Use `Result` when you want the caller to inspect the outcome immediately. Use `Fail` when you want failures to propagate up to a handler boundary without manual forwarding at each step. The two convert easily. Lifting a `Result` into `Fail`: ```saga fun parse_and_validate : String -> Int needs {Fail} parse_and_validate s = case (parse_int s) { Ok n -> n Err e -> fail! e } ``` Going the other direction, `to_result` converts a `Fail` computation back into a `Result` at whatever boundary makes sense. ### `panic` and `todo` `panic` and `todo` are language builtins that crash the process immediately. ```saga panic "this should never happen" todo () ``` Both functions unify with any type, so they work in any expression position without a type error. Unhandled, they print to stderr and exit with code 1. Use `panic` for logic errors and genuinely unreachable code. Use `todo` for unfinished code during development. Neither is appropriate for recoverable errors: use `Fail` or return a `Result` type. ### `catch_panic`: Recovery Boundaries For cases where you need to protect a boundary from an unexpected crash, `catch_panic` runs a function and returns `Ok value` on success or `Err message` on panic: ```saga import Std.Process (catch_panic) case catch_panic (fun () -> handle_request req) { Ok resp -> resp Err msg -> { log! $"request panicked: {msg}" error_response 500 } } ``` This is not for error handling -- use `Fail` for recoverable errors. `catch_panic` exists for two specific cases: - **Process protection**: preventing one bad request from crashing a server loop - **Testing**: asserting that a function panics when it should ```saga test "head of empty list panics" { assert_panics (fun () -> List.head []) } ``` Effects work normally inside the thunk. Handlers from the surrounding scope are captured by the lambda, so effectful code runs as expected. ### Process Control For explicit exit codes, `Std.Process` provides two functions: ```saga import Std.Process Process.exit 0 # immediate halt, success Process.exit 1 # immediate halt, failure Process.shutdown 0 # graceful BEAM VM shutdown (flushes I/O, runs shutdown hooks) ``` Both return `-> a` and can appear anywhere a value is expected. Use `Process.exit` when you want to halt immediately. Use `Process.shutdown` when you want the VM to clean up first. ### Supervision For long-running processes, the right response to a failure is often to restart rather than propagate. Because `Fail` is just an effect, supervision is a handler that catches failures and re-runs the computation. This is covered in full in [Supervision](/guide/supervision). ## Handler Patterns *Named, inline, let-bound, and factory handlers. Stacking, ordering, and the return clause.* So far, handlers have been either named declarations or inline blocks attached with `with`. Handlers are more flexible than that: they are values you can bind, pass around, build from configuration, and compose together. ### Handler Bindings Handlers are values. You can bind them to variables with `let`, which means you can select a handler at runtime: ```saga fun main : Unit -> Unit main () = { let logger = if env == "dev" then console else sentry let db = if env == "dev" then sqlite else postgres run_app () with { logger, db } } ``` Both branches must produce the same handler type (`Handler Log` in this case), but the choice happens at runtime. The computation itself is written once. ### Handler Factories A handler expression (`handler for Effect { ... }`) produces a `Handler Effect` value without giving it a name. This is useful for factory functions that build handlers from configuration: ```saga fun make_logger : String -> Handler Log needs {Stdio} make_logger prefix = handler for Log { log msg = { print! $"[{prefix}] {msg}" resume () } } fun main : Unit -> Unit main () = { let logger = make_logger "app" run_app () with { logger, stdio } } ``` The factory closes over `prefix`, so every `log!` call in the handled computation gets the configured prefix. ### Bundling Multiple Effects A single handler can implement multiple effects. This is useful for grouping related configuration into one unit: ```saga handler dev for Log, Database needs {Stdio, Sqlite} { log msg = { print! msg resume () } query sql = sqlite_query! sql |> resume } handler prod for Log, Database needs {Sentry, Postgres} { log msg = { sentry_send! level msg resume () } query sql = postgres_query! sql |> resume } fun main : Unit -> Unit main () = { let env_handler = if env == "production" then prod else dev run_app () with env_handler } ``` ### Stacking Handlers When a computation needs multiple effects, attach multiple handlers in a `with` block. Named handlers, bindings, and inline arms can be mixed freely: ```saga main () = { run_app () } with { console, postgres, get req = http_get! req |> resume, } ``` For a single handler, the braces are optional: ```saga greet "world" with console ``` #### Ordering A `with` block is syntactic sugar for nested `with` expressions. The first handler listed is innermost: ```saga expr with { a, b, c } # is equivalent to: { { expr with a } with b } with c ``` When an effect operation is performed, the nearest enclosing handler gets the first chance to handle it. If that handler does not define the operation, it propagates outward to the next one. So if `a` and `b` both define `log`, `a` handles it because it is innermost. ### The `return` Clause As introduced in the previous section, `return` intercepts the success value of a handled computation. The most common use is `to_result`: ```saga handler to_result for Fail { fail reason = Err reason return value = Ok value } ``` Without `return`, a successful computation passes its value through unchanged. With `return`, the handler transforms it. #### `return` chains through stacking When multiple stacked handlers define `return` clauses, they compose through the nesting order. Given `expr with { a, b }` (which desugars to `{ expr with a } with b`): 1. The computation produces a value 2. `a.return` transforms it 3. `b.return` transforms the result of step 2 This means each `return` clause sees the output of the previous one, not the original value. ### Handlers Close Over Scope Handlers are closures. They can reference variables from the surrounding scope: ```saga fun main : Unit -> Unit main () = { let conn = connect! config let debug = True run_app () } with { query sql = { if debug then log! sql else () execute! conn sql |> resume }, } ``` The inline handler captures `conn` and `debug` from the enclosing block. Named handlers close over the scope where they are defined (typically module scope). Handler factories, as shown above, close over their arguments. ### Re-entrant Effects (Middleware) A handler can re-perform the same effect it handles by declaring it in its own `needs`. The re-performed operation routes to an outer handler, not back to itself. This enables middleware: intercept an effect, delegate to the real implementation, and add behavior around it. ```saga handler with_retry for Http needs {Http} { get req = { let resp = get! req if resp.status == 500 then get! req else resume resp } } # Stack it: with_retry wraps http { fetch_data () with with_retry } with http ``` When `with_retry` calls `get! req`, its `needs {Http}` is satisfied by the outer `http` handler. The result flows back to `with_retry`, which decides whether to retry or resume. This pattern works for any effect: logging wrappers, caching layers, metrics, retry logic. The shape is always the same: handle the effect, re-perform it to delegate, wrap the result. ### Cleanup with `finally` A handler arm can include a `finally` block that runs after `resume`'s continuation completes, regardless of whether the computation succeeded or was aborted by another handler. This guarantees resource cleanup: ```saga effect Resource { fun acquire : String -> String } handler managed for Resource { acquire name = { let handle = open_resource name resume handle } finally { close_resource handle } } ``` The `finally` block runs after everything that follows `resume` has finished. If the computation completes normally, `finally` runs. If another handler (like `to_result`) aborts the computation, `finally` still runs: ```saga fun work : Unit -> String needs {Resource, Fail} work () = { let db = acquire! "db" fail! "something went wrong" } # The managed handler's finally block runs even though # to_result aborts the computation let result = { work () with managed } with to_result # db resource is cleaned up, result is Err "something went wrong" ``` The `finally` block inherits the scope of its handler arm, so variables bound before `resume` (like `handle` above) are accessible in the cleanup code. One important constraint: effects performed inside a `finally` block cannot propagate beyond the handler they belong to. The block can use effects that are already available inside the handler (including those declared in its own `needs`), but it cannot introduce new unhandled effects. This ensures cleanup code is self-contained and cannot itself be interrupted by an outer handler. ## Advanced Effects *Row polymorphism, effect signatures on higher-order functions, subtyping, and multishot continuations.* The previous sections covered declaring effects, writing handlers, and composing them. This section goes deeper into how effects interact with higher-order functions, and what else you can do with `resume`. ### Open vs Closed Effect Rows When a higher-order function takes a callback, the type annotation controls which effects that callback is allowed to use. A **closed** row lists exactly the effects allowed. The higher-order function is expected to handle them internally: ```saga fun try : (Unit -> a needs {Fail}) -> Result a String try f = f () with to_result ``` The callback can use `Fail` and nothing else. `try` handles it, and no effects leak out. A callback with no `needs` clause at all is the most closed: it must be pure. An **open** row uses `..e` to accept additional effects and forward them to the caller: ```saga fun run_logged : (Unit -> a needs {Log, ..e}) -> a needs {..e} run_logged f = f () with console ``` `run_logged` handles `Log` itself but forwards everything else through `..e`. If the callback also uses `Fail`, then `..e` includes `Fail` and the effect propagates upward through `run_logged`'s signature: ```saga # callback uses Log and Fail # run_logged handles Log, Fail propagates through ..e fun main : Unit -> Unit needs {Fail} main () = run_logged (fun () -> { log! "about to do something risky" fail! "oops" }) ``` This is how the standard library works. `List.map` and `List.filter` take pure callbacks (closed, no effects). `List.iter` takes an open row because it exists to run a function for its effects: ```saga pub fun iter : (a -> Unit needs {..e}) -> List a -> Unit needs {..e} ``` When `..e` is empty (a pure callback), `iter` has no effects. When `..e` is `{Log}`, `iter` needs `{Log}`. The row variable captures whatever the callback actually does. ### Combining Effects and Traits A function can require both effects and trait bounds. In the signature, `needs` comes first and `where` comes second: ```saga pub fun print_all : List a -> Unit needs {Log} where {a: Show} print_all items = case items { [] -> () h :: t -> { log! (show h) print_all t } } ``` Read it as: "this function needs these effects" (runtime), "where these types satisfy these traits" (compile-time). In that example the function performs `Log` itself and the trait bound (`Show`) is pure. The next section covers the other case: a trait method that is *itself* effectful. ### Effectful Trait Methods Trait methods are pure by default. `Show`, `Eq`, and `Ord` declare no effects, and that purity is enforced — but the rule is about what *escapes* an impl, not what it does internally. An impl may perform any effect operation; what it cannot do is leave an effect *unhandled* unless the trait method's row permits it. So a pure method means every effect an impl performs must be handled within the impl itself. ```saga trait Show a { fun show : a -> String } # Fine: log! is performed, but handled inside the impl, so nothing escapes. # show stays pure. impl Show for User { show user = { log! "rendering a user" user.name } with { log _ = resume () } } # Error: log! escapes unhandled, and show's pure row leaves no room for # Log to propagate out to the caller. impl Show for User { show user = { log! "rendering a user" user.name } } ``` This is the open-row forwarding rule from earlier, applied to traits: an impl can only let effects *escape* that the trait method's signature names or opens — anything else it must handle itself. Keeping it opt-in is what lets generic code over `Show`/`Eq` stay clean — a function `where {a: Show}` never picks up surprise effects, because no impl can leak one. #### Declaring the capability To let impls perform effects, give the method an effect row. Just like a callback, it can be **closed** (naming exactly which effects are allowed) or **open** (`..e`, allowing any). A closed row names the effects every impl may use: ```saga trait Render a { fun render : a -> String needs {Log} } impl Render for User needs {Log} { render user = { log! $"rendering {user.id}" user.name } } ``` Because those effects are part of the method's declared type, they propagate like any other named effect — calling `render` requires `Log` whether the call is on a concrete type or behind a generic bound. An open row lets each impl choose its own effects, filling in `..e`: ```saga trait Render a { fun render : a -> String needs {..e} } # This impl fills ..e with {Database} impl Render for ProductId needs {Database} { render id = query! $"SELECT name FROM products WHERE id = {id}" } # A pure impl fills ..e with nothing impl Render for Int { render n = show n } ``` #### Effects flow from the impl to the caller When you call an effectful trait method on a *concrete* type, the selected impl's effects become part of your function's effects: ```saga # render on a ProductId needs {Database}, so describe does too fun describe : ProductId -> String needs {Database} describe id = "product: " <> render id ``` Calling `render` on an `Int` adds nothing, because that impl is pure. The effects you pick up are exactly the ones the chosen impl actually performs. #### Forwarding through a generic function Inside a generic function the self type is abstract, so the compiler can't know which impl will be chosen, nor which effects `render` will perform. It can't handle effects it can't name, so it must **forward** them. The effects contributed by a constraint surface as a row variable named after the type variable — `..a` reads as "a's effects": ```saga fun render_all : List a -> String needs {..a} where {a: Render} render_all [] = "" render_all (x :: rest) = render x <> "\n" <> render_all rest ``` `render_all` works for any `a` that implements `Render`. Instantiated at `ProductId`, `..a` resolves to `{Database}` and the caller needs a `Database` handler; at `Int` it resolves to nothing and `render_all` is pure. One function, effects determined per instantiation. A function bound over two effectful traits forwards one row variable per constraint: ```saga fun report : a -> b -> String needs {..a, ..b} where {a: Render, b: Render} ``` A `pub` function must declare these rows in its signature; a private function may leave them to be inferred. This explicitness is deliberate. A generic function's effects are fixed by the trait bounds written at its definition, never by which impls happen to exist elsewhere — so adding a new effectful impl in another module can never silently change `render_all`'s type. It is the same reason effect-capability is opt-in on the method in the first place: effects you didn't ask for never appear. ### Scoped Resources The `finally` keyword from [Handler Patterns](/guide/handler-patterns) enables a pattern for flat, multi-resource management. Some libraries (like Effect-TS) need a dedicated Scope API for this. In Saga, it falls out of the existing handler machinery in a few lines: ```saga effect Scope { fun acquire_scoped : (acquire: Unit -> a) -> (release: a -> Unit) -> a } handler run_scoped for Scope { acquire_scoped acquire release = { let resource = acquire () resume resource } finally { release resource } } ``` `acquire_scoped!` takes two functions: one to acquire a resource and one to release it. The handler acquires the resource, passes it to the computation via `resume`, and the `finally` block guarantees cleanup runs when the computation exits, in reverse acquisition order. ```saga { let db = acquire_scoped! (fun () -> connect "postgres") disconnect let cache = acquire_scoped! (fun () -> connect "redis") disconnect query db lookup cache } with run_scoped # redis disconnected first, then postgres ``` Cleanup runs regardless of how the computation exits: normal completion, abort via `Fail`, or panic. And because each resource registers its own cleanup at acquisition time, you can acquire multiple resources without nesting: ```saga let result = { let db = acquire_scoped! (fun () -> connect "postgres") disconnect let cache = acquire_scoped! (fun () -> connect "redis") disconnect query db fail! "something broke" } with { run_scoped, to_result } # both resources cleaned up, result is Err "something broke" ``` ### Multishot Continuations In every handler example so far, `resume` has been called at most once. But a handler can call `resume` multiple times. Each call re-runs the entire remaining computation from the effect call site, branching the execution. Here is a simple example: a Log handler that runs the continuation twice for every `log!` call: ```saga handler double_log for Log { log msg = { dbg ("[1] " <> msg) resume () dbg ("[2] " <> msg) resume () } } fun do_work : Unit -> Unit needs {Log} do_work () = { log! "A" log! "B" } main () = do_work () with double_log # [1] A # [1] B # [2] B # [2] A # [1] B # [2] B ``` The first `resume` runs the rest of the computation (which hits `log! "B"` and branches again). The second `resume` re-runs from the same point, producing a tree of executions. #### Backtracking search Multishot continuations become genuinely useful for nondeterministic search. A `Choose` effect lets you write search problems as straight-line code, and the handler silently explores all branches: ```saga effect Choose { fun choose : List a -> a } handler all_solutions for Choose, Fail { choose options = List.flat_map (fun x -> resume x) options fail _ = [] return value = [value] } ``` The handler calls `resume` once per option in the list, collecting all results with `flat_map`. Branches that hit `fail!` produce an empty list and are pruned. This lets you write a constraint solver that reads like imperative code: ```saga fun guard : Bool -> Unit needs {Fail} guard True = () guard False = fail! "pruned" fun pythagorean_triples : Int -> List (Int, Int, Int) pythagorean_triples n = { let nums = List.range 1 n let a = choose! nums let b = choose! (List.range a n) let c = choose! (List.range b n) guard (a * a + b * b == c * c) (a, b, c) } with all_solutions pythagorean_triples 20 # [(3, 4, 5), (5, 12, 13), (6, 8, 10), (8, 15, 17), (9, 12, 15), (12, 16, 20)] ``` Each `choose!` looks like it picks one value, but the handler tries every possibility. `guard` prunes branches that don't satisfy the constraint. The result is all solutions collected into a list. This is the full power of algebraic effects: the same `resume` mechanism that handles logging and error recovery also expresses nondeterministic search, all without special language support. ## Testing *Effect-based testing with describe, test, handler swapping, and CI integration.* Saga has a built-in test runner that discovers and executes tests automatically. Tests are ordinary modules that export a `tests` function using the `Testing` effect. The effect system handles test isolation naturally: swap a handler and your function runs against a different implementation with zero mocking libraries. ### Writing Tests Test files live in the `tests/` directory and are regular saga modules. Each test module exports a `pub fun tests` function that registers tests using functions from `Std.Test`: ```saga module MathTest import Math import Std.Test (Testing, describe, test, assert_eq) pub fun tests : Unit -> Unit needs {Testing} tests () = { describe "Math" (fun () -> { test "addition" (fun () -> { assert_eq (Math.add 2 3) 5 }) test "subtraction" (fun () -> { assert_eq (Math.sub 5 3) 2 }) }) } ``` `test`, `describe`, `skip`, and `only` are ordinary functions imported from `Std.Test`. `test` and `skip` take a name and a lambda. `describe` groups related tests and can be nested: ```saga pub fun tests : Unit -> Unit needs {Testing} tests () = { describe "User" (fun () -> { describe "validation" (fun () -> { test "rejects empty name" (fun () -> { assert_eq (validate "") (Err "name required") }) test "accepts valid input" (fun () -> { assert_eq (validate "Alice") (Ok "Alice") }) }) }) } ``` ### Assertions `Std.Test` provides assertion functions that all use the `Test` effect internally. The test runner handles this effect automatically: | Function | Checks | | -------------- | ------------------ | | `assert_eq` | `a == b` | | `assert_neq` | `a != b` | | `assert_true` | value is `True` | | `assert_false` | value is `False` | | `assert_gt` | `a > b` | | `assert_gte` | `a >= b` | | `assert_lt` | `a < b` | | `assert_lte` | `a <= b` | | `assert_some` | value is `Just _` | | `assert_none` | value is `Nothing` | | `assert_ok` | value is `Ok _` | | `assert_err` | value is `Err _` | On failure, assertions produce a descriptive message: ``` Expected 5, got 3 Expected True, got False Expected Ok(_), got Err("not found") ``` You can also write your own assertion helpers by calling `assert!` directly: ```saga import Std.Test (Test) fun assert_positive : Int -> Unit needs {Test} assert_positive n = assert! (n > 0) $"Expected positive, got {n}" ``` ### Shared Setup Since tests are registered from a regular function, shared setup is just ordinary `let` bindings before your test registrations: ```saga pub fun tests : Unit -> Unit needs {Testing} tests () = { describe "User" (fun () -> { let user = User { name: "Alice", age: 30 } test "has name" (fun () -> { assert_eq user.name "Alice" }) test "has age" (fun () -> { assert_eq user.age 30 }) }) } ``` ### Testing with Effects The effect system makes test isolation natural. Functions that use effects are testable by providing different handlers: ```saga pub effect Database { fun query : String -> List String } pub fun get_users : Unit -> List String needs {Database} get_users () = query! "SELECT name FROM users" ``` ```saga test "returns query results" (fun () -> { let result = { get_users () } with { query sql = resume ["alice", "bob"], } assert_eq result ["alice", "bob"] }) ``` No mocking library, no dependency injection setup. The test provides a handler and the function runs against it. ### Testing Panics `assert_panics` verifies that a function panics when it should: ```saga test "head of empty list panics" (fun () -> { assert_panics (fun () -> List.head []) }) ``` ### `only` and `skip` `skip` marks a test to be reported but not executed. `only` is global across the selected suite: if any `only` exists anywhere, every non-`only` test in every selected module is reported as skipped. ```saga skip "not ready yet" (fun () -> { assert_eq 1 2 }) only "focus on this" (fun () -> { assert_eq 1 1 }) ``` ### Running Tests ``` saga test # run all tests saga test math # filter by name ``` Test files are regular modules that import the code under test and only have access to `pub` items. The `describe`/`test` hierarchy maps directly to indented output: ``` math_test ✓ addition ✓ subtraction user_test User validation ✓ rejects empty name ✓ accepts valid input age ✗ must be positive (Expected 5 to be greater than 0) Tests: 4 passed, 1 failed, 5 total ``` `saga test` exits with code 0 if all tests pass, code 1 if any test fails, so it works directly in CI pipelines. ### Under the Hood The entire test framework is built on saga's own effect system. The `Test` effect exposes a single `assert` operation: ```saga effect Test { fun assert : (ok: Bool) -> (msg: String) -> Unit } ``` All assertion helpers (`assert_eq`, `assert_true`, etc.) are ordinary functions that call `assert!`. For example: ```saga fun assert_eq : a -> a -> Unit needs {Test} where {a: Debug + Eq} assert_eq a b = if a == b then assert! True "" else assert! False $"Expected {debug b}, got {debug a}" ``` The runner handles each test body with a handler that resumes on success and aborts on failure: ```saga body () with { assert ok msg = if ok then resume () else Failed msg return _ = Passed } ``` When an assertion passes, the handler calls `resume` to continue the test. When one fails, it simply doesn't resume, and the test stops at that point and records a failure. No special control flow, no exceptions, no catching panics, no macros. Fail-fast is just what happens when a handler chooses not to resume. The same mechanism that powers application logic powers the test framework too. ## Project & Modules *Project layout, project.toml configuration, module declarations, imports, pub visibility, and qualified access.* So far, every example has been a single-file script with a `main` function. Modules let you split code across files with namespaces and visibility control. ### Creating a Project To use modules, you need a project. Create one with: ```bash saga new my-app ``` This creates a directory with a `project.toml` and a `Main.saga` file. From here you can add more `.saga` files, each declaring its own module. ### Declaring a Module A module file declares its name at the top: ```saga module Math ``` Module names do not need to match the file path. A file at `lib/utils/helpers.saga` can declare `module Helpers` or `module Utils.Helpers` or anything else. The compiler discovers all `.saga` files in your project and resolves imports by declared module name. Nested modules use dots: ```saga module Data.Collections ``` Script files (single files with just a `main` function) do not declare a module name. Modules are only used in projects. ### Imports Import a module to use its public definitions: ```saga # Import a module (access via Math.abs, Math.max) import Math # Import with an alias (access via M.abs, M.max) import Math as M # Import specific names into scope (access as bare abs, max) import Math (abs, max) # Both alias and specific names import Math as M (abs, max) ``` A plain `import Math` gives you qualified access only: `Math.abs`, `Math.max`. To use names without a qualifier, import them explicitly with `import Math (abs, max)`. When two modules export the same name, use qualifiers to disambiguate: ```saga import Data.List import Data.Set let xs = List.map f items let ys = Set.map f items ``` Aliases let you choose a different qualifier: ```saga import Data.List as L L.map f items ``` ### Visibility By default, definitions are private to their module. Use `pub` to export them: ```saga pub fun add : Int -> Int -> Int # function pub type Shape = Circle Float | Rect Float Float # ADT pub record User { name: String, age: Int } # record pub handler console for Log { ... } # handler ``` Public functions require a type annotation. This means making something public forces you to document its type, which serves as both a stable API contract and inline documentation for anyone reading the module's exports. Private functions can be fully inferred: ```saga # Public: annotation required pub fun abs : Int -> Int abs n when n < 0 = -n abs n = n # Private: annotation optional, type is inferred double x = x * 2 ``` ### Opaque Types An opaque type exports the type name but hides its constructors. Other modules can pattern match on it but cannot construct values directly: ```saga module Auth opaque type Token = Token String pub fun make_token : String -> Token make_token secret = Token (hash secret) ``` ```saga module App import Auth (Token, make_token) # Pattern matching is fine case token { Token _ -> "has a token" } # Type error: Token constructor is not visible outside Auth let bad = Token "forged" ``` This enforces invariants: values of the type can only be created through the module's public API. ### Project Structure A Saga project uses a `project.toml` file at its root. The simplest form declares an executable: ```toml [project] name = "my-app" [bin] main = "src/Main.saga" ``` `[bin].main` defaults to `Main.saga`. The main file must define a `main` function. The compiler scans all `.saga` files in the project directory to build the module map. Running `saga build`, `saga run`, or `saga test` starts from the project root (the directory containing `project.toml`). Test files live in a `tests/` directory by default and are discovered automatically by `saga test`. ### Libraries A project can be a library that other projects depend on. Add a `[library]` section to declare what's exposed: ```toml [project] name = "mathlib" [library] module = "Math" # root namespace, required expose = ["Math", "Math.Vector", "Math.Matrix"] # public modules, required ``` - `[library].module` is the root namespace. Every module listed in `expose` must be prefixed by it. - `[library].expose` lists the modules consumers can `import`. Unlisted modules are still compiled (they're needed at runtime) but are invisible to the type system, so consumers can't import them. This lets you keep internal helpers private while still using them inside the library. A project can have `[library]`, `[bin]`, or both. A combined project ships an executable while also being usable as a dependency: ```toml [project] name = "my-app" [library] module = "MyApp" expose = ["MyApp.Client", "MyApp.Types"] [bin] main = "Main.saga" ``` For consuming dependencies (Hex, git, path), see [Ecosystem](/guide/ecosystem). For calling Erlang or Elixir libraries directly, see [Interop](/guide/interop). ## Ecosystem *Hex, git, and path dependencies. saga install, the lockfile, transitive deps, NIF compilation with rebar3, and the dep cache.* Saga projects can pull in code from three places: the [Hex package registry](https://hex.pm) (BEAM ecosystem packages), git repositories, and local filesystem paths. All three are declared in `project.toml` under a `[deps]` section, fetched and compiled by `saga install`, and pinned by a `saga.lock` lockfile. ### Declaring Dependencies ```toml [project] name = "my-app" [bin] main = "Main.saga" [deps] base64url = { version = "1.0.1" } # Hex saga_csv = { git = "https://github.com/dylantf/saga_csv" } # git mathlib = { path = "../mathlib" } # path ``` Hex is the default source — if a dep entry has no `path` or `git` field, it's treated as a Hex package and the dep key is the Hex package name. ### Hex Packages [Hex](https://hex.pm) is the package registry for the BEAM ecosystem (Erlang and Elixir). Hex packages are compiled to BEAM bytecode and available on the code path. They are not typechecked by Saga — to call into them you write FFI declarations (see [Interop](/guide/interop)). ```toml [deps] base64url = { version = "1.0.1" } jason = { version = "1.4.0" } argon2 = { version = "1.2.0" } ``` `saga install` downloads the tarball from `repo.hex.pm`, extracts it, compiles it, and installs the result into your project's `deps/{name}/` directory. #### Pure Erlang vs NIFs Hex packages are compiled with one of two strategies: - **Pure Erlang packages** (no native code): compiled directly with `erlc`. Fast, no extra tools needed. - **Packages with NIFs or build hooks**: detected by the presence of `c_src/`, `native/`, or `pre_hooks` / `port_specs` in the package's `rebar.config`. These are compiled with `rebar3 bare compile`, which handles native code (C, Rust, etc.) via the package's build hooks. NIF packages require `rebar3` on your PATH: ```bash mise install rebar3 ``` If `saga install` reports a missing `rebar3`, the failing package needs it. #### Version requirements For now, Hex deps use exact versions. Transitive dependencies from Hex packages may specify `~>` requirements (e.g., `~> 1.0`), which are resolved to the latest compatible version automatically. ### Git Dependencies For libraries not on Hex, or for your own projects: ```toml [deps] math = { git = "https://github.com/someone/math-lib", tag = "v1.0.0" } utils = { git = "https://github.com/someone/utils", branch = "main" } http = { git = "https://github.com/someone/http", rev = "abc123f" } ``` Specify exactly one of `tag`, `branch`, or `rev`. If none is given, the default is `HEAD`. A git dependency must be a Saga project (have a `project.toml` with a `[library]` section). See [Project & Modules](/guide/modules#libraries) for how to declare a library. ### Path Dependencies For local libraries during development: ```toml [deps] mathlib = { path = "../mathlib" } ``` Path dependencies must also have a `project.toml` with a `[library]` section. This is the most direct way to develop a library and consumer in tandem without publishing or pushing. ### Aliasing with `as` Both path and git deps support `as` to remap the library's module prefix: ```toml [deps] http = { path = "deps/http", as = "Net" } ``` If the dep declares `module = "HTTP"` in its `[library]` section and the consumer specifies `as = "Net"`, then `HTTP.Client` becomes `Net.Client` in the consumer's code. Useful when two deps would otherwise collide on a name, or when you want to use a shorter local name. ### The Lockfile `saga install` writes a `saga.lock` file that pins each dependency to an exact resolved state, so subsequent builds are reproducible: ```toml # saga.lock (auto-generated, do not edit by hand) [deps.math] git = "https://github.com/someone/math-lib" ref = "v1.0.0" commit = "abc123def456789..." [deps.base64url] hex = "base64url" version = "1.0.1" checksum = "f9b3add4731a02a9..." ``` Workflow: - `saga install` — resolve all deps, write `saga.lock`, install into `deps/`. - Subsequent `saga build` / `saga run` — use pinned versions from the lock, skip resolution. - `saga update` — re-resolve refs (e.g., follow a branch to its latest commit, pick up new Hex versions), write a new lockfile. Commit `saga.lock` to version control so collaborators and CI build against the same exact dependencies. ### Transitive Dependencies If your dep has its own deps, they're handled differently depending on source: - **Hex transitives** are resolved and installed automatically. If `base64url` declares its own Hex requirements, those are fetched and compiled by `saga install` along with `base64url` itself. - **Path and git transitives** are *not* automatically exposed to your project. They're compiled (since they're needed at runtime) but their modules don't appear in your import resolution. To use them, the intermediate dep must list them in its `[library].expose`, or you must add them to your own `[deps]`. This prevents transitive implementation details from leaking into your project's namespace. ### Collision Detection If two deps expose the same module name (after `as` aliasing), the compiler errors and asks you to add an `as` alias to one of them. ### Build Output After `saga install` and a build, your project layout looks like: ``` my-app/ project.toml saga.lock src/ Main.saga deps/ base64url/ ebin/ # compiled .beam files priv/ # package assets, if any saga_csv/ _build/ # compiled Saga library output _build/ dev/ # your project's compiled .beam files .stdlib/ # precompiled stdlib (per project, keyed by compiler version) ``` To reinstall everything from scratch, delete `deps/` and run `saga install` again. Source downloads are cached globally to avoid re-downloading: - Hex tarballs: `~/.saga/cache/hex/` - Git repos: `~/.saga/cache/git/` (bare clones, shared across projects) ### Wrapping Hex Packages Hex packages are opaque to the type system. To call into them from Saga, use `@external` to declare typed wrappers around their Erlang functions: ```saga @external("erlang", "base64url", "encode") pub fun encode : String -> String ``` When the Erlang function's calling convention doesn't map cleanly, write a small Erlang bridge file. Bridge files can call Hex deps directly because Erlang module calls are late-bound (resolved at runtime, not compile time): ```erlang %% argon2_bridge.erl -module(argon2_bridge). -export([hash/1]). hash(Password) -> {ok, Hash} = argon2:hash(Password), Hash. ``` ```saga @external("erlang", "argon2_bridge", "hash") pub fun argon2_hash : String -> String ``` See [Interop](/guide/interop) for the full FFI story, including type representations and the limitation on effectful callbacks. # BEAM Platform ## Concurrency & Actors *The Actor effect: spawn, send, receive. Isolation and message passing on the BEAM.* Saga runs on the BEAM, which means concurrency is built on isolated processes that communicate through message passing. There is no shared memory between processes and no data races by construction. Saga wraps the BEAM's actor model in effects, so spawning processes and sending messages use the same `!` syntax as any other effect operation. ### The Actor Model A BEAM process is a lightweight, isolated unit of execution with its own mailbox. Processes communicate by sending immutable messages to each other. This is fundamentally different from thread-based concurrency: there are no locks, no mutexes, and no shared state. Saga exposes this through two effects: - **`Process`**: spawning processes, sending messages, and exiting processes - **`Actor msg`**: receiving messages in the current process's typed mailbox Both are handled by the `beam_actor` handler, which maps directly to BEAM primitives. ```saga import Std.Actor (Process, Actor, beam_actor) ``` ### Spawning and Sending `spawn!` takes a function and runs it in a new process. It returns a `Pid msg`, a process identifier typed by the message type the process accepts: ```saga type Msg = Hello String | Goodbye fun greeter : Unit -> Unit needs {Actor Msg} greeter () = receive { Hello name -> { dbg $"Hello, {name}!" greeter () } Goodbye -> dbg "Bye!" } main () = { let pid = spawn! (fun () -> greeter ()) send! pid (Hello "Alice") send! pid (Hello "Bob") send! pid Goodbye } with beam_actor ``` `spawn!` returns a `Pid Msg` because the greeter function uses `Actor Msg`. The type system ensures you can only send `Msg` values to this pid. Sending a value of the wrong type is a compile error. ### Receiving Messages `receive` is a keyword expression (not an effect operation) that suspends the current process until a matching message arrives. It uses pattern matching syntax, just like `case`: ```saga fun counter : Int -> Unit needs {Actor CounterMsg, Process} counter count = receive { Increment n -> counter (count + n) GetCount caller -> { send! caller count counter count } Stop -> () } ``` The process blocks at `receive` until a message in its mailbox matches one of the patterns. Unmatched messages stay in the mailbox for future `receive` calls. This is selective receive, the same mechanism as Erlang's `receive`. The `receive` block is fully typed. The compiler builds a union of the process's message type (from `Actor msg`) and any system messages (like `Down` from monitors). If your actor uses `Actor CounterMsg` and you have a monitor active, the receive block accepts `CounterMsg` variants and `Down`/`Exit` system messages, and nothing else. Unlike `case`, exhaustiveness checking is disabled for `receive`: you can match on a subset of the message type, and unmatched messages simply stay in the mailbox for a future `receive` call. #### Timeouts Add an `after` clause to avoid waiting forever: ```saga receive { Response data -> handle data after 5000 -> dbg "timed out" } ``` The timeout is in milliseconds. If no matching message arrives within the timeout, the `after` branch runs instead. ### Getting Your Own Pid `self!` returns the current process's pid: ```saga fun request_count : Pid CounterMsg -> Int needs {Process, Actor Int} request_count counter_pid = { send! counter_pid (GetCount (self! ())) receive { n -> n } } ``` The type of `self! ()` is `Pid msg`, where `msg` matches the current process's `Actor msg` effect. This means a process that uses `Actor Int` gets back a `Pid Int` from `self!`. ### A Complete Example Here is a counter actor that handles increment, query, and stop messages: ```saga import Std.Actor (Process, Actor, beam_actor) type CounterMsg = | Increment Int | GetCount (Pid Int) | Stop fun counter : Int -> Unit needs {Process, Actor CounterMsg} counter count = receive { Increment n -> counter (count + n) GetCount caller -> { send! caller count counter count } Stop -> () } fun run_counter : Unit -> Unit needs {Process, Actor Int} run_counter () = { let pid = spawn! (fun () -> counter 0) send! pid (Increment 5) send! pid (Increment 3) send! pid (GetCount (self! ())) let result = receive { n -> n } dbg $"count: {result}" } main () = { run_counter () } with beam_actor ``` ### Monitoring Monitoring lets one process watch another and receive a notification when it exits. The `Monitor` effect provides `monitor!` and `demonitor!`: ```saga import Std.Actor (Process, Actor, Monitor, beam_actor) fun watcher : Unit -> Unit needs {Process, Actor WorkerMsg, Monitor} watcher () = { let pid = spawn! (fun () -> worker ()) let _ref = monitor! pid send! pid (Work 42) send! pid Die receive { Down _pid reason -> dbg $"Worker exited: {reason}" after 5000 -> dbg "Timed out waiting" } } main () = { watcher () } with beam_actor ``` When a monitored process exits, a `Down pid reason` system message is delivered to the monitoring process's mailbox. These system messages can be matched in `receive` blocks alongside regular messages. ### Linking `Link` provides bidirectional crash propagation. If two processes are linked and one crashes, the other crashes too: ```saga import Std.Actor (Link) fun start_worker : Unit -> Unit needs {Process, Actor msg, Link} start_worker () = { let pid = spawn! (fun () -> worker ()) link! pid } ``` Links are useful when two processes are co-dependent: if one can't function without the other, linking ensures they fail together rather than leaving one in a broken state. ### Async / Await For simple "run these things concurrently and collect the results" patterns, `Std.Async` provides a higher-level API on top of actors: ```saga import Std.Actor (beam_actor) import Std.Async (Async, async_handler) fun run : Unit -> List Int needs {Async} run () = { let f1 = async! (fun () -> 1) let f2 = async! (fun () -> 2) let f3 = async! (fun () -> 3) Async.all [f1, f2, f3] } main () = { let results = run () with {async_handler, beam_actor} dbg results } ``` `async!` spawns a function in a new process and returns a `Future a`. `Async.all` awaits all futures and collects the results into a list. ### Timers The `Timer` effect provides delays and scheduled messages: ```saga import Std.Actor (Timer) # Pause the current process sleep! 1000 # Send a message to a pid after a delay let ref = send_after! pid 5000 Timeout # Cancel a pending timer cancel_timer! ref ``` ### Wiring It Up All BEAM concurrency effects are handled by a single handler: ```saga handler beam_actor for Process, Actor msg, Monitor, Link, Timer ``` Attach it at your program's entry point: ```saga main () = { run_app () } with beam_actor ``` ## Supervision *Supervision as a handler pattern. Restart strategies, backoff, and let-it-crash.* In most languages, supervision requires a framework or a special runtime construct. In Saga, supervision is a handler pattern: catch a failure, log it, and restart the computation. This falls directly out of the effect system with no special language support. ### The Basic Pattern A supervisor is a handler for `Fail` that restarts the computation instead of propagating the error: ```saga fun supervise : (Unit -> Unit needs {Fail, ..e}) -> Unit needs {..e} supervise f = f () with { fail reason = { dbg $"crashed: {reason}" supervise f } } ``` When the computation calls `fail!`, the handler catches it, logs the reason, and calls `supervise f` again. The computation restarts from scratch. Because the supervisor doesn't call `resume`, the failed computation is abandoned. A fresh invocation starts clean. ```saga fun unreliable_worker : Unit -> Unit needs {Fail} unreliable_worker () = { dbg "working..." fail! "something broke" } main () = supervise unreliable_worker # working... # crashed: something broke # working... # crashed: something broke # (loops forever) ``` ### Retry Limits An infinite restart loop is rarely what you want. Add a retry counter: ```saga fun supervise_n : Int -> (Unit -> a needs {Fail, ..e}) -> Result a String needs {..e} supervise_n 0 _ = Err "retries exhausted" supervise_n n f = { let result = f () with { fail reason = { dbg $"attempt failed: {reason}" supervise_n (n - 1) f } return value = Ok value } result } ``` After `n` failures, the supervisor gives up and returns `Err`. ### Using `Std.Supervisor` The standard library provides `supervised`, which wraps this pattern: ```saga import Std.Supervisor (supervised) fun unreliable : Unit -> Int needs {Fail String} unreliable () = fail! "something went wrong" main () = { let result = supervised 3 (fun () -> unreliable ()) dbg $"result: {debug result}" } # result: Err("something went wrong") ``` `supervised` catches failures and retries up to the given number of times. It returns `Ok value` on success or `Err reason` with the last failure if all retries are exhausted. ### Backoff Since supervision is just a function, adding backoff is straightforward. Use the `Timer` effect to add a delay between retries: ```saga import Std.Actor (Timer) fun supervise_backoff : Int -> Int -> (Unit -> a needs {Fail, ..e}) -> Result a String needs {Timer, ..e} supervise_backoff 0 _ _ = Err "retries exhausted" supervise_backoff n delay f = { let result = f () with { fail reason = { dbg $"failed: {reason}, retrying in {show delay}ms" sleep! delay supervise_backoff (n - 1) (delay * 2) f } return value = Ok value } result } # Usage: exponential backoff starting at 100ms, up to 5 retries supervise_backoff 5 100 my_worker ``` The delay doubles on each retry: 100ms, 200ms, 400ms, 800ms, 1600ms. Because the `Timer` effect is just another effect in the `needs` clause, no special integration is required. ### Let It Crash The BEAM's "let it crash" philosophy says: don't try to handle every possible error defensively. Instead, let processes fail and have a supervisor restart them. This works because BEAM processes are isolated. One crashing process cannot corrupt another's state. In Saga, this philosophy maps directly to effects: 1. Write your logic assuming everything works. Use `fail!` when something goes wrong. 2. Wrap it in a supervisor handler at the boundary. 3. The supervisor decides the restart policy. The business logic doesn't know or care. ```saga fun server_loop : Unit -> Unit needs {Fail, Actor Request, Process} server_loop () = { let req = receive { r -> r } let response = process_request req send! req.reply_to response server_loop () } main () = { supervise (fun () -> server_loop ()) } with beam_actor ``` If `process_request` fails, the supervisor restarts the loop. The requesting process gets no response (it should have its own timeout), but the server keeps running. ### Supervision and Resources When a supervised computation acquires resources, combine supervision with scoped cleanup to ensure resources are released on restart: ```saga fun worker : Unit -> Unit needs {Fail, Scope} worker () = { let db = acquire_scoped! (fun () -> connect "postgres") disconnect do_work db } main () = { supervise (fun () -> { worker () with run_scoped }) } ``` Each restart acquires a fresh connection, and each failure cleans up the old one through the `finally` block in `run_scoped`. The patterns compose because they are all just handlers. ## BitStrings *Binary construction, pattern matching, segment specifiers, and Std.BitString.* BitStrings are the BEAM's native binary data type. Saga provides syntax for constructing and pattern matching on binary data, following Erlang's bit syntax with a few simplifications. ### Construction Build a bitstring with `<< >>`: ```saga let bs = <<72, 101, 108, 108, 111>> # "Hello" as bytes let empty = <<>> ``` By default, each element is an 8-bit unsigned integer (a byte). For other sizes and types, add segment specifiers after a colon: ```saga # Explicit sizes <<1:8, 256:16/big>> # 1 byte + 2 bytes big-endian # Strings expand to their UTF-8 bytes <<1:8, 256:16/big, "hello">> # Endianness <> # big-endian (default) <> # little-endian # Float segments <> # 64-bit IEEE 754 # UTF-8 codepoints <> # Binary (variable-length) segments <> ``` #### Concatenation BitStrings support the `<>` operator: ```saga let a = <<1, 2, 3>> let b = <<4, 5, 6>> let combined = a <> b # <<1, 2, 3, 4, 5, 6>> ``` ### Pattern Matching BitString patterns use the same `<< >>` syntax in `case` expressions. This is where binary data handling gets powerful: ```saga case packet { <> -> process tag rest <<>> -> dbg "empty" _ -> dbg "no match" } ``` #### Segment specifiers in patterns Patterns support the same specifiers as construction: ```saga case data { <> -> { dbg $"tag: {tag}, length: {len}" dbg (debug payload) } _ -> dbg "no match" } ``` Notice that `len` is bound by an earlier segment and used as the size of `payload`. This variable-sized segment matching is one of the BEAM's most powerful features for parsing binary protocols. #### Parsing a binary protocol Here is a more complete example parsing a simple packet format with a tag byte, a 16-bit length, and a payload: ```saga import Std.BitString type Packet = | Data BitString | Ping | Unknown Int fun parse_packet : BitString -> Packet parse_packet bs = case bs { <<1:8, len:16/big, payload:len/binary>> -> Data payload <<2:8>> -> Ping <> -> Unknown tag _ -> Unknown 0 } ``` ### Stdlib Operations `Std.BitString` provides functions for working with bitstrings programmatically: ```saga import Std.BitString let bs = BitString.from_list [1, 2, 3] BitString.size bs # 3 BitString.to_list bs # [1, 2, 3] BitString.at 0 bs # Just 1 BitString.slice 1 2 bs # <<2, 3>> BitString.is_empty <<>> # True # Integer encoding BitString.encode_int 4 256 # <<0, 0, 1, 0>> (big-endian) BitString.decode_int <<0, 0, 1, 0>> # 256 # Little-endian variants BitString.encode_int_little 4 256 # <<0, 1, 0, 0>> BitString.decode_int_little <<0, 1, 0, 0>> # 256 # String conversion BitString.from_string "hello" # <<104, 101, 108, 108, 111>> BitString.to_string <<104, 101, 108, 108, 111>> # Ok "hello" ``` ## Refs *Atomic references for controlled mutable state. When to use refs vs recursion.* 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 ```saga 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: | Operation | Type | Description | |-----------|------|-------------| | `new!` | `a -> MutRef a` | Create a new ref with an initial value | | `get!` | `MutRef a -> a` | Read the current value | | `set!` | `MutRef a -> a -> Unit` | Overwrite the value | | `modify!` | `MutRef a -> (a -> a) -> a` | Read, 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. ```saga 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: ```saga 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: ```saga 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: ```saga 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`) ## Interop *Calling Erlang, Hex dependencies, git deps, and project.toml configuration.* Saga compiles to Core Erlang and runs on the BEAM, so it can call any Erlang or Elixir library directly. This page covers the FFI: the `@external` annotation, bridge files for type-mismatched conventions, and the limitations on effectful callbacks. For declaring and installing dependencies (Hex, git, path), see [Ecosystem](/guide/ecosystem). ### Calling Erlang Functions The `@external` annotation declares a function whose implementation lives in an Erlang module: ```saga @external("erlang", "lists", "reverse") pub fun reverse : List a -> List a ``` The three arguments are the target (always `"erlang"`), the Erlang module name, and the Erlang function name. A type signature is required. The compiler trusts it and emits a direct foreign call with no runtime validation. #### When it works directly If the Erlang function's argument and return types already match Saga's BEAM representations, no extra work is needed: ```saga @external("erlang", "erlang", "length") pub fun length : List a -> Int @external("erlang", "maps", "put") pub fun put : k -> v -> Dict k v -> Dict k v where {k: Eq} ``` This works for functions that take and return plain values (integers, floats, binaries, lists, maps), `{ok, V} | {error, E}` (matches `Result`), and `true | false` (matches `Bool`). #### Bridge files When an Erlang function's return convention doesn't match Saga's type representations, you write a bridge file: a `.erl` file that adapts between conventions. For example, Erlang returns `Value | undefined` for optional values, but Saga represents `Maybe` as `{just, V} | {nothing}`. A bridge converts between these: ```saga # Int.saga @external("erlang", "my_int_bridge", "parse") pub fun parse : String -> Maybe Int ``` ```erlang %% my_int_bridge.erl -module(my_int_bridge). -export([parse/1]). parse(S) -> case string:to_integer(S) of {N, []} -> {just, N}; _ -> {nothing} end. ``` Place `.erl` bridge files anywhere in your project root (excluding `_build/` and `tests/`). They are compiled alongside the generated Core Erlang files automatically. The `-module(name)` in the `.erl` file must match the module string in `@external`. #### Type representations Bridge functions must return values matching these BEAM representations: | Saga Type | BEAM Representation | Example | |-----------|-------------------|---------| | `Int` | Integer | `42` | | `Float` | Float | `1.5` | | `String` | Binary | `<<"hello">>` | | `Bool` | Atoms `true`/`false` | `true` | | `Unit` | Atom `unit` | `unit` | | `List a` | Erlang list | `[1, 2, 3]` | | `(a, b)` | Tuple | `{1, <<"hi">>}` | | `Ok v` | `{ok, V}` | `{ok, <<"data">>}` | | `Err e` | `{error, E}` | `{error, <<"fail">>}` | | `Just v` | `{just, V}` | `{just, 42}` | | `Nothing` | `{nothing}` | `{nothing}` | | Custom `Foo x` | `{module_Foo, X}` | `{shapes_Circle, 5}` | Note that `Err` maps to the atom `error` (not `err`), and `Unit` is the atom `unit` (not an empty tuple). Custom ADT constructors are prefixed with the module name in lowercase. #### Limitation: effectful callbacks Pure Saga functions can be passed across the FFI boundary and called from Erlang. But effectful functions (those with a `needs` clause) cannot. The compiler rewrites effectful functions into CPS form with extra hidden parameters, so an Erlang function that tries to call one will get an arity mismatch. If you need a "wrap a callback in setup/teardown" pattern (transactions, locks, resource handles), expose separate `acquire` and `release` primitives from the bridge and call the Saga callback from Saga code, where the effect machinery is available. See the `finally` pattern in [Handler Patterns](/guide/handler-patterns) for how this works in practice. # Reference ## Generic Deriving *Make your own traits derivable. The Generic representation, building-block instances, and the rules a derivable trait must follow.* Saga lets you write traits that users can derive on their own types, without modifying the compiler. If you're building a JSON library, a CSV codec, a database row mapper, or anything else where users would otherwise write tedious boilerplate, this is the mechanism you reach for. This guide is written for library authors. If you just want to _use_ derives on your own types, the [Traits](/guide/traits) page covers that side. ### The Payoff A user of your library writes this: ```saga import MyJsonLibrary (ToJson) record Person { name: String, age: Int } deriving (ToJson) main () = to_json (Person { name: "Alice", age: 30 }) ``` To make that work, you write a few small `impl`s (one per generic shape) plus instances for whatever primitive types you want users to be able to put in their fields. Using the Generic trait, we can accomplish this without macros, compiler patches, or a code generation step. ### How It Works At a high level, deriving has three moving parts: 1. The compiler converts the user's type into a structural `Rep`. 2. Your library provides trait impls for the generic building blocks. 3. The compiler synthesizes the user-facing impl by routing values to and from their `Rep`. The rest of this guide fills in the details of each step. ### The Generic Trait Every record and ADT can be mechanically translated into a small set of generic shapes. The compiler does this translation automatically for every type a user defines, producing a `Rep` type and an `impl Generic` that converts between the user's type and its Rep. There's no `deriving (Generic)` syntax. Every type already has a Generic representation available. #### The Building Blocks A Rep is built from these wrappers: - **`Leaf a`**: a single field's value (an `Int`, a `String`, a nested record). - **`Labeled (n : Symbol) a`**: a record field. The name `n` lives at the type level; the value is the only runtime data. - **`Variant (n : Symbol) a`**: a sum constructor. The constructor name `n` lives at the type level; the payload is the only runtime data. - **`And l r`**: two things side by side (multiple fields in a record, multiple arguments to a constructor). - **`Or l r`**: a choice between two things (which variant of a sum type). - **`Record a`**: the outer frame around a whole record. Carries the runtime type name as a `String`. - **`Adt a`**: the outer frame around a whole sum type. Carries the runtime type name as a `String`. - **`U1`**: the empty case (a constructor with no arguments, like `Triangle` or `Nothing`). #### A Concrete Rep For `record Point { x: Int, y: Int }`, the structural shape (type-level) is: ```saga Record (And (Labeled 'x (Leaf Int)) (Labeled 'y (Leaf Int))) ``` Field names live at the _type_ level as symbol literals (`'x`, `'y`). At runtime, the value the compiler produces for `Point { x: 3, y: 4 }` is: ```saga Record "Point" (And (Labeled (Leaf 3)) (Labeled (Leaf 4))) ``` Read outside in: it's a record (with a runtime type name), containing two things joined by `And`, each a labeled field wrapping a leaf value. The field names (`x`, `y`) aren't in the runtime value — they're in the type, available to library code via type-class reflection. #### Type-level names The `n` parameter on `Labeled` and `Variant` has kind `Symbol`, a kind for type-level interned names. Symbols are written `'name` in Saga source — `'admin`, `'x`, `'first_name` are all valid symbol literals. A library impl that needs the runtime string for a symbol takes a `KnownSymbol` constraint and uses `symbol_name`: ```saga impl ToJson for Labeled n a where {n: KnownSymbol, a: ToJson} { to_json (Labeled value) = { let name = symbol_name (Proxy : Proxy n) "\"" <> name <> "\": " <> to_json value } } ``` `Proxy n` is a phantom type whose only purpose is to carry `n` to the call site; `symbol_name` reads off the source name and returns it as a `String`. This pattern — names at the type level, reflected when needed — is what makes sum-type from-direction derives correct. The library compares the expected symbol against the input's tag and selects the right variant. (See "From-Direction Traits" below.) #### What This Means For You A library only has to implement your trait for these eight public building blocks. The compiler handles the generated per-type `Rep__...` wrappers itself — you never write impls for those. `Generic` itself is a two-parameter trait relating a user type to its structural representation. You'll see `Generic Person r` in generated code; read it as "`Person`'s Rep is `r`." ### Designing Your Trait The derive machinery has to either _consume_ the user type (walk it apart, field by field) or _produce_ one (assemble it from input), but not both. Two rules follow from that: **Each method has the user type on the parameter side or the return side, but not both.** Methods that mention `a` only in their parameters are to-direction; methods that mention `a` only in their return type are from-direction. Multiple occurrences on the same side are fine, so `fun compare : a -> a -> Ordering` and `fun decode : Input -> (a, a)` both derive. The compiler decides per method by inspecting the signature, so a single trait can freely mix the two. A `JsonCodec` with both `encode : a -> String` and `decode : String -> Result a Error` derives in one go. A method with `a` on both sides (`fun roundtrip : a -> a`) or with `a` on neither side (`fun zero : Unit -> Int`) doesn't fit; the derive aborts with a diagnostic naming the offending method. **From-direction return types put `a` at a direct field position.** The compiler inspects the wrapper's structure, finds positions where the user type `a` appears, and threads the conversion through them. So all of these work: - `a` (bare: `fun decode : Input -> a`) - `Result a e` for any error type `e` - `Maybe a` - Any user-defined wrapper whose `TypeDef` is in scope. Examples: a three-state result type `DbResult a = DbOk a | DbErr DbError | DbNoRows`, a validation type `Validated e a = Valid a | Invalid (List e)`, or a record wrapper `Boxed a = { value: a, meta: String }`. "Direct field position" means `a` has to appear as one of the constructor's own fields, not buried inside another type and not as a phantom parameter. A few concrete failures: - **Non-leaf `a`.** `Wrapped a = Yep (List a) | Nope` doesn't fit because `a` lives inside `List`. The compiler can't thread the conversion through an arbitrary container. - **Phantom `a`.** `Schema a = Schema String` mentions `a` in the type parameter list but no constructor carries an actual `a` field. The compiler reports there's "nothing for `from` to thread through." - **Opaque wrappers.** If the wrapper's `TypeDef` isn't visible (defined in another module without exposed constructors), the compiler can't inspect its structure. If your trait doesn't fit, users can still write `impl YourTrait for ...` by hand. Deriving is a convenience for the common shape, not the only path. ### Worked Example: A ToJson Library The trait and primitives: ```saga trait ToJson a { fun to_json : a -> String } impl ToJson for Int { to_json n = show n } impl ToJson for Bool { to_json b = if b then "true" else "false" } impl ToJson for String { to_json s = "\"" <> s <> "\"" } ``` The JSON rendering here is deliberately minimal to keep the example small. A real library would escape strings, handle Unicode, and emit valid JSON for nested structures. Now the building-block instances. These are what make the trait derivable: ```saga impl ToJson for U1 { to_json U1 = "" } impl ToJson for Leaf a where {a: ToJson} { to_json (Leaf x) = to_json x } impl ToJson for Labeled n a where {n: KnownSymbol, a: ToJson} { to_json (Labeled value) = { let name = symbol_name (Proxy : Proxy n) "\"" <> name <> "\": " <> to_json value } } impl ToJson for And l r where {l: ToJson, r: ToJson} { to_json (And l r) = to_json l <> ", " <> to_json r } impl ToJson for Or l r where {l: ToJson, r: ToJson} { to_json (Or_Left l) = to_json l to_json (Or_Right r) = to_json r } impl ToJson for Variant n a where {n: KnownSymbol, a: ToJson} { to_json (Variant payload) = { let name = symbol_name (Proxy : Proxy n) "{\"" <> name <> "\": " <> to_json payload <> "}" } } impl ToJson for Record a where {a: ToJson} { to_json (Record _ inner) = "{" <> to_json inner <> "}" } impl ToJson for Adt a where {a: ToJson} { to_json (Adt _ inner) = to_json inner } ``` That's the library. Users can now derive `ToJson` on any record or ADT whose fields are themselves `ToJson`. Note that the user wrote `deriving (ToJson)` without mentioning `Generic`! `Generic` is implied automatically whenever a user-defined derive is requested. `Record` and `Adt` are top-level framing wrappers. They give your codec a place to emit per-type prefixes/suffixes (here, the outer JSON braces). `Variant` carries the constructor name at the type level, which lets you render `Some 7` as `{"Some": 7}` while record fields (which use `Labeled`) render as `"name": "Alice"`. ### Under the Hood When the user writes `deriving (ToJson)` on `Person`, the compiler generates the following decls invisibly: ```saga # The structural representation: a single-variant wrapper type type Rep__Person = Rep__Person (Record (And (Labeled 'name (Leaf String)) (Labeled 'age (Leaf Int)))) # How to convert a Person to/from its Rep impl Generic Person Rep__Person { to p = Rep__Person (Record "Person" (And (Labeled (Leaf p.name)) (Labeled (Leaf p.age)))) from (Rep__Person (Record _ (And (Labeled (Leaf n)) (Labeled (Leaf a))))) = { name: n, age: a } } # Bridge: ToJson for the Rep type unwraps and forwards impl ToJson for Rep__Person { to_json (Rep__Person inner) = to_json inner } # Delegating: ToJson for Person goes through Generic impl ToJson for Person where {Generic Person r, ToJson r} { to_json p = to_json (to p) } ``` When `to_json alice` is called: 1. The delegating impl converts `alice` to its Rep, wrapped in `Record "Person" (...)`. 2. The bridge impl unwraps the `Rep__Person` newtype. 3. Your `ToJson for Record` adds the outer `{`/`}` and recurses. 4. Your `ToJson for And` walks the inner tree, calling `ToJson for Labeled` on each piece. 5. Each `Labeled` impl reflects its symbol parameter via `KnownSymbol` to recover the field name (`"name"`, `"age"`) and renders it. The same handful of impls handle every user record and ADT. ### ADTs and Variants Sum types fold into `Or` chains, with each variant wrapped in `Variant` to carry the constructor name at the type level: ```saga type Shape = | Circle Float | Rect Float Float | Triangle deriving (ToJson) ``` Schematically, the Rep looks like: ```saga Adt (Or (Variant 'Circle (Leaf Float)) (Or (Variant 'Rect (And (Leaf Float) (Leaf Float))) (Variant 'Triangle U1))) ``` The runtime value carries the type name `"Shape"` inside `Adt` and the chosen variant's payload (no constructor name at the value level — that lives in the type as the `Symbol` parameter on `Variant`). Your `Adt` instance wraps the whole tree (you'll usually pass through, but the type name is there if you want it); your `Or` instance gets called twice (once per nesting level); your `Variant` instance reflects the symbol via `KnownSymbol` to get the constructor name as a `String`; `And` handles multi-field variants; `U1` is the empty contribution for nullary constructors like `Triangle`. Compare this to records, which use `Record` (outer) and `Labeled` (inner). That split is why your codec can give record fields and sum constructor names different output formats. ### From-Direction Traits Traits that _produce_ the user type from some input (parsing, deserialization, database row decoding) work the same way: write instances for the building blocks, deriving handles the rest. A `FromJson` trait pairs naturally with the `ToJson` example above: ```saga trait FromJson a { fun from_json : String -> Result a JsonError } ``` Most of the impls are mechanical mirrors of their to-direction counterparts — `Leaf` wraps a decoded value, `And` decodes both sides, `Record` and `Adt` pass through. The one subtle case is sum-type decoding, which is worth calling out: ```saga # Variant compares its expected constructor name against the input's tag # and fails if they don't agree. impl FromJson for Variant n a where {n: KnownSymbol, a: FromJson} { from_json input = { let expected = symbol_name (Proxy : Proxy n) case parse_tag input { Ok (actual, payload) when actual == expected -> from_json payload |> Result.map Variant Ok _ -> Err (JsonError $"expected variant '{expected}'") Err e -> Err e } } } # Or tries the left branch; if it rejects, falls back to the right. impl FromJson for Or l r where {l: FromJson, r: FromJson} { from_json input = case from_json input { Ok l -> Ok (Or_Left l) Err _ -> from_json input |> Result.map Or_Right } } ``` This pair is what makes sum-type decoding correct. Each `Variant` impl knows its expected constructor name from its type-level `Symbol` and rejects mismatched input. `Or` walks the variant chain by trying the left branch and falling back on failure — and because branches genuinely fail on tag mismatch, the right branch gets a real chance to match. If the constructor name lived at the value level instead, the synthesized `from` would have to wildcard it and `Or` would always pick the first branch regardless of input. The type-level representation fixes that structurally. ### Recursive Types Recursive types Just Work: ```saga type IntList = Nil | Cons Int IntList deriving (ToJson) main () = { let xs = Cons 1 (Cons 2 (Cons 3 Nil)) dbg (to_json xs) } ``` The generated Rep is: ```saga Adt (Or (Variant 'Nil U1) (Variant 'Cons (And (Leaf Int) (Leaf IntList)))) ``` Notice the recursive position: `Leaf IntList`, not an unfolded `Or ...`. The Rep stops at the type boundary and trusts the `ToJson IntList` instance to handle it from there. That dispatch happens at runtime through the normal trait machinery, so library authors don't need to do anything special. The building-block impls already written above handle this case. ### Parameterized Types Type parameters in the user's type propagate naturally through the Rep: ```saga record Box a { value: a } deriving (ToJson) ``` The generated impl is roughly: ```saga impl ToJson for Box a where {a: ToJson, Generic (Box a) r, ToJson r} ``` The user gets `to_json (Box { value: 42 })` for free as long as `ToJson Int` exists. ### Customizing Output Each building block is a hook for a specific kind of formatting: - **`Record a`**: top-level wrapper on records. Outer framing (e.g. JSON `{}`, an XML element, a SQL row tuple). Carries the runtime type name. - **`Adt a`**: top-level wrapper on ADTs. Same idea for sum types; often a passthrough, sometimes you want type-name-aware output here. - **`Labeled (n : Symbol) a`**: record field. The name `n` lives at the type level; reflect it via `KnownSymbol` to format key/value pairs. - **`Variant (n : Symbol) a`**: sum constructor. Same reflection pattern as `Labeled`. Distinct from `Labeled`, so you can render `"name": "Alice"` for fields and `{"Some": 7}` for variants without conflict. - **`And l r` / `Or l r`**: joiners. Separators between fields, branches between variants. If you don't care about a particular hook, write a one-line passthrough: ```saga impl ToJson for Adt a where {a: ToJson} { to_json (Adt _ inner) = to_json inner } ``` ### When To Use Hand-Written Impls Instead Reach for hand-written `impl YourTrait for SpecificType` when: - You need to rename or skip specific fields (no attribute system exists yet for per-field configuration). - The type doesn't fit the supported shapes (functions, opaque types, third-party types you can't add `deriving` to). - Performance matters and you want to avoid the routing overhead. ### Symbols Beyond Derive The `Symbol` kind isn't just for derive impls — it's a general type-level tagging mechanism. The canonical use pairs a symbol-parameterized wrapper with [type aliases](/guide/types#type-aliases) to give many roles distinct identities without one wrapper per role: ```saga type Id (k : Symbol) = Id Int type alias UserId = Id 'user type alias PostId = Id 'post let u : UserId = Id 1 let p : PostId = Id 2 # let bad : UserId = (p : PostId) # rejected — 'user ≠ 'post ``` `Id 'user` and `Id 'post` are distinct types because the symbols differ, even though both wrap an `Int`. At runtime there's no overhead beyond the single-constructor wrapper — the symbol is erased. Reach for this when several "primitive-flavored" types should not be mixed (user IDs, post IDs, session tokens, currency codes) and one underlying representation serves many semantic roles. ### Summary To make your trait derivable: 1. Every method has the user type on the parameter side or the return side, but not both. 2. From-direction methods return `a` directly, or any wrapper type (`Result a e`, `Maybe a`, your own `DbResult a`, etc.) with `a` at a direct field position. 3. Provide instances for `U1`, `Leaf a`, `Labeled n a`, `Variant n a`, `And l r`, `Or l r`, `Record a`, and `Adt a` (from `Std.Generic`). The `Labeled` and `Variant` impls take a `KnownSymbol` bound on `n` to recover the type-level name via `symbol_name (Proxy : Proxy n)`. 4. Provide instances for the primitive types you want users to be able to include as fields (`Int`, `String`, `Bool`, etc.). Users then write `deriving (YourTrait)` on their types, and the compiler generates everything needed to wire it up. # Worked Examples ## Hello World *The simplest Saga program.* The simplest Saga program. A `main` function takes `Unit` and prints a greeting. ```saga main () = { dbg "Hello, world!" } ``` Output: ``` Hello, world! ``` ## Binary Search Tree *A functional BST with insert, contains, and in-order traversal using ADTs and recursion.* A simple binary search tree using an ADT and recursion. ### The tree type A tree is either empty or a node with a left subtree, a value, and a right subtree: ```saga type Tree = | Empty | Node Tree Int Tree ``` ### Insert and lookup Both functions recurse down the tree, branching left or right based on comparison: ```saga insert t x = case t { Empty -> Node Empty x Empty Node l v r -> if x < v then Node (insert l x) v r else if x > v then Node l v (insert r x) else t } contains t x = case t { Empty -> False Node l v r -> if x == v then True else if x < v then contains l x else contains r x } ``` ### In-order traversal Walking the tree in order produces a sorted list: ```saga to_list t = case t { Empty -> [] Node l v r -> List.append (to_list l) (v :: to_list r) } ``` ### Putting it together ```saga main () = { let t = List.foldl insert Empty [5, 3, 7, 1, 4, 6, 8] to_list t |> dbg contains t 4 |> dbg contains t 9 |> dbg } ``` Output: ``` [1, 3, 4, 5, 6, 7, 8] True False ``` ### Full source ```saga type Tree = | Empty | Node Tree Int Tree insert t x = case t { Empty -> Node Empty x Empty Node l v r -> if x < v then Node (insert l x) v r else if x > v then Node l v (insert r x) else t } contains t x = case t { Empty -> False Node l v r -> if x == v then True else if x < v then contains l x else contains r x } to_list t = case t { Empty -> [] Node l v r -> List.append (to_list l) (v :: to_list r) } main () = { let t = List.foldl insert Empty [5, 3, 7, 1, 4, 6, 8] to_list t |> dbg contains t 4 |> dbg contains t 9 |> dbg } ``` ## Validation with Error Accumulation *Collect all validation errors instead of stopping at the first one, using continuations.* Most validation stops at the first error. This example collects all errors using a `Validate` effect with a continuation-based handler that accumulates failures instead of aborting. ### The Validate effect A single operation: report that something is invalid. ```saga effect Validate { fun invalid : String -> Unit } ``` Note that `invalid` returns `Unit`, not `a`. The computation continues after each validation error, which is the key difference from `Fail`. ### Validation helpers Plain functions that perform the effect: ```saga fun require_non_empty : String -> String -> Unit needs {Validate} require_non_empty label value = if value == "" then invalid! $"{label} is required" else () fun require_min_age : Int -> Int -> Unit needs {Validate} require_min_age min age = if age < min then invalid! $"age must be at least {min}" else () ``` ### The collecting handler This is where it gets interesting. The handler calls `resume` after every `invalid!`, letting the computation continue. But it captures the error and prepends it to the error list returned alongside the value: ```saga handler collecting for Validate { invalid err = { let (v, errs) = resume () (v, err :: errs) } return v = (v, []) } ``` The `return` clause wraps the success value in a tuple with an empty error list. Each `invalid!` call adds to that list as the continuation unwinds. The result is always `(value, errors)` where `errors` is a list of all validation failures. ### Using it ```saga record UserInput { name: String, age: Int } record ValidUser { name: String, age: Int } type Validation e a = | Valid a | Invalid e fun validate_user : UserInput -> Validation (List String) ValidUser validate_user input = { let (v, errs) = { require_non_empty "name" input.name require_min_age 18 input.age ValidUser { name: input.name, age: input.age } } with collecting case errs { [] -> Valid v _ -> Invalid errs } } ``` ### Running it ```saga main () = { validate_user (UserInput { name: "Alice", age: 25 }) |> dbg validate_user (UserInput { name: "", age: 25 }) |> dbg validate_user (UserInput { name: "Bob", age: 15 }) |> dbg validate_user (UserInput { name: "", age: 15 }) |> dbg } ``` Output: ``` Valid: ValidUser { name: "Alice", age: 25 } Invalid: ["name is required"] Invalid: ["age must be at least 18"] Invalid: ["name is required", "age must be at least 18"] ``` The last case is the payoff: both errors are reported, not just the first one. ### Full source ```saga effect Validate { fun invalid : String -> Unit } fun require_non_empty : String -> String -> Unit needs {Validate} require_non_empty label value = if value == "" then invalid! $"{label} is required" else () fun require_min_age : Int -> Int -> Unit needs {Validate} require_min_age min age = if age < min then invalid! $"age must be at least {min}" else () record UserInput { name: String, age: Int } record ValidUser { name: String, age: Int } deriving (Debug) type Validation e a = | Valid a | Invalid e impl Debug for Validation e a where {e: Debug, a: Debug} { debug v = case v { Valid x -> $"Valid: {debug x}" Invalid e -> $"Invalid: {debug e}" } } handler collecting for Validate { invalid err = { let (v, errs) = resume () (v, err :: errs) } return v = (v, []) } fun validate_user : UserInput -> Validation (List String) ValidUser validate_user input = { let (v, errs) = { require_non_empty "name" input.name require_min_age 18 input.age ValidUser { name: input.name, age: input.age } } with collecting case errs { [] -> Valid v _ -> Invalid errs } } main () = { let print_result = validate_user >> debug >> dbg print_result (UserInput { name: "Alice", age: 25 }) print_result (UserInput { name: "", age: 25 }) print_result (UserInput { name: "Bob", age: 15 }) print_result (UserInput { name: "", age: 15 }) } ``` ## File I/O *Reading, writing, and deleting files with the File effect and ADT error types.* Reading, writing, and deleting files using the `File` effect. Errors are represented as an ADT (`FileError`), not strings, so you can match on specific failure cases. ```saga import Std.File (File, FileError, fs) fun process : Unit -> Unit needs {File} process () = { let result = File.write! "test_output.txt" "hello from saga!" case result { Ok () -> () Err _ -> dbg "write failed" } let result = File.read! "test_output.txt" case result { Ok content -> dbg content Err e -> case e { File.NotFound -> dbg "file not found" File.PermissionDenied -> dbg "permission denied" _ -> dbg "other error" } } let _ = File.delete! "test_output.txt" dbg $"file exists after delete?: {File.exists! "test_output.txt"}" let missing = File.read! "nonexistent.txt" case missing { Ok _ -> dbg "unexpected" Err File.NotFound -> dbg "correctly got NotFound" Err e -> dbg $"wrong error: {debug e}" } } main () = process () with fs ``` Output: ``` hello from saga! file exists after delete?: False correctly got NotFound ``` The `fs` handler connects the `File` effect to the real filesystem. In tests, you could provide a handler that operates on an in-memory map instead. ## Async Tasks *Spawning concurrent tasks and collecting results with the Async effect.* Saga's standard library provides an async/await API, but it required no special language support to build. The entire `Async` module is implemented using effects and the actor system: ```saga module Std.Async import Std.Actor (Process, Actor) opaque type Future a = Future(Pid a) pub effect Async { fun async : (f: Unit -> a) -> Future a fun await : (future: Future a) -> a } pub handler async_handler for Async needs {Process, Actor a} { async f = { let caller = self! () let pid = spawn! (fun () -> { let result = f () send! caller result }) resume Future pid } await _future = { receive { value -> resume value } } } pub fun all : (futures: List (Future a)) -> List a needs {Async} all futures = case futures { [] -> [] f :: rest -> await! f :: all rest } ``` That's the whole thing. `async!` spawns a BEAM process that runs the function and sends the result back. `await!` receives the result. No new language features, just effects and actors. ### Using it `async!` spawns a task and returns a `Future`. The task runs in parallel immediately. `await!` blocks until the result is ready: ```saga import Std.Actor (beam_actor) import Std.Async (Async, async_handler) fun run : Unit -> Int needs {Async} run () = { let future = async! (fun () -> expensive_computation ()) # ... do other work while the task runs ... await! future } ``` To fire off multiple tasks in parallel and collect all results, use `Async.all`: ```saga fun run_all : Unit -> List Int needs {Async} run_all () = { let f1 = async! (fun () -> 1) let f2 = async! (fun () -> 2) let f3 = async! (fun () -> 3) Async.all [f1, f2, f3] } main () = { let results = run_all () with {async_handler, beam_actor} dbg results } ``` Output: ``` [1, 2, 3] ``` ### Handling failures Since effects compose, async tasks can use `Fail` the same way synchronous code does. Handle it inside the task to get back a `Result`: ```saga fun fetch_user : Int -> User needs {Http, Fail} fetch_user id = { let resp = get! (Request $"/api/users/{id}") if resp.status == 404 then fail! $"user {id} not found" else parse_user resp.body } fun fetch_users : List Int -> List (Result User String) needs {Async, Http} fetch_users ids = { let futures = List.map (fun id -> async! (fun () -> fetch_user id with to_result) ) ids List.map (fun f -> await! f) futures } ``` Each task runs in parallel and independently succeeds or fails. You can also let failures propagate, like an unhandled promise rejection. Here `fetch_both` doesn't handle `Fail` itself, so it stays in its `needs`: ```saga fun fetch_both : Unit -> (User, User) needs {Async, Http, Fail} fetch_both () = { let f1 = async! (fun () -> fetch_user 1) let f2 = async! (fun () -> fetch_user 2) (await! f1, await! f2) } ``` The caller decides how to handle the failure. Wrapping the whole thing in `to_result` means if either fetch fails, the entire result is `Err`: ```saga let result = fetch_both () with {async_handler, beam_actor, http, to_result} # Ok (user1, user2) or Err "user 2 not found" ``` The difference is just where you place `to_result`: inside the task for per-task error handling, outside for fail-fast. ## Supervisor *Automatic retry with a limit using the supervised handler pattern.* Automatic retry with a limit using the `supervised` function from `Std.Supervisor`. If the computation fails, it restarts up to N times before giving up. ```saga import Std.Fail (Fail) import Std.Result import Std.Supervisor (supervised) fun unreliable : Unit -> Int needs {Fail String} unreliable () = fail! "something went wrong" main () = { let r1 = supervised 3 (fun () -> unreliable ()) dbg $"unreliable: {debug r1}" } ``` Output: ``` unreliable: Err("something went wrong") ``` `supervised 3` runs the function and retries up to 3 times on failure. If all attempts fail, it returns the last error as an `Err`. If any attempt succeeds, it returns `Ok` with the value. This is built on the same effect handler patterns described in [Supervision](/beam/supervision). The `supervised` function is a convenience wrapper around a handler that catches `Fail` and re-runs the computation. ## N-Queens Solver *Backtracking search using Choose + Fail effects with multishot continuations.* This example solves the N-Queens problem using two effects: `Choose` for nondeterministic search and `Fail` for pruning invalid placements. The idea: place N queens on an NxN board so no two threaten each other. Each queen goes in its own row, so we just need to choose a column per row. The `Choose` handler explores all possibilities and `Fail` prunes the dead ends. ### Effects We define two effects. `Choose` picks an element from a list nondeterministically, and `Fail` aborts a branch of the search: ```saga effect Choose { fun choose : List a -> a } effect Fail { fun fail : String -> a } ``` ### The handler `all_solutions` is where the magic happens. When `choose` is called, the handler resumes _once for each option_ and collects the results with `flat_map`. When `fail` is called, it returns an empty list (no solutions down this path). A successful computation wraps its value in a singleton list: ```saga handler all_solutions for Choose, Fail { choose options = List.flat_map (fun x -> resume x) options fail _ = [] return value = [value] } ``` This is a multi-shot continuation: `resume` is called multiple times in the `choose` arm, once per option. The handler effectively turns a sequential program into a backtracking search. ### Safety check A helper that checks whether placing a queen at a given column conflicts with any already-placed queen. `placed` is a list of columns for rows already filled, bottom to top: ```saga fun guard : Bool -> Unit needs {Fail} guard True = () guard False = fail! "pruned" fun safe : (col: Int) -> (placed: List Int) -> Bool safe col placed = { let rows_back = List.range 1 (List.length placed) let checks = List.zip placed rows_back List.all (fun pair -> { let (c, dist) = pair c != col && Int.abs (c - col) != dist }) checks } ``` ### Solver The solver places one queen per row. It chooses a column, checks if it's safe, and recurses. The effects handle the backtracking automatically: ```saga fun solve : (n: Int) -> (placed: List Int) -> List Int needs {Choose, Fail} solve n placed = if List.length placed == n then List.reverse placed else { let col = choose! (List.range 1 n) guard (safe col placed) solve n (col :: placed) } fun queens : (n: Int) -> List (List Int) queens n = solve n [] with all_solutions ``` ### Running it ```saga main () = { println "4-Queens solutions:" let solutions = queens 4 List.iter dbg solutions println $"Found {List.length solutions} solutions" let eight = queens 8 println $"8-Queens: found {List.length eight} solutions" } with {console} ``` Output: ``` 4-Queens solutions: [2, 4, 1, 3] [3, 1, 4, 2] Found 2 solutions 8-Queens: found 92 solutions ``` ### Full source ```saga effect Choose { fun choose : List a -> a } effect Fail { fun fail : String -> a } handler all_solutions for Choose, Fail { choose options = List.flat_map (fun x -> resume x) options fail _ = [] return value = [value] } fun guard : Bool -> Unit needs {Fail} guard True = () guard False = fail! "pruned" fun safe : (col: Int) -> (placed: List Int) -> Bool safe col placed = { let rows_back = List.range 1 (List.length placed) let checks = List.zip placed rows_back List.all (fun pair -> { let (c, dist) = pair c != col && Int.abs (c - col) != dist }) checks } fun solve : (n: Int) -> (placed: List Int) -> List Int needs {Choose, Fail} solve n placed = if List.length placed == n then List.reverse placed else { let col = choose! (List.range 1 n) guard (safe col placed) solve n (col :: placed) } fun queens : (n: Int) -> List (List Int) queens n = solve n [] with all_solutions main () = { println "4-Queens solutions:" let solutions = queens 4 List.iter dbg solutions println $"Found {List.length solutions} solutions" let eight = queens 8 println $"8-Queens: found {List.length eight} solutions" } with {console} ``` ## Ambient Context *Request-scoped values and audit logs as effects, replacing argument plumbing and the Reader/Writer monads.* A common pain point in functional code: threading "request-scoped" values — the current user, a request ID, feature flags — through every layer of the call tree. Effects let you declare those values as ambient capabilities instead. Deep functions read them via `!`, the request boundary installs a handler once, and the type system still tracks the dependency through `needs`. This is the classic Reader/Writer monad pattern, expressed as algebraic effects. We'll show both: an ambient _source_ (Reader) for pulling values in, and an ambient _sink_ (Writer) for pushing events out — and why the reader version is often clearer when you write it yourself, while the writer is better off using `Std.Control`. ### Reader: ambient read-only context The reader pattern lets a deep function pull request-scoped values without having them passed as arguments. We'll build a small checkout flow where pricing depends on the current user's feature flags. ```saga import Std.Set record User { id: Int, name: String, } record Request { id: String, user: User, flags: Set String, } effect RequestContext { fun current_user : Unit -> User fun request_id : Unit -> String fun feature_enabled : (name: String) -> Bool } ``` `Std.Control` provides a generic `Reader r` effect with a single `ask` operation, but defining our own with named operations reads better. With a generic `Reader Request`, deep functions would write `(ask! ()).user` — reaching into the environment each time. With a domain-specific effect, they write `current_user! ()` — the operation name carries the meaning. The handler is also free to pull each value from anywhere (the request, env vars, a config service) without callers needing to change. #### Pure pricing helpers Plain functions, no effects: ```saga fun new_price : String -> Int new_price "book" = 12 new_price "pen" = 2 new_price _ = 5 fun old_price : String -> Int old_price "book" = 15 old_price "pen" = 3 old_price _ = 7 ``` #### Deep in the call tree These functions never receive the user or feature flags as arguments. They declare `needs {RequestContext}` and call the operations directly: ```saga fun price_for : (item: String) -> Int needs {RequestContext} price_for item = if feature_enabled! "new_pricing" then new_price item else old_price item fun cart_total : List String -> Int needs {RequestContext} cart_total [] = 0 cart_total (item :: rest) = price_for item + cart_total rest fun checkout : (cart: List String) -> String needs {RequestContext} checkout cart = { let user = current_user! () let rid = request_id! () let total = cart_total cart $"[req={rid}] {user.name} checked out {List.length cart} items, total {total}" } ``` `price_for` doesn't know what a `Request` is. `checkout` doesn't take a user as input. They just ask for what they need, when they need it. #### The request boundary The handler supplies each operation by `resume`-ing with a value closed over from `req`: ```saga fun handle : (req: Request) -> String handle req = { checkout ["book", "pen", "mug"] } with { current_user () = resume req.user request_id () = resume req.id feature_enabled name = resume Set.member name req.flags } ``` That's it. The handler is installed once, at the boundary, and every deeper call of `current_user! ()` returns whatever the handler chose to provide. #### Running it ```saga main () = { let alice_req = Request { id: "req-001", user: User { id: 1, name: "alice" }, flags: Set.from_list ["new_pricing"], } let bob_req = Request { id: "req-002", user: User { id: 2, name: "bob" }, flags: Set.new (), } handle alice_req |> dbg handle bob_req |> dbg } ``` Output: ``` [req=req-001] alice checked out 3 items, total 19 [req=req-002] bob checked out 3 items, total 25 ``` Same checkout function, two different request contexts, no argument plumbing. Alice gets the new pricing because her request's flags include `"new_pricing"`; Bob gets the legacy path. ### Writer: ambient append-only output The dual problem: deep functions need to _emit_ events — audit logs, metrics, trace spans — without folding extra values into every return type. The `Tell` effect from `Std.Control` provides this: ```saga import Std.Control (Tell, run_writer) record Event { step: String, detail: String, } deriving (Debug) fun reserve_inventory : (item: String) -> Unit needs {Tell Event} reserve_inventory item = tell! (Event { step: "inventory", detail: $"reserved 1x {item}" }) fun charge_card : (amount: Int) -> Unit needs {Tell Event} charge_card amount = tell! (Event { step: "billing", detail: $"charged {amount} cents" }) fun ship_order : (item: String) -> Unit needs {Tell Event} ship_order item = tell! (Event { step: "shipping", detail: $"queued {item} for shipment" }) ``` Each function declares `needs {Tell Event}` and pushes events via `tell!` without changing its return type to `(Result, List Event)` or similar. ```saga fun place_order : (item: String) -> (price: Int) -> String needs {Tell Event} place_order item price = { reserve_inventory item charge_card price ship_order item $"order placed: {item}" } ``` `place_order` returns a plain `String`. The audit trail rides alongside via the effect, not in the return type. #### Recovering the log `run_writer` runs the computation and returns a tuple of the result and the accumulated log: ```saga main () = { let (result, audit) = run_writer (fun () -> place_order "book" 1299) dbg result dbg $"audit trail ({List.length audit} events):" List.iter dbg audit } ``` Output: ``` order placed: book audit trail (3 events): Event { step: "inventory", detail: "reserved 1x book" } Event { step: "billing", detail: "charged 1299 cents" } Event { step: "shipping", detail: "queued book for shipment" } ``` #### Why we used `Std.Control` here The reader's handler is trivial — every arm just `resume`s with a closed-over value. But the writer's handler involves continuation logic that's easy to get wrong: each `tell!` has to accumulate into a list as the continuation unwinds, and the `return` clause has to seed the final tuple. That's exactly the kind of thing the standard library should handle: ```saga fun run_writer : (f: Unit -> a needs {Tell w}) -> (a, List w) ``` If you ever need a writer with non-list accumulation (a monoid sum, a deduplicating set), you'd write your own variant. For a chronological log, `run_writer` is the right reach. ### A note on multiple instances Saga's effects dispatch by operation name. `Reader r` and `Tell w` from `Std.Control` each expose a single operation (`ask` and `tell`), and there's no way to disambiguate two instances in the same scope: `needs {Reader Config, Reader User}` would leave `ask!` with no way to pick which one to call. The generic forms only fit the single-instance case. When you need multiple readers or multiple write streams, the workaround is the same pattern this recipe uses for the reader half: define domain-specific effects with uniquely named operations. ```saga # Instead of multiple writers of the same effect type... needs {Tell LogEntry, Tell Metric, Tell AuditEvent} # ...define separate effects: effect Log { fun log : String -> Unit } effect Metric { fun emit : Metric -> Unit } effect Audit { fun audit : Event -> Unit } ``` You give up the reusability of `run_writer` — each handler has to redo the accumulator threading — but the trade is straightforward and inevitable until named handler instances land. ### When to roll your own vs reach for `Std.Control` These two patterns illustrate a general guideline: - **Roll your own effect when the value is domain-specific.** Multiple related operations, named by what they mean (`current_user`, `feature_enabled`, `request_id`). The handler is usually a few lines of `resume`-with-a-value. - **Reach for `Std.Control` when you want a single generic operation with non-trivial handler logic.** Writer is the clearest case — the accumulator threading is fiddly enough that you don't want to redo it. The two approaches compose freely. A project can use a custom `RequestContext` for reading values _and_ a `Tell Event` for emitting them in the same handled scope, with both effects listed in `needs`. ### Compared to monad transformers In Haskell, the equivalent program would build a transformer stack: `ReaderT Config (WriterT [Event] (ExceptT String IO)) a`, with explicit `lift . lift . ask` to reach into the right layer. Saga still stacks handlers lexically, and order matters because the innermost handler intercepts first. What you don't do is stack at the _type_ level. Function signatures use a flat, order-independent effect set: ```saga fun process : Request -> Response needs {Reader Config, Tell Event, Fail String} ``` No `lift`, no transformer ordering puzzle, no nested type tower. Adding a fourth effect is just adding it to `needs` and providing a handler — no restructuring of the signature, no rewriting of any call site that already uses the existing effects. ### Full source #### Reader ```saga import Std.Set record User { id: Int, name: String, } record Request { id: String, user: User, flags: Set String, } effect RequestContext { fun current_user : Unit -> User fun request_id : Unit -> String fun feature_enabled : (name: String) -> Bool } fun new_price : String -> Int new_price "book" = 12 new_price "pen" = 2 new_price _ = 5 fun old_price : String -> Int old_price "book" = 15 old_price "pen" = 3 old_price _ = 7 fun price_for : (item: String) -> Int needs {RequestContext} price_for item = if feature_enabled! "new_pricing" then new_price item else old_price item fun cart_total : List String -> Int needs {RequestContext} cart_total [] = 0 cart_total (item :: rest) = price_for item + cart_total rest fun checkout : (cart: List String) -> String needs {RequestContext} checkout cart = { let user = current_user! () let rid = request_id! () let total = cart_total cart $"[req={rid}] {user.name} checked out {List.length cart} items, total {total}" } fun handle : (req: Request) -> String handle req = { checkout ["book", "pen", "mug"] } with { current_user () = resume req.user request_id () = resume req.id feature_enabled name = resume Set.member name req.flags } main () = { let alice_req = Request { id: "req-001", user: User { id: 1, name: "alice" }, flags: Set.from_list ["new_pricing"], } let bob_req = Request { id: "req-002", user: User { id: 2, name: "bob" }, flags: Set.new (), } handle alice_req |> dbg handle bob_req |> dbg } ``` #### Writer ```saga import Std.Control (Tell, run_writer) record Event { step: String, detail: String, } deriving (Debug) fun reserve_inventory : (item: String) -> Unit needs {Tell Event} reserve_inventory item = tell! (Event { step: "inventory", detail: $"reserved 1x {item}" }) fun charge_card : (amount: Int) -> Unit needs {Tell Event} charge_card amount = tell! (Event { step: "billing", detail: $"charged {amount} cents" }) fun ship_order : (item: String) -> Unit needs {Tell Event} ship_order item = tell! (Event { step: "shipping", detail: $"queued {item} for shipment" }) fun place_order : (item: String) -> (price: Int) -> String needs {Tell Event} place_order item price = { reserve_inventory item charge_card price ship_order item $"order placed: {item}" } main () = { let (result, audit) = run_writer (fun () -> place_order "book" 1299) dbg result dbg $"audit trail ({List.length audit} events):" List.iter dbg audit } ```