DocsStandard library

Failure, state, and randomness.

Use standard handlers to turn requests into results, local state, or reproducible draws.

These modules provide a few common effects together with ready-made ways to run them. The idea is to keep the effectful calculation separate from the runner that decides how it behaves.

API reference: Fail · Failure · Trace · State · Runtime.Local · Random

Fail and Failure.

import Fail exposing (Fail)
import Result exposing (Result(..))

positive : Int ->{Fail String} Int
positive number = if number > 0 then number else Fail.fail "not positive"

main() =
    case Fail.attempt { positive (-1) } of
        Ok value -> print value
        Err message -> print message

This prints not positive. fail aborts to the matching handler, and attempt returns Ok when the computation completes normally and Err when it fails. fromResult bridges an API that returns a result into a computation that can fail.

When cleanup can fail too, use attemptReport. Its error holds the primary value along with snapshots of the suppressed ones. The separate Failure module exposes the names and type-checked arguments of those snapshots, without exposing resumptions or handlers.

Failure diagnostics.

When you need to know where a failure started, use attemptDiagnostic:

import Fail exposing (Fail)
import Result exposing (Result(..))

positive : Int ->{Fail String} Int
positive number = if number > 0 then number else Fail.fail "not positive"

main() =
    case Fail.attemptDiagnostic { positive (-1) } of
        Ok value -> print value
        Err diagnostic ->
            print diagnostic.primary
            print (List.length diagnostic.suppressed)
not positive
0

The diagnostic holds the primary error, a trace of the stack where the failure began, and any suppressed cleanup failures. The trace is taken at the original failure site, before cleanup unwinds the stack. Trace.render diagnostic.trace turns it into readable lines. Each cleanup snapshot can carry a trace of its own, which Failure.trace returns as a Maybe; it is Nothing when none was captured.

Capturing a stack costs something, so it is opt-in. Ordinary attempt and attemptReport don’t capture one, and neither does a successful attemptDiagnostic. A failure keeps its stack as it propagates, but calling fail again creates a new failure with a new stack. That includes fromResult, because a plain Result doesn’t carry the old trace.

To log where you are right now, rather than where a failure began, call Trace.capture(). It returns frames with the function, file, and line, innermost first. Runtime failures shows how the same frames appear in crash reports.

State.

import State

main() =
    result = State.run 0 {
        current = State.get()
        State.put (current + 1)
        State.get()
    }
    print result.value
    print result.state

Both lines print 1. State.run initial action owns a cell and returns the action’s value together with the final state. Nested runs each get their own cell. That makes local state possible while the outer calculation stays pure, as long as no other effects remain.

State.modify f replaces the state with f applied to it, and State.exchange next stores next and returns the value it replaced. Both are a single State.update step, where step takes the current state and returns a result together with the next state.

Tasks started inside State.run share its cell, and a get followed by a put from two tasks can lose one of the updates. State.runAtomic makes each update, and so each modify or exchange, atomic:

import State

main() =
    result = State.runAtomic 1 {
        State.modify { n -> n + 1 }
        State.update { n -> (n * 10, n + 1) }
    }
    print result.value
    print result.state
20
3

Under runAtomic, step may run more than once when tasks compete, so keep it pure. For several values that must change together across tasks, use STM.

Scoped local state.

When the state belongs to a single callback, Runtime.Local gives it a private cell, without introducing a named State application:

import Runtime.Local as Local

main() =
    result = Local.run 10 { cell ->
        cell.write (cell.read() + 2)
        cell.read()
    }
    print result
12

The runner returns the callback’s value. Its fresh local permission stops the cell, or any function that holds on to it, from escaping, so return computed data such as 12 instead. Buffered readers use this same mechanism for their private cursor state.

Random.

import Random

roll() = Random.int 1 6

main() =
    print (Random.runSeeded 42 roll)
    print (Random.runSeeded 42 roll)

The two lines show the same draw. Random.int asks for a value between its bounds, both included. runSeeded picks deterministic behavior, while runSystem seeds the generator from system entropy and adds IO. runSystem is also the default handler, so a main that calls Random.int without a runner draws fresh numbers.

The generator isn’t cryptographic. Each runner has its own independent state, and deterministic runs can even execute during compile-time evaluation. Use seeded runs for examples and tests that need to be repeatable.