karpal-core 0.5.0

HKT encoding, Functor, Semigroup, Monoid for the Industrial Algebra ecosystem
Documentation

karpal-core

Core algebraic structures for Rust: HKT encoding, a full functor hierarchy (Functor through Monad), Semigroup, Monoid, adjunctions, ends/coends, dinatural transformations, and do_!/ado_! notation macros.

What's inside

HKT encoding

GAT-based Higher-Kinded Type encoding with zero dependencies:

use karpal_core::hkt::{HKT, HKT2, OptionF, ResultF, VecF, ResultBF, TupleF};

// HKT  — OptionF::Of<T> = Option<T>, ResultF<E>::Of<T> = Result<T, E>, VecF::Of<T> = Vec<T>
// HKT2 — ResultBF::P<A, B> = Result<B, A>, TupleF::P<A, B> = (A, B)

Functor hierarchy

Trait Supertrait Instances
Functor HKT OptionF, ResultF, VecF
Apply Functor OptionF, ResultF, VecF
Applicative Apply OptionF, ResultF, VecF
Chain Apply OptionF, ResultF, VecF
Monad Applicative + Chain blanket impl
Alt Functor OptionF, ResultF, VecF
Plus Alt OptionF, VecF
Alternative Applicative + Plus blanket impl
Foldable HKT OptionF, ResultF, VecF
Traversable Functor + Foldable OptionF, ResultF, VecF
FunctorFilter Functor OptionF, VecF
Selective Applicative OptionF
Contravariant HKT PredicateF
Bifunctor HKT2 ResultBF, TupleF
NaturalTransformation HKT OptionToVec, VecHeadToOption
Adjunction HKT IdentityAdj, CurryAdj
ContravariantAdjunction HKT ContAdj
DinaturalTransformation HKT2 DinaturalId
End HKT2 trait with generic run<A>
Coend HKT2 struct with elim
ComposeF HKT Functor composition

Chain and do_! — sequential, dependent computations

When each step depends on the result of the previous one, do_! replaces nested .and_then() chains with flat, readable notation:

use karpal_core::{do_, Applicative};
use karpal_core::hkt::OptionF;

// A multi-step lookup where each step can fail
fn resolve(items: &[(u32, &str)], aliases: &[(&str, &str)], id: u32) -> Option<String> {
    do_! { OptionF;
        name = items.iter().find(|(i, _)| *i == id).map(|(_, n)| *n);
        alias = aliases.iter().find(|(n, _)| *n == name).map(|(_, a)| *a);
        OptionF::pure(format!("{} ({})", name, alias))
    }
}

let items = vec![(1, "alice"), (2, "bob")];
let aliases = vec![("alice", "A"), ("bob", "B")];
assert_eq!(resolve(&items, &aliases, 1), Some("alice (A)".into()));
assert_eq!(resolve(&items, &aliases, 99), None); // first step fails → short-circuits

Applicative and ado_! — independent computations combined

Unlike do_!, ado_! makes it explicit that the computations don't depend on each other. All bindings are evaluated independently, then combined in the yield:

use karpal_core::{ado_, Applicative};
use karpal_core::hkt::OptionF;

// All three lookups are independent — if any fails, the whole thing fails
fn load_config(env: &[(&str, &str)]) -> Option<String> {
    let find = |key: &str| env.iter().find(|(k, _)| *k == key).map(|(_, v)| v.to_string());
    ado_! { OptionF;
        host = find("HOST");
        port = find("PORT");
        yield format!("{}:{}", host, port)
    }
}

let env = vec![("HOST", "localhost"), ("PORT", "5432")];
assert_eq!(load_config(&env), Some("localhost:5432".into()));

let env = vec![("HOST", "localhost")]; // PORT missing
assert_eq!(load_config(&env), None);

Traversable — batch operations that fail fast

"Run this fallible operation on every element; if any one fails, the whole batch fails." traverse does this without manual loops or early-return boilerplate:

use karpal_core::Traversable;
use karpal_core::hkt::{OptionF, VecF};

let ids = vec!["100", "200", "300"];
let parsed: Option<Vec<u64>> = VecF::traverse::<OptionF, _, _, _>(
    ids, |s| s.parse::<u64>().ok(),
);
assert_eq!(parsed, Some(vec![100, 200, 300]));

// One invalid entry poisons the whole batch
let ids = vec!["100", "not_a_number", "300"];
let parsed: Option<Vec<u64>> = VecF::traverse::<OptionF, _, _, _>(
    ids, |s| s.parse::<u64>().ok(),
);
assert_eq!(parsed, None);

