2 releases

0.1.1 Feb 25, 2026
0.1.0 Feb 25, 2026

#2027 in Rust patterns

MIT/Apache

155KB
4K SLoC

functype

Scala-inspired functional programming library for Rust.

Brings the ergonomics of Scala's type system — Either, Validated, IO, do-notation — to Rust, without fighting the borrow checker.

use functype::prelude::*;
use functype::IO;
use functype_core::fdo;

let result = fdo! {
    x <- IO::succeed(10);
    y <- IO::succeed(20);
    yield x + y
};
assert_eq!(result.run().unwrap(), 30);

Crates

Crate Description
functype Umbrella crate — re-exports everything
functype-core Core types: Either, Validated, Try, NonEmptyVec, persistent collections, fdo! macro
functype-io Lazy IO effect type with resource safety and async interop

Core Types

Either<L, R>

Right-biased disjoint union. Short-circuits on Left.

use functype::prelude::*;
use functype_core::fdo;

let result: Either<&str, i32> = fdo! {
    x <- Either::<&str, i32>::right(10);
    y <- Either::<&str, i32>::right(20);
    yield x + y
};
assert_eq!(result, Either::Right(30));

Full API: map, flat_map, bimap, fold, swap, filter_or_else, ap, zip, conversions to/from Result and Option.

Validated<E, A>

Applicative validation that accumulates all errors instead of short-circuiting. Errors are collected in NonEmptyVec<E> — guaranteed non-empty at the type level.

use functype::prelude::*;

fn validate_name(s: &str) -> Validated<String, String> {
    if s.is_empty() { Validated::invalid("name required".into()) }
    else { Validated::valid(s.into()) }
}

fn validate_age(n: i32) -> Validated<String, i32> {
    if n < 0 { Validated::invalid("age must be positive".into()) }
    else { Validated::valid(n) }
}

let result = validate_name("").zip(&validate_age(-1));
// Both errors collected, not just the first
assert!(result.is_invalid());

IO<A>

Lazy, composable effect type. Nothing executes until .run().

use functype::IO;

let io = IO::effect(|| {
    println!("side effect!");
    42
}).map(|x| x * 2);

// Nothing printed yet
let result = io.run().unwrap(); // prints "side effect!", returns 84

Why IO<A> instead of three type parameters:

  • No R (environment): Rust has compile-time DI via function arguments + trait bounds.
  • No E (typed error): Result already handles typed errors. For typed errors, use IO<Result<A, E>> — opt-in, not mandatory.
  • One type param: Clean signatures. io.map(|x| x + 1) without error type propagation everywhere.

Constructors

IO::succeed(42)                              // pure value
IO::fail(anyhow::anyhow!("boom"))            // pure error
IO::effect(|| compute())                     // infallible side effect
IO::effect_result(|| Ok(compute()?))         // fallible side effect
IO::from_result(Ok(42))                      // lift Result
IO::from_option(Some(42), || error)          // lift Option
IO::from_either(Either::Right(42))           // lift Either

Combinators

io.map(|x| x + 1)                           // transform success
io.flat_map(|x| IO::succeed(x + 1))         // chain effects
io.zip(other)                                // combine into tuple
io.zip_with(other, |a, b| a + b)            // combine with function
io.then(other)                               // sequence, discard first
io.tap(|x| println!("{x}"))                  // side effect, return original

Error Handling

io.map_error(|e| anyhow::anyhow!("wrapped: {e}"))  // transform error
io.catch(|e| IO::succeed(fallback))                  // recover
io.or_else(IO::succeed(default))                     // fallback on error
io.either()                                          // IO<Either<Error, A>> (infallible)
IO::retry(|| make_request(), 3)                      // retry up to 3 times

Resource Safety

IO::bracket(
    IO::effect(|| open_file()),       // acquire
    |file| IO::effect(|| read(file)), // use
    |file| close(file),               // release (always runs)
);

io.ensuring(|| cleanup())  // finalizer runs on both success and error

Async Interop

IO::from_future(async { Ok(fetch().await?) })  // future -> IO
io.to_future().await                            // IO -> future

fdo! Macro

Scala/Haskell-style do-notation. Works with any type that has and_then and map: Option, Result, Either, Validated, IO.

use functype_core::fdo;

// Option
let result: Option<i32> = fdo! {
    x <- Some(1);
    y <- Some(2);
    yield x + y          // Some(3)
};

// Result
let result: Result<i32, &str> = fdo! {
    x <- Ok(10);
    y <- Ok(20);
    yield x + y          // Ok(30)
};

// Let bindings and guards
let result: Option<i32> = fdo! {
    x <- Some(42);
    let doubled = x * 2;
    when doubled > 0;     // short-circuit to None if false
    yield doubled
};

Try

Captures panics as values, inspired by Scala's Try.

use functype::prelude::*;

let result = try_catch(|| {
    "42".parse::<i32>().unwrap()
});
assert!(result.is_success());

let result = try_catch(|| {
    panic!("boom");
});
assert!(result.is_failure());

NonEmptyVec<T>

A Vec guaranteed to have at least one element at the type level.

use functype::prelude::*;
use functype_core::nev;

let v = nev![1, 2, 3];
assert_eq!(*v.head(), 1);    // no Option — always exists
assert_eq!(v.len(), 3);

Persistent Collections

Immutable, structurally-shared collections backed by rpds. Thread-safe via Arc.

use functype::prelude::*;
use functype_core::{list, map};

let l = list![1, 2, 3];
let l2 = l.prepend(0);
assert_eq!(l.len(), 3);   // original unchanged
assert_eq!(l2.len(), 4);

let m = map!{ "a" => 1, "b" => 2 };
let m2 = m.insert("c", 3);
assert_eq!(m.len(), 2);   // original unchanged
assert_eq!(m2.len(), 3);

Extension Traits

Adds Scala-style combinators to standard library types.

OptionExt: get_or_else, fold, to_either, to_validated, tap_some, tap_none, when, contains_where, zip_with

ResultExt: get_or_else, fold, to_either, to_validated, tap_ok, tap_err, bimap, recover, recover_with, swap

TupleExt: map_n for 2- through 5-tuples

Pure Trait

Lifts a value into any monadic context. Implemented for Option, Result, Either, Validated, and IO.

use functype::prelude::*;

let opt: Option<i32> = Pure::pure(42);    // Some(42)
let res: Result<i32, String> = Pure::pure(42); // Ok(42)

Usage

Add to your Cargo.toml:

[dependencies]
functype = { git = "https://github.com/jordanburke/functype-rs" }

Or for specific crates:

[dependencies]
functype-core = { git = "https://github.com/jordanburke/functype-rs" }
functype-io = { git = "https://github.com/jordanburke/functype-rs" }

Requirements

  • Rust 1.75+
  • Edition 2021

License

MIT OR Apache-2.0

Dependencies

~2.8–4MB
~72K SLoC