std.functional
The std.functional modules ship the monadic types. Four modules exist today: std.functional.maybe, std.functional.either, std.functional.result, and std.functional.io. The spec’s fifth monad, the list monad, is not in the tree yet.
Maybe, Either, and Result are ordinary generic enums; IO<T> is a struct wrapping a collected thunk. Importing any of them does not grant a paradigm, so constructing them, matching on them, and calling their helper functions works in any file. Only do notation requires @paradigm functional.
Imported names are flat: after @import std.functional.maybe you call is_some and unwrap_or with no prefix. Enum constructors keep their type name, so you write Maybe.Some(42) and Maybe.None.
std.functional.maybe
Section titled “std.functional.maybe”An optional value. It is Some with a payload or None.
enum Maybe<T> { Some(v: T), None,}| Function | Description |
|---|---|
is_some<T>(m: Maybe<T>) -> bool | True when the value is Some. |
is_none<T>(m: Maybe<T>) -> bool | True when the value is None. |
unwrap_or<T>(m: Maybe<T>, fallback: T) -> T | The payload, or fallback when None. |
maybe_map<A, B>(m: Maybe<A>, f: (A) -> B) -> Maybe<B> | Applies f to a Some payload, passes None through. |
maybe_and_then<A, B>(m: Maybe<A>, f: (A) -> Maybe<B>) -> Maybe<B> | Chains a Maybe returning step onto a Some payload. |
maybe_or_else<A>(m: Maybe<A>, f: () -> Maybe<A>) -> Maybe<A> | Runs f for a fallback Maybe when the value is None. |
unwrap_or collapses a Maybe to a plain value. To act on the payload directly, match on the variants.
@import std.functional.maybe
func main() -> int32 { m: Maybe<int64> = Maybe.Some(42) if is_some(m) { println("present") // present } println(unwrap_or(m, 0)) // 42
none: Maybe<int64> = Maybe.None println(unwrap_or(none, 99)) // 99 println(is_none(none)) // 1
doubled := maybe_map(m, lambda (x: int64) -> int64 { return x * 2 }) println(unwrap_or(doubled, 0)) // 84
match m { Some(v) => println("got {}", v), // got 42 None => println("empty"), } return 0}Maybe appears elsewhere in the standard library: map_get in std.map returns a Maybe<V>, so lookups that can miss resolve through unwrap_or or a match rather than a sentinel value.
std.functional.either
Section titled “std.functional.either”A value of one of two types. Left is the error or first case by convention, Right is the success or second case.
enum Either<L, R> { Left(l: L), Right(r: R),}| Function | Description |
|---|---|
is_left<L, R>(e: Either<L, R>) -> bool | True when the value is Left. |
left_or<L, R>(e: Either<L, R>, fallback: L) -> L | The Left payload, or fallback when Right. |
right_or<L, R>(e: Either<L, R>, fallback: R) -> R | The Right payload, or fallback when Left. |
either_map<L, R, B>(e: Either<L, R>, f: (R) -> B) -> Either<L, B> | Applies f to a Right payload, passes Left through. |
either_map_left<L, R, B>(e: Either<L, R>, f: (L) -> B) -> Either<B, R> | Applies f to a Left payload, passes Right through. |
either_and_then<L, R, B>(e: Either<L, R>, f: (R) -> Either<L, B>) -> Either<L, B> | Chains an Either returning step onto a Right payload. |
either_or_else<L, R>(e: Either<L, R>, f: (L) -> Either<L, R>) -> Either<L, R> | Runs f for a fallback Either when the value is Left. |
Unlike the builtin error type, which carries only a message, Either puts a typed payload on both sides, so it suits failures that carry data. See error handling for how the two approaches relate.
@import std.functional.either
func checked_div(a: int64, b: int64) -> Either<string, int64> { if b == 0 { return Either.Left("division by zero") } return Either.Right(a / b)}
func main() -> int32 { e := checked_div(10, 0) if is_left(e) { println(left_or(e, "no error")) // division by zero }
ok := checked_div(10, 2) match ok { Left(l) => println("error: {}", l), Right(r) => println("value: {}", r), // value: 5 }
doubled := either_map(ok, lambda (x: int64) -> int64 { return x * 2 }) println(right_or(doubled, 0)) // 10 return 0}std.functional.result
Section titled “std.functional.result”Result<T, E> is enum Result<T, E> { Ok(v: T), Err(e: E) }, a success or a typed failure. Added in 0.5.3.
enum Result<T, E> { Ok(v: T), Err(e: E),}| Function | Description |
|---|---|
bind, unit (in monad Result, E fixed to string) | The monad pair a do Result { ... } block desugars against. |
result_ok<T>(v: T) -> Result<T, string> | Wrap a value in Ok. |
result_err<T>(msg: string) -> Result<T, string> | Wrap a message in Err. |
result_from<T>(v: T, e: error) -> Result<T, string> | Bridge a (value, error) pair into a Result. |
is_ok<T, E>(r: Result<T, E>) -> bool | True when the value is Ok. |
is_err<T, E>(r: Result<T, E>) -> bool | True when the value is Err. |
result_unwrap_or<T, E>(r: Result<T, E>, fallback: T) -> T | The payload, or fallback when Err. |
result_map<T, E, U>(r: Result<T, E>, f: (T) -> U) -> Result<U, E> | Applies f to an Ok payload, passes Err through. |
result_map_err<T, E, F>(r: Result<T, E>, f: (E) -> F) -> Result<T, F> | Applies f to an Err payload, passes Ok through. |
result_and_then<T, E, U>(r: Result<T, E>, f: (T) -> Result<U, E>) -> Result<U, E> | Chains a Result returning step onto an Ok payload. |
result_or_else<T, E, F>(r: Result<T, E>, f: (E) -> Result<T, F>) -> Result<T, F> | Runs f for a fallback Result when the value is Err. |
The monad Result { ... } block fixes E to string, the common case, since a generic E cannot flow through do inference; a caller needing a different error type uses the plain constructors and helpers above instead of do Result { ... }. A do Result { ... } block threads Ok values and short circuits on the first Err (see do notation).
@paradigm functional
@import std.functional.result
func main() -> int32 { r := do Result { a <- Result.Ok(1) b <- Result.Ok(20) a + b } match r { Ok(v) => println("ok {}", v), Err(e) => println("err {}", e), } return 0}result_from bridges a fallible call’s (value, error) return into a Result, folding an existing error into Err(e.toString()) and an absent one into Ok(v). Handing result_from a bound error discharges the caller’s must-handle obligation, the same as handing it to any other parameter declared error; see error handling.
std.functional.io
Section titled “std.functional.io”IO<T> is struct IO<T> { run: collector<() -> T> }, a true lazy monad that composes through generic do like any other monad. Added in 0.5.3, bind and unit build a new collected thunk instead of running anything, so a do IO { ... } chain is a suspended computation the moment it is built and nothing fires until run forces it, on the calling thread. The thunk and every step it captures live on the collected heap, so a chain outlives the frame that built it and survives a collection forced between build and force. Building or running a chain touches neither the event loop nor the thread pool.
| Function | Description |
|---|---|
io_pure<A>(x: A) -> IO<A> | Wrap a value in a lazy IO. |
bind, unit (in monad IO) | The monad pair a do IO { ... } block desugars against. |
run<A>(io: IO<A>) -> A | Force the thunk on the calling thread and return the value. |
io_map<A, B>(m: IO<A>, f: collector<(A) -> B>) -> IO<B> | Map a pure function over the value once forced. |
io_and_then<A, B>(m: IO<A>, f: collector<(A) -> IO<B>>) -> IO<B> | Sequence an effectful step after m, without do. |
io_print(msg: string) -> IO<bool> | Print msg with no newline when forced, yields true. |
io_println(msg: string) -> IO<bool> | Print msg with a newline when forced, yields true. |
io_read_line() -> IO<Result<string, string>> | Read one line when forced; Err at end of input or on a read error. |
@paradigm functional
@import std.functional.io
func main() -> int32 { r := run(do IO { a <- io_pure(10) b <- io_pure(20) a + b }) println(r) // 30 return 0}IO<T> does not exist for void; an effect that returns nothing yields bool instead, as io_print and io_println do. As a collected value, an IO<T> is confined to the thread that built it: it cannot cross a spawn or submit capture, a channel, or an interface box.
Migration note. Before 0.5.3, run minted a future and offloaded the carried value to a pool worker, so a program had to bring the event loop and the pool up with loop_init and pool_start first and tear them down after the last run. That contract is gone: run forces its thunk directly on the calling thread, with no loop or pool required. A program that kept that ceremony around an IO chain for no other reason can drop it.
Do notation
Section titled “Do notation”Do notation chains computations that return a monadic value, short circuiting the rest of the chain when one step fails. It requires @paradigm functional (see the paradigm system).
A do block is a sequence of name <- expr binds ending in a result expression. The compiler desugars it into nested bind calls, with the final expression lifted through unit: each <- becomes a bind whose continuation lambda binds the name for the lines below it, so evaluation runs top to bottom. A line without a <- still runs through bind; its result is bound to a hidden discard name.
A bare do { ... } desugars against top level functions named bind and unit. A named do Name { ... } desugars against Name.bind and Name.unit, declared in a monad Name { ... } block, so several monads coexist in one file. The monad keyword also belongs to the functional paradigm.
Since 0.4.3, do notation composes over any generic monad, not only a bind and unit already ground to concrete types. The desugar emits its continuation chain over an open type hole, and monomorphization instantiates the bind and unit pair fresh at each do site. The shipped Maybe, Result, and IO modules each carry a monad block, so do Maybe { ... }, do Result { ... }, and do IO { ... } work straight from the import. Either ships no monad block, so it has no do form. You can still declare your own monad block for a specific instantiation, as the examples below do. See functional programming for the full desugar and the types-only recheck pass.
Maybe with do notation
Section titled “Maybe with do notation”bind on a Maybe runs the continuation on the Some payload and passes None through untouched. One None anywhere in the chain makes the whole block None.
@paradigm functional
@import std.functional.maybe
monad MaybeInt { func bind(m: Maybe<int64>, f: (int64) -> Maybe<int64>) -> Maybe<int64> { match m { Some(v) => return f(v), None => return Maybe.None, } } func unit(v: int64) -> Maybe<int64> { return Maybe.Some(v) }}
func half(n: int64) -> Maybe<int64> { if n % 2 == 0 { return Maybe.Some(n / 2) } return Maybe.None}
func main() -> int32 { r := do MaybeInt { x <- half(20) y <- half(x) x + y } println(unwrap_or(r, -1)) // 15
s := do MaybeInt { x <- half(7) x + 1 } println(unwrap_or(s, -1)) // -1 return 0}In the first block, half(20) is Some(10) and half(10) is Some(5), so the result expression lifts to Some(15). In the second, half(7) is None, so the continuation never runs and the whole block is None.
Either with do notation
Section titled “Either with do notation”The same shape works for Either, threading the Right payload forward and passing the first Left through unchanged. The Left payload survives, so the block reports which step failed.
@paradigm functional
@import std.functional.either
monad EitherInt { func bind(e: Either<string, int64>, f: (int64) -> Either<string, int64>) -> Either<string, int64> { match e { Left(l) => return Either.Left(l), Right(r) => return f(r), } } func unit(v: int64) -> Either<string, int64> { return Either.Right(v) }}
func checked_div(a: int64, b: int64) -> Either<string, int64> { if b == 0 { return Either.Left("division by zero") } return Either.Right(a / b)}
func main() -> int32 { r := do EitherInt { x <- checked_div(100, 5) y <- checked_div(x, 0) x + y } match r { Left(l) => println("error: {}", l), // error: division by zero Right(v) => println("value: {}", v), } return 0}For the desugaring rules, the monad block, and the functional builtins that surround these types, see functional programming and the paradigms guide.