Alt — fallback chains

Try multiple strategies in order, taking the first success:

use karpal_core::Alt;
use karpal_core::hkt::OptionF;

fn resolve_timeout(flag: Option<u64>, env: Option<u64>, default: u64) -> u64 {
    // flag overrides env, env overrides default
    OptionF::alt(OptionF::alt(flag, env), Some(default)).unwrap()
}

assert_eq!(resolve_timeout(Some(5), Some(30), 60), 5);
assert_eq!(resolve_timeout(None, Some(30), 60), 30);
assert_eq!(resolve_timeout(None, None, 60), 60);

Foldable — generic aggregation with Monoid

Summarize any foldable structure using any Monoid. The fold knows nothing about the container or the summary type — it just needs combine and empty:

use karpal_core::{Foldable, Monoid, Semigroup};
use karpal_core::hkt::VecF;

struct Histogram { buckets: Vec<(String, u32)> }

impl Semigroup for Histogram {
    fn combine(mut self, other: Self) -> Self {
        for (key, count) in other.buckets {
            if let Some(entry) = self.buckets.iter_mut().find(|(k, _)| *k == key) {
                entry.1 += count;
            } else {
                self.buckets.push((key, count));
            }
        }
        self
    }
}
impl Monoid for Histogram {
    fn empty() -> Self { Histogram { buckets: vec![] } }
}

let events = vec!["click", "view", "click", "view", "view"];
let hist: Histogram = VecF::fold_map(events, |e| {
    Histogram { buckets: vec![(e.to_string(), 1)] }
});
let clicks = hist.buckets.iter().find(|(k, _)| k == "click").unwrap().1;
let views = hist.buckets.iter().find(|(k, _)| k == "view").unwrap().1;
assert_eq!(clicks, 2);
assert_eq!(views, 3);

Bifunctor — map both sides of a Result or tuple

use karpal_core::Bifunctor;
use karpal_core::hkt::{ResultBF, TupleF};

// Map the error to a structured type and the success to a display string
let result: Result<&str, &str> = Err("connection refused");
let mapped = ResultBF::bimap(result, |e| format!("NetworkError: {}", e), |v| v.len());
assert_eq!(mapped, Err("NetworkError: connection refused".to_string()));

// Transform both halves of a key-value pair
let entry = ("temperature", 98.6_f64);
let display = TupleF::bimap(entry, |k| k.to_uppercase(), |v| format!("{:.1}F", v));
assert_eq!(display, ("TEMPERATURE".to_string(), "98.6F".to_string()));

FunctorFilter — map and filter in one pass

use karpal_core::FunctorFilter;
use karpal_core::hkt::VecF;

// Parse valid entries, silently skip malformed ones
let raw = vec!["42", "bad", "7", "", "13"];
let valid: Vec<i32> = VecF::filter_map(raw, |s| s.parse().ok());
assert_eq!(valid, vec![42, 7, 13]);

Semigroup / Monoid

use karpal_core::{Semigroup, Monoid};

// Combine anything associative
assert_eq!(vec![1, 2].combine(vec![3, 4]), vec![1, 2, 3, 4]);
assert_eq!(Some(3i32).combine(Some(4)), Some(7));

// Monoid adds an identity element
assert_eq!(Vec::<i32>::empty().combine(vec![1, 2]), vec![1, 2]);

// Tuple instances combine component-wise
assert_eq!((1i32, 10i32).combine((2, 20)), (3, 30));

Instances: all numeric types (additive), String, Vec<T>, Option<T: Semigroup>, (A, B).

Newtype wrappers

Select alternative Semigroup/Monoid instances for numeric types:

use karpal_core::{Sum, Product, Min, Max, First, Last, Semigroup, Monoid, Foldable};
use karpal_core::hkt::VecF;

// Product uses multiplication instead of addition
let product = VecF::fold_map(vec![1, 2, 3, 4], |x| Product(x));
assert_eq!(product, Product(24));

// Min/Max for ordered types
assert_eq!(Min(3i32).combine(Min(7)), Min(3));
assert_eq!(Max(3i32).combine(Max(7)), Max(7));

// First/Last pick the first/last Some value
assert_eq!(First(None::<i32>).combine(First(Some(2))), First(Some(2)));
assert_eq!(Last(Some(1i32)).combine(Last(None)), Last(Some(1)));

Features

Feature Default Description
std yes Enables Vec, String, PredicateF instances
alloc no Same instances via alloc (for no_std)

License

AGPL-3.0-or-later