Tutorial

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

foundations.mojo Download
"""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

optional.mojo Download
"""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] or Err[E] in place, and clauses take those constructors. The guarded clause in describe applies only to values above
  • The methods map, flat_map, fold and 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 NoneType for an absent one, or Optional[T] as a catch-all. The match borrows, so the list is still in subject afterwards.
  • ControlFlow, the result of fold_until, is a Choice of Break[B] and Continue[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.