5. Algebraic data types stored in place¶
Use Choice[F] for a non-recursive algebraic data type whose value should be
stored in place. It holds one constructor inside a native Variant, without
allocating storage for the Choice itself. Declare the constructors with
Cases, as in chapter 4, and match them with fp.match.
Choice uses the same clause syntax as Node. The distinction is storage:
Node supports shared values and recursive fields, while Choice owns a
constructor directly. A bare native Variant is not a match subject.
Store a value in a Choice¶
"""Native callbacks, terminal folds, typed errors, and a value stored in place."""
import fp
from std.testing import assert_equal, assert_false
from fp.adt import Data, Cases, Choice, Value
from fp.iteration import fold_left
from fp.data import attempt, raise_on_err
@fieldwise_init
struct Empty(Copyable):
pass
@fieldwise_init
struct Number(Copyable):
var value: Int
@fieldwise_init
struct Pair(Copyable):
var left: Int
var right: Int
struct Term(Data):
comptime Layer[R: Value] = Cases[Empty, Number, Pair]
@fieldwise_init
struct InvalidCount(Copyable, Writable):
var count: Int
def main() raises:
var calls = 0
def add(var total: Int, var item: Int) {mut calls} -> Int:
calls += 1
return total + item
var total = fold_left(add, 0, range(1, 6))
assert_equal(total, 15)
assert_equal(calls, 5)
var value: Choice[Term] = Pair(total, 7)
var answer = fp.match(value,
lambda (e: Empty) -> Int: 0,
lambda (n: Number) -> Int: n.value,
lambda (p: Pair) -> Int: p.left + p.right)
assert_equal(answer, 22)
def checked() raises InvalidCount {var answer} -> Int:
if answer < 0:
raise InvalidCount(answer)
return answer
var result = attempt(checked)
var restored = 0
var failed = False
try:
restored = raise_on_err(result^)
except error:
failed = True
assert_false(failed)
assert_equal(restored, 22)
print("fold =", total, "; match =", answer, "; callbacks =", calls)
Run from the repository root pixi run mojo run -I src docs/examples/foundations.mojo
Output
fold = 15 ; match = 22 ; callbacks = 5
fold_left(add, 0, range(1, 6)) returns 15 after five calls to add.
Pair(total, 7) converts to Choice[Term], and the match calls the Pair
clause, which returns 15 + 7 = 22. attempt(checked) wraps that answer in
Ok, and raise_on_err returns 22 without raising. The program prints
fold = 15 ; match = 22 ; callbacks = 5.
Term lists its constructors in Cases, and a constructor converts implicitly
to Choice[Term]. Copying a Choice copies that constructor, so Choice is
Copyable only when every constructor is. It can also store a constructor that
is only Movable; unwrap[C]() moves the constructor out again.
A match clause borrows the stored constructor without copying it. A clause
taking the whole Choice[Term] acts as a catch-all. For direct inspection, use
value.isa[Pair](), value[Pair] or value == Pair(15, 7).
A type with a recursive field cannot be a Choice. The compiler reports
Choice: field left of Plus is recursive; a value of a recursive data type is a
Node. Recursive data is the subject of the next chapter.
Match Result, Optional and ControlFlow¶
"""Match a standard Optional and a Result: the clauses borrow the stored value."""
import fp
from fp.data import Ok, Err, Result
from std.testing import assert_equal
def total(values: List[Int]) -> Int:
var result = 0
for value in values:
result += value
return result
def sum_or_missing(values: Optional[List[Int]]) -> Int:
return fp.match(values,
lambda (present: List[Int]) -> Int: total(present),
lambda (absent: NoneType) -> Int: -1)
def describe(result: Result[Int, String]) -> String:
return fp.match(result,
fp.when[lambda (o: Ok[Int]) -> Bool: o.value > 40, lambda (o: Ok[Int]) -> String: "large"],
lambda (o: Ok[Int]) -> String: "small",
lambda (e: Err[String]) -> String: "failed: " + e.value)
def main() raises:
var values: List[Int] = [10, 12, 20]
var subject = Optional(values^)
var result = sum_or_missing(subject)
assert_equal(result, 42)
# The match borrowed the list; it is still there.
assert_equal(len(subject.value()), 3)
assert_equal(sum_or_missing(None), -1)
assert_equal(describe(Ok(result)), "large")
assert_equal(describe(Ok(7)), "small")
assert_equal(describe(Err(String("no data"))), "failed: no data")
print("optional total =", result)
Run from the repository root pixi run mojo run -I src docs/examples/optional.mojo
Output
optional total = 42
sum_or_missing returns 42 for the list [10, 12, 20] and -1 for None.
describe returns "large" for Ok(42), "small" for Ok(7) and
"failed: no data" for Err("no data").
- Result holds
Ok[T]orErr[E]in place, and clauses take those constructors. The guarded clause indescribeapplies only to values above - The methods
map,flat_map,foldand the rest work as before; a match is the form that checks every case and allows guards. - Optional is the standard type. A clause takes the value's type for a
present value and
NoneTypefor an absent one, orOptional[T]as a catch-all. The match borrows, so the list is still insubjectafterwards. - ControlFlow, the result of
fold_until, is aChoiceofBreak[B]andContinue[C].
Every kind of value can take part in a match of several values:
fp.match((result, optional), ...) covers every combination of Ok or Err
with present or absent. The tuple holds its values, so copy a value that is not
implicitly copyable into it with .copy(), or move it with ^.
Take a value out¶
Matching only borrows. To take a value out, use the consuming methods of the
type: Result.fold_owned or raise_on_err, Optional.take(), or
Choice.unwrap.
For the API, see fp.adt, fp.data and fp.matching. Next, declare recursive data.