3. Results and recovery¶
Result[T, E] is a sum type with two constructors: Ok[T] for success and
Err[E] for failure. Use it to return, store or collect an outcome that the
caller will handle later. map transforms a successful value; flat_map
sequences computations that can themselves return an Err.
Stored failures and raised errors are separate. If a callback raises, its
error propagates to the caller. Use attempt to catch it in a Result.
Transform only the active branch¶
"""Domain errors remain distinct from raised callback failures."""
from fp.data import Result, Ok, Err, attempt, raise_on_err
from std.collections import StringDict
from std.testing import assert_equal
comptime Checked = Result[Int, String]
def positive(var value: Int) -> Checked:
if value <= 0:
return Err(String("expected a positive value"))
return Ok(value)
def twice(var value: Int) -> Int:
return value * 2
def success(value: Int) -> String:
return "ok: " + String(value)
def failure(error: String) -> String:
return "domain: " + error
def may_raise(var value: Int) raises String -> Int:
if value == 21:
raise String("callback failed")
return value
def main() raises:
var good = positive(21).map(twice)
assert_equal(good.fold(success, failure), "ok: 42")
print(good.fold(success, failure))
var bad = positive(-1).map(twice)
assert_equal(bad.is_err(), True)
print(bad.fold(success, failure))
var caught = False
try:
var value = positive(21).map(may_raise)
print(value.fold(success, failure))
except error:
assert_equal(error, "callback failed")
caught = True
print("raised:", error)
assert_equal(caught, True)
# Explicitly capture the same native error as a Result value.
var captured: Checked = attempt(may_raise, 21)
assert_equal(captured.is_err(), True)
caught = False
try:
_ = raise_on_err(captured^)
except error:
assert_equal(error, "callback failed")
caught = True
assert_equal(caught, True)
# Forward a positional prefix and native keyword options.
var calls = 0
def scale(value: Int, /, var **options: Int) {mut calls} -> Int:
calls += 1
var factor = 1
for entry in options.items():
if entry.key == "factor": factor = entry.value
return value * factor
var scaled: Result[Int, Never] = attempt(scale, 7, factor=3)
assert_equal(raise_on_err(scaled^), 21)
assert_equal(calls, 1)
var options = StringDict[Int]()
options["factor"] = 4
assert_equal(raise_on_err(attempt(scale, 7, **options^)), 28)
assert_equal(calls, 2)
# Changes made before a native error remain visible to the caller.
var balance = 5
def debit(mut value: Int, /, var **options: Int) raises String -> Int:
for entry in options.items(): value -= entry.value
if value < 0: raise String("overdrawn")
return value
var first_debit: Checked = attempt(debit, balance, amount=3)
assert_equal(first_debit.is_ok(), True)
assert_equal(balance, 2)
var second_debit: Checked = attempt(debit, balance, amount=4)
assert_equal(second_debit.is_err(), True)
assert_equal(balance, -2)
Run from the repository root pixi run mojo run -I src docs/examples/results.mojo
Output
ok: 42
domain: expected a positive value
raised: callback failed
positive(21) returns Ok(21), and map(twice) turns it into Ok(42).
positive(-1) returns Err, so twice never runs. fold borrows whichever
payload is present and calls success or failure, which gives "ok: 42" and
"domain: expected a positive value".
positive returns Ok(value) or Err(...) directly; both convert to the
declared Checked result. map consumes its Result. For a named local, write
value^.map(callback); the temporary that positive(...) returns is consumed
directly. A callback that changes the type produces Result[U, E], and an
Err passes through unchanged.
These transformations can be chained. Each calls its callback only for the case it handles:
flat_mapis monadic bind: it passes a successful value to a callback that returns anotherResult, keeping the stored error type.map_errchanges the error payload.or_elserecovers from an error with anotherResult.
Code that is generic over algebra instances reaches the same methods through
map[ResultFamily[E]].
Keep raised errors raised¶
may_raise is declared raises String and raises for 21. Calling
positive(21).map(may_raise) propagates that error to the except block, which
prints raised: callback failed. The shared String error type does not turn
the raised error into a stored Err.
To convert it on purpose, call attempt(may_raise, 21). It returns
Err("callback failed") with the declared String error type.
raise_on_err consumes that Result and raises the error again. These two
operations let you choose where failures move between values and exceptions.
Forward arguments through attempt¶
attempt calls its callback exactly once with the arguments you give it. The
example shows each rule with its own values:
attempt(scale, 7, factor=3)forwards the positional7and the keywordfactor=3to the captured closurescale, which declaresvar **options: Int. It returnsResult[Int, Never]holding21, becausescaledoes not raise, and the counter records one call.- A second call consumes a
StringDict[Int]through**options^, returns28, and brings the same counter to two. - A
mutprefix keeps the callback's changes even when it raises.debittakesbalancefrom 5 to 2, then to -2 while returningErr("overdrawn").
What attempt accepts:
- Zero, one or two positional arguments, optionally followed by a homogeneous native keyword pack. The bridge function and the prefixes are positional-only.
- Every positional argument, including those with defaults; omitted prefixes are not supported.
- Borrowed resource arguments stay usable after the call. Move a named resource
that the callback consumes with
^. - Conflicting mutable borrows are rejected as in native calls.
- Ordinary fixed keyword parameters and generic positional packs are not supported. Bind those forms in a local native closure, when Mojo admits that closure's encoding.
- A callback that returns an ordinary
Resultgets it nested insideOk;attemptdoes not capture thatResult's error.
Aggregate a sequence¶
collect_results consumes Result values in order. When the source runs out,
it returns Ok(List[T]). At the first Err it returns that error straight
away, without pulling another input, and native cleanup releases the successful
prefix and the owned source. The configuration processor uses
this to separate parsing from validation.
Generic transaction helpers¶
submit and transaction are generic functions whose results keep the
callback's exact success and error types. The caller holds the Result after
both helpers return and decides how to handle it.
"""A generic transaction helper preserves state and returns the exact error."""
from fp.data import Result, attempt, raise_on_err
from std.testing import assert_equal
def transaction[A: Movable & Deinitable, R: Movable & Deinitable,
X: Movable & Deinitable, //,
F: def(mut A, /, var **kwargs: Int) raises X -> R](
function: F, mut state: A, /, var **options: Int
) -> Result[R, X]:
return attempt(function, state, **options^)
def submit[A: Movable & Deinitable, R: Movable & Deinitable,
X: Movable & Deinitable, //,
F: def(mut A, /, var **kwargs: Int) raises X -> R](
function: F, mut state: A, /, var **options: Int
) -> Result[R, X]:
return transaction(function, state, **options^)
@fieldwise_init
struct Declined(Movable):
var balance: Int
def main() raises:
var calls = 0
def debit(mut balance: Int, /, var **charges: Int) raises Declined {mut calls} -> String:
calls += 1
for entry in charges.items(): balance -= entry.value
if balance < 0: raise Declined(balance)
return String(balance)
var balance = 10
var approved: Result[String, Declined] = submit(debit, balance, purchase=6, fee=1)
var receipt: String
try: receipt = raise_on_err(approved^)
except error: raise Error("unexpected decline")
assert_equal(receipt, "3")
var declined: Result[String, Declined] = submit(debit, balance, purchase=5)
var failure_balance = 0
try: _ = raise_on_err(declined^)
except error: failure_balance = error.balance
assert_equal(failure_balance, -2)
assert_equal(balance, -2)
assert_equal(calls, 2)
print("generic attempt: receipt=3; declined=-2; balance=-2; calls=2")
Run from the repository root pixi run mojo run -I src docs/examples/attempt_generic.mojo
Output
generic attempt: receipt=3; declined=-2; balance=-2; calls=2
The balance starts at 10. The first debit charges purchase=6 and fee=1,
leaves the balance at 3, and returns the receipt "3". The second charges
purchase=5, leaves the balance at -2, and returns Declined(-2). Catching the
error does not roll back the debit. The same callback serves both calls, each
transaction invokes it once, and the keyword values pass through both helpers.
The helpers call the public attempt directly, with no callable object or
result cast.
Generic transformations¶
mapped and forwarded consume a Result and borrow the same reusable
callback. Each raising helper forwards X explicitly. The caller then chooses
whether to sequence a successful value, translate a stored error, or recover.
"""Generic Result processing keeps domain failures separate from raised failures."""
from fp.data import Result, Ok, Err
from std.testing import assert_equal
def mapped[T: Movable & Deinitable, E: Movable & Deinitable,
U: Movable & Deinitable, X: Movable & Deinitable, //,
F: def(var T) raises X -> U](
var value: Result[T, E], callback: F
) raises X -> Result[U, E]:
return value^.map[X=X](callback)
def forwarded[T: Movable & Deinitable, E: Movable & Deinitable,
U: Movable & Deinitable, X: Movable & Deinitable, //,
F: def(var T) raises X -> U](
var value: Result[T, E], callback: F
) raises X -> Result[U, E]:
return mapped[X=X](value^, callback)
@fieldwise_init
struct Domain(Movable):
var code: Int
@fieldwise_init
struct ProcessingFailure(Movable):
var code: Int
@fieldwise_init
struct Notice(Movable):
var code: Int
def accepted(var value: Int) -> Result[String, Domain]:
if value < 10: return Err(Domain(40))
return Ok(String("accepted"))
def explain(var error: Domain) -> Notice:
return Notice(error.code + 100)
def recover(var error: Notice) -> Result[String, Never]:
return Ok("notice " + String(error.code))
def main() raises:
var calls = 0
def process(var value: Int) raises ProcessingFailure {mut calls} -> Int:
calls += 1
if value < 0: raise ProcessingFailure(90)
return value * 2
for mode in range(3):
var source = Result[Int, Domain](Err(Domain(7))) if mode == 1 else Result[Int, Domain](Ok(6 if mode == 0 else -1))
var outcome: String
try:
var mapped_value = forwarded(source^, process)
outcome = mapped_value^.flat_map(accepted).map_err(explain).or_else(recover).raise_on_err()
except failure:
comptime assert type_of(failure) == ProcessingFailure
outcome = "raised " + String(failure.code)
assert_equal(outcome, "accepted" if mode == 0 else "notice 107" if mode == 1 else "raised 90")
print(outcome)
assert_equal(calls, 2)
Run from the repository root pixi run mojo run -I src docs/examples/result_transform_generic.mojo
Output
accepted
notice 107
raised 90
The three inputs produce accepted, notice 107 and raised 90:
Ok(6)becomes12throughprocess, andacceptedreturnsOk("accepted").Err(Domain(7))skipsprocessandaccepted.map_err(explain)turns it into aNoticewith code 107, andor_else(recover)recovers it.Ok(-1)makesprocessraiseProcessingFailure(90). The error leaves the chain at once and reachesexcept;map_errandor_elsedo not see it.
The counter ends at two, because only the two Ok inputs call process. The
branch types that flat_map and or_else leave unchanged are part of their
native callback signatures.
Next, select among structured values with data types and matching. For every operation, its ownership mode and exact error signature, see fp.data.