FP Mojo: functional programming and exhaustive matching in pure Mojo

FP Mojo: functional programming and exhaustive matching in pure Mojo

Hi everyone,

Meet FP Mojo! It’s a functional programming library written in pure Mojo, released under the Apache License 2.0. Compose functions into pipelines, process collections lazily, handle failures with typed results, and define algebraic and inductive data types from your own structs. Functions and closures keep their native types and ownership conventions, with no Python runtime dependency.

Start with two expression constructors, Num and Add. The same two-clause evaluator below handles a million levels of nesting and a shared expression whose fully expanded tree would contain more than a quintillion leaves:

import fp
from fp.adt import Cases, Data, Node, Value


@fieldwise_init
struct Num(Copyable):
    var value: Int


@fieldwise_init
struct Add[R: Value](Movable):
    var left: Self.R
    var right: Self.R


struct Expr(Data):
    comptime Layer[R: Value] = Cases[Num, Add[R]]


comptime E = Node[Expr]


def evaluate(expression: E) -> Int:
    return fp.match(
        expression,
        lambda (n: Num) -> Int: n.value,
        lambda (a: Add[Int]) -> Int: a.left + a.right,
    )


def main():
    var one: E = Num(1)

    var shared: E = one
    for _ in range(60):
        shared = Add(shared, shared)
    print("60 shared additions:", evaluate(shared))

    var deep: E = Num(0)
    for _ in range(1_000_000):
        deep = Add(deep, one)
    print("1,000,000 levels deep:", evaluate(deep))

Output:

60 shared additions: 1152921504606846976
1,000,000 levels deep: 1000000

The first loop builds just 61 distinct nodes. Each addition shares its two operands, so it doubles the result without duplicating the preceding expression. With the copyable Int result used here, each shared node is evaluated once per match. The answer is 2**60.

The second loop builds an expression a million additions deep. The Add[Int] clause receives both child results already evaluated; fp.match manages the traversal with an explicit stack, so the depth does not overflow the native call stack. Remove either constructor’s clause and the compiler rejects the incomplete match. The recursive data tutorial also shows branch selection and expression rewriting.

The library provides:

  • Function composition. pipe passes a value through functions from left to right. flow and compose build reusable compositions, partial binds a positional prefix, and piped(...).then(...) supports longer chains.
  • Lazy iteration and folds. map, filter, filter_map, flat_map, and scan_left produce iterators. Terminal operations such as fold_left, find, and collect_list pull the values they need.
  • Typed results and errors. Result[T, E] represents success with Ok and failure with Err. flat_map skips later callbacks after an Err; attempt and raise_on_err bridge returned and raised errors while preserving the error type.
  • Your own data types. Node stores immutable, shared recursive values. Choice stores non-recursive alternatives in place. Pattern matching checks constructor coverage at compile time, and fp.rewrite transforms recursive values from the leaves upward.
  • Native ownership and borrowing. Use ordinary Mojo functions, closures, collections, and structs, including move-only values and borrowed views. Borrowed values retain their origins.
  • Algebra and effects. Functor, Applicative, Monad, and Traversable operations work across Identity, Optional, Result, and List instances. Reader, State, Writer, and monad transformers let you compose computations that read an environment, pass state, accumulate a log, or handle failure.
  • Loops as expressions. while_loop, fori_loop, and scan carry values through a computation and return its result.

For an everyday example, build a pipeline with separate stages for filtering, mapping, folding, and formatting:

from fp.functions import piped
from fp.iteration import FilterIterator, MapIterator, filter, fold_left, map


def positive(value: Int) -> Bool: return value > 0
def square(value: Int) -> Int: return value * value
def add(total: Int, value: Int) -> Int: return total + value


comptime Positives = FilterIterator[Int, List[Int].IteratorOwnedType]
comptime Squares = MapIterator[Int, Int, Positives]


def report(total: Int) -> String:
    return "sum of positive squares = " + String(total)


def main():
    var values: List[Int] = [3, -1, 4, -1, 5]
    var result = (
        piped(values^)
        .then(lambda (var items: List[Int]) -> Positives: filter(positive, items^))
        .then(lambda (var items: Positives) -> Squares: map(square, items^))
        .then(lambda (var items: Squares) -> Int: fold_left(add, 0, items^))
        .then(report)
        .get()
    )
    print(result)

Output:

sum of positive squares = 50

values^ moves the list into the pipeline. Positives and Squares name the lazy iterator types passed between stages. The first two .then(...) calls construct adapters without processing any elements. When fold_left pulls, filter keeps 3, 4, and 5, map squares them, and the fold adds 9, 16, and 25. No intermediate lists are created. The final stage passes the total to report, and .get() returns the formatted string.

For validation, a Result keeps failure explicit and stops the success path at the first error:

from fp.data import Err, Ok, Result


def parse_port(text: String) -> Result[Int, String]:
    try:
        return Ok(atol(text))
    except:
        return Err("not an integer: " + text)


def valid_port(port: Int) -> Result[Int, String]:
    if port < 1 or port > 65535:
        return Err(String("port must be between 1 and 65535"))
    return Ok(port)


def endpoint(port: Int) -> String:
    return "localhost:" + String(port)


def rejected(error: String) -> String:
    return "invalid: " + error


def main():
    for text in ["8080", "70000", "oops"]:
        print(parse_port(text).flat_map(valid_port).fold(endpoint, rejected))

Output:

localhost:8080
invalid: port must be between 1 and 65535
invalid: not an integer: oops

"70000" parses successfully, then fails validation. "oops" fails to parse, so valid_port never runs. fold selects either endpoint or rejected to produce the final string. These failures remain values; returning an Err does not raise an exception.

FP Mojo is verified with Mojo 1.1.0 on Linux x86-64 and macOS arm64 and is available as fp_mojo in the Modular community channel. With Pixi installed, create a project:

pixi init fp-start \
  --channel https://conda.modular.com/max \
  --channel https://repo.prefix.dev/modular-community \
  --channel conda-forge
cd fp-start
pixi add fp_mojo "mojo==1.1.0"

Save any one of the programs above as showcase.mojo beside pixi.toml. From that directory, run:

pixi run mojo run showcase.mojo

The package name is fp_mojo; Mojo imports use fp, with names imported from their owning modules, such as fp.functions and fp.data.

The documentation includes a nine-part tutorial, runnable examples, an API reference, and the architecture behind the ownership, error, and matching guarantees.

I’d love to hear what you build with it! Examples, API feedback, and contributions are welcome.

5 Likes

Have you investigated if it would be possible to define |+| , *> , and, dare I say, >>= or flatMap? Chaining example from above could use some syntactic sugar and familiar operators :grimacing:

Result[T, E] - surprised to see value on the left (I’m assuming T stands for “type” and E stands for “error” ). Is this a Rust convention?
Either type is right-baised in both Haskell and Scala

Hi, @maxchisto!

Yes, Result<T, E> is Rust’s convention. Since Mojo positions itself as a systems language, adopting it seems appropriate. The same probably applies to operators and syntactic sugar.

I certainly agree that syntactic sugar is badly needed :slight_smile:
The question is which conventions to adopt. Lambdas could definitely use something like Rust’s |x| ... closure syntax. But for something like >>= (bind), it’s not obvious that adopting Haskell’s convention is the way to go, since it isn’t self-explanatory to users without a pure FP background. Rust, for instance, uses a named method instead of an operator. Here is a simple flat map example:

let words = vec!["hello", "world"];

// Split each word into characters and flatten them into one stream
let chars: Vec<char> = words.iter()
    .flat_map(|s| s.chars())
    .collect();

assert_eq!(chars, vec!['h', 'e', 'l', 'l', 'o', 'w', 'o', 'r', 'l', 'd']);

@maxchisto Frankly, I’m open to any syntax and sugar.

What matters most to me is automatically checkable guarantees of properties like exhaustiveness and termination. Providing those guarantees is the reason this library exists.

The grand idea (beyond FP Mojo) is to give data scientists tools to define models, automatically derive their properties, check their correctness, and handle deployment and inference within one ecosystem.

Today, models are usually defined in Python, their properties are derived in a CAS (e.g. Mathematica) and checked in theorem provers (e.g. Lean 4, Isabelle/HOL), and the result is shipped to production in something else (say, Mojo/MAX).

It would be lovely if this pipeline could be unified in one language: easy enough that people from diverse backgrounds can contribute, yet performant enough to run the whole thing.

So the purpose of FP Mojo is to supply some basic primitives, such as inductive types, that carry some of these guarantees. On top of those, I want to build a HOL kernel that could check more interesting stuff in conjunction with a CAS and a library of lemmas imported from other HOL-based systems.

1 Like

Nice library! :slight_smile:

I was thinking we could add to Mojo’s Pythonic touch with some wordy ideas from Scala 3.

Rust is symbol heavy and overdoes it to a point that’s not easy to digest for those used to Python or JVM functional style (definitely not meaning Clojure). Was thinking about expressive enough syntax sugar for the DX. So for the let chars example, thought of something like:

var chars = (
flow(words)
.flat_map(lambda (s: String) → List[Codepoint]: codepoints_of(s))
.to_list()
)

I’m mixing standard Mojo conventions with FP style, but you could offer a better abstraction indeed.

1 Like

@Kausch

That’s very pretty!

I’ve added method chaining for iterators, so this example could be executable right now. I’ve also bumped fp_mojo to v1.0.1, it should appear in mojo-community channel soon.

Now you can write something like that:

from fp.iteration import iterated
from std.collections.string import Codepoint
from std.testing import assert_equal

def codepoints_of(s: String) -> List[Codepoint]:
    return List(s.codepoints())

def main() raises:
    var words: List[String] = ["hello", "world"]
    var chars = (iterated(words^)
                    .flat_map(codepoints_of)
                    .to_list()
                )

    assert_equal(chars, List("helloworld".codepoints()))
    print(chars)
1 Like

That was going to be my follow up. If you haven’t, play around with Scala (both vanilla and Typelevel libs). See if it inspires any ideas. I really like ergonomics of it, incl minor things like calling bind flatMap - intuitive to understand

For a sec I thought the example was in Scala

1 Like