std.functional
The std.functional modules ship the monadic types. Two modules exist today: std.functional.maybe and std.functional.either. The spec lists std.functional.result (Result<T, E>) and std.functional.io (IO<T>) as planned; they are not in the standard library yet.
Both types are ordinary generic enums, so importing them does not grant any paradigm. 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. |
unwrap_or<T>(m: Maybe<T>, fallback: T) -> T | The payload, or fallback when 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
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. |
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 } return 0}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.
One version caveat: do notation currently desugars against a bind whose types are concrete. A bind generic over the element type is not yet monomorphized through do, so ground monads work and fully generic ones wait on a later release. For this reason the std.functional modules ship the types and their helpers but no generic bind or unit. You declare a small monad block for the concrete instantiation you need.
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.