/// Grand Synthesis: a capstone combining multiple category theory concepts.
///
/// This example builds a validated data pipeline that demonstrates:
///
/// 1. Sum types & product types (data modeling)
/// 2. Curry-Howard (type-safe proofs as validation functions)
/// 3. Profunctor (adapting validation pipeline inputs/outputs)
/// 4. Comonad (context-aware processing with Env comonad)
/// 5. Adjunctions (State monad for pipeline state)
/// 6. Limits/Colimits (products for combining validations, sums for errors)
/// 7. Validated applicative (accumulating errors โ not short-circuiting)
///
/// The pipeline: raw input -> validated -> enriched -> formatted output
use std::collections::HashMap;
use std::rc::Rc;
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// 1. SUM + PRODUCT TYPES โ The data model
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
#[derive(Debug, Clone, PartialEq)]
enum ValidationError {
TooShort { field: String, min: usize, got: usize },
TooLong { field: String, max: usize, got: usize },
OutOfRange { field: String, min: i64, max: i64, got: i64 },
MissingChar { field: String, required: char },
InvalidFormat { field: String, reason: String },
}
impl ValidationError {
fn message(&self) -> String {
match self {
ValidationError::TooShort { field, min, got } =>
format!("{}: too short (min {}, got {})", field, min, got),
ValidationError::TooLong { field, max, got } =>
format!("{}: too long (max {}, got {})", field, max, got),
ValidationError::OutOfRange { field, min, max, got } =>
format!("{}: {} out of range [{}, {}]", field, got, min, max),
ValidationError::MissingChar { field, required } =>
format!("{}: missing required character '{}'", field, required),
ValidationError::InvalidFormat { field, reason } =>
format!("{}: invalid format โ {}", field, reason),
}
}
}
#[derive(Debug, Clone)]
struct RawInput {
name: String,
age: String,
email: String,
role: String,
}
#[derive(Debug, Clone, PartialEq)]
struct ValidatedUser {
name: String,
age: u32,
email: String,
role: Role,
}
#[derive(Debug, Clone, PartialEq)]
enum Role {
Admin,
Editor,
Viewer,
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// 2. VALIDATED APPLICATIVE โ Colimit of errors (accumulating)
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
/// Validated<A>: either a valid A or a non-empty list of errors.
/// Unlike Result, multiple errors accumulate (applicative style).
#[derive(Debug, Clone)]
enum Validated<A> {
Ok(A),
Err(Vec<ValidationError>),
}
impl<A: Clone> Validated<A> {
fn map<B: Clone>(&self, f: impl Fn(A) -> B) -> Validated<B> {
match self {
Validated::Ok(a) => Validated::Ok(f(a.clone())),
Validated::Err(es) => Validated::Err(es.clone()),
}
}
fn and_then<B: Clone>(self, f: impl Fn(A) -> Validated<B>) -> Validated<B> {
match self {
Validated::Ok(a) => f(a),
Validated::Err(es) => Validated::Err(es),
}
}
fn is_ok(&self) -> bool {
matches!(self, Validated::Ok(_))
}
}
/// Applicative `apply`: accumulate errors from both sides.
/// This is the PRODUCT of two validated results (limit construction).
fn validated_apply<A: Clone, B: Clone>(
vf: Validated<impl Fn(A) -> B + Clone>,
va: Validated<A>,
) -> Validated<B> {
match (vf, va) {
(Validated::Ok(f), Validated::Ok(a)) => Validated::Ok(f(a)),
(Validated::Err(e1), Validated::Ok(_)) => Validated::Err(e1),
(Validated::Ok(_), Validated::Err(e2)) => Validated::Err(e2),
(Validated::Err(mut e1), Validated::Err(e2)) => {
e1.extend(e2); // key: accumulate ALL errors
Validated::Err(e1)
}
}
}
fn ok<A>(a: A) -> Validated<A> { Validated::Ok(a) }
fn err<A>(e: ValidationError) -> Validated<A> { Validated::Err(vec![e]) }
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// 3. PROFUNCTOR โ Adapting validation functions
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
/// A validator is a Mapper<A, Validated<B>>.
/// We can use profunctor operations to adapt it.
struct Validator<A: 'static, B: Clone + 'static> {
f: Rc<dyn Fn(A) -> Validated<B>>,
}
impl<A: Clone + 'static, B: Clone + 'static> Validator<A, B> {
fn new(f: impl Fn(A) -> Validated<B> + 'static) -> Self {
Validator { f: Rc::new(f) }
}
fn apply(&self, a: A) -> Validated<B> {
(self.f)(a)
}
/// lmap: pre-process the input (contravariant)
fn lmap<C: Clone + 'static>(self, pre: impl Fn(C) -> A + 'static) -> Validator<C, B> {
Validator::new(move |c| (self.f)(pre(c)))
}
/// rmap: post-process the output (covariant)
fn rmap<D: Clone + 'static>(self, post: impl Fn(B) -> D + 'static) -> Validator<A, D> {
let f = self.f.clone();
Validator::new(move |a| f(a).map(|b| post(b)))
}
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// 4. ENV COMONAD โ Context-aware processing
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
/// Validation context: settings that affect validation rules.
#[derive(Debug, Clone)]
struct ValidationConfig {
min_name_len: usize,
max_name_len: usize,
min_age: u32,
max_age: u32,
allowed_roles: Vec<String>,
strict_email: bool,
}
impl Default for ValidationConfig {
fn default() -> Self {
ValidationConfig {
min_name_len: 2,
max_name_len: 50,
min_age: 0,
max_age: 130,
allowed_roles: vec!["admin".to_string(), "editor".to_string(), "viewer".to_string()],
strict_email: true,
}
}
}
/// Env comonad: value paired with environment
#[derive(Clone)]
struct Env<E: Clone, A: Clone> {
env: E,
value: A,
}
impl<E: Clone, A: Clone> Env<E, A> {
fn new(env: E, value: A) -> Self { Env { env, value } }
fn extract(&self) -> A { self.value.clone() }
fn ask(&self) -> E { self.env.clone() }
fn extend<B: Clone>(&self, f: impl Fn(&Env<E, A>) -> B) -> Env<E, B> {
Env { env: self.env.clone(), value: f(self) }
}
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// 5. STATE MONAD โ Pipeline state (from adjunction)
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
#[derive(Debug, Clone, Default)]
struct PipelineState {
processed: usize,
errors: usize,
warnings: Vec<String>,
}
#[derive(Clone)]
struct State<S: Clone, A: Clone> {
run_fn: Rc<dyn Fn(S) -> (A, S)>,
}
impl<S: Clone + 'static, A: Clone + 'static> State<S, A> {
fn new(f: impl Fn(S) -> (A, S) + 'static) -> Self {
State { run_fn: Rc::new(f) }
}
fn run(&self, s: S) -> (A, S) { (self.run_fn)(s) }
fn return_(a: A) -> Self { State::new(move |s| (a.clone(), s)) }
fn bind<B: Clone + 'static>(self, f: impl Fn(A) -> State<S, B> + 'static) -> State<S, B> {
State::new(move |s| {
let (a, s2) = self.run(s);
f(a).run(s2)
})
}
}
fn modify<S: Clone + 'static>(f: impl Fn(S) -> S + 'static) -> State<S, ()> {
State::new(move |s| ((), f(s)))
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// 6. VALIDATORS using Env comonad + Validated applicative
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
fn validate_name(cfg: &ValidationConfig, name: &str) -> Validated<String> {
let len = name.trim().len();
if len < cfg.min_name_len {
err(ValidationError::TooShort { field: "name".into(), min: cfg.min_name_len, got: len })
} else if len > cfg.max_name_len {
err(ValidationError::TooLong { field: "name".into(), max: cfg.max_name_len, got: len })
} else {
ok(name.trim().to_string())
}
}
fn validate_age_str(cfg: &ValidationConfig, age_str: &str) -> Validated<u32> {
match age_str.trim().parse::<u32>() {
Err(_) => err(ValidationError::InvalidFormat {
field: "age".into(),
reason: format!("'{}' is not a valid number", age_str)
}),
Ok(n) if n < cfg.min_age || n > cfg.max_age =>
err(ValidationError::OutOfRange {
field: "age".into(),
min: cfg.min_age as i64, max: cfg.max_age as i64, got: n as i64
}),
Ok(n) => ok(n),
}
}
fn validate_email(cfg: &ValidationConfig, email: &str) -> Validated<String> {
let e = email.trim();
if !e.contains('@') {
return err(ValidationError::MissingChar { field: "email".into(), required: '@' });
}
if cfg.strict_email {
let parts: Vec<&str> = e.splitn(2, '@').collect();
if parts[0].is_empty() || !parts[1].contains('.') {
return err(ValidationError::InvalidFormat {
field: "email".into(),
reason: "must be user@domain.tld".into()
});
}
}
ok(e.to_string())
}
fn validate_role(cfg: &ValidationConfig, role_str: &str) -> Validated<Role> {
match role_str.trim().to_lowercase().as_str() {
"admin" if cfg.allowed_roles.contains(&"admin".to_string()) => ok(Role::Admin),
"editor" if cfg.allowed_roles.contains(&"editor".to_string()) => ok(Role::Editor),
"viewer" if cfg.allowed_roles.contains(&"viewer".to_string()) => ok(Role::Viewer),
other => err(ValidationError::InvalidFormat {
field: "role".into(),
reason: format!("'{}' not in allowed roles: {:?}", other, cfg.allowed_roles)
}),
}
}
/// Validate raw input using the Env comonad (config as environment).
fn validate_with_env(env: &Env<ValidationConfig, RawInput>) -> Validated<ValidatedUser> {
let cfg = env.ask();
let raw = env.extract();
// Use Validated applicative: accumulate ALL errors
let vname = validate_name(&cfg, &raw.name);
let vage = validate_age_str(&cfg, &raw.age);
let vemail = validate_email(&cfg, &raw.email);
let vrole = validate_role(&cfg, &raw.role);
// Combine via applicative (product = limit):
// ValidatedUser { name, age, email, role }
let make_user = |name: String| move |age: u32| {
let name2 = name.clone();
move |email: String| {
let name3 = name2.clone();
let email2 = email.clone();
move |role: Role| ValidatedUser { name: name3.clone(), age, email: email2.clone(), role }
}
};
validated_apply(
validated_apply(
validated_apply(
vname.map(make_user),
vage,
),
vemail,
),
vrole,
)
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// 7. STATE-TRACKED PIPELINE
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
fn process_one(
cfg: ValidationConfig,
raw: RawInput,
) -> State<PipelineState, Result<ValidatedUser, Vec<String>>> {
let env = Env::new(cfg, raw);
let result = validate_with_env(&env);
match result {
Validated::Ok(user) => {
let user2 = user.clone();
modify(|mut s: PipelineState| { s.processed += 1; s })
.bind(move |_| State::return_(Result::Ok(user2.clone())))
}
Validated::Err(errors) => {
let msgs: Vec<String> = errors.iter().map(|e| e.message()).collect();
let msgs2 = msgs.clone();
modify(|mut s: PipelineState| { s.errors += 1; s })
.bind(move |_| State::return_(Result::Err(msgs2.clone())))
}
}
}
fn main() {
println!("=== Grand Synthesis: Category Theory in a Validated Pipeline ===\n");
println!("Combining: Sum/Product Types, Validated Applicative, Profunctor,");
println!(" Env Comonad, State Monad (from adjunction)\n");
let cfg = ValidationConfig::default();
// โโ Test cases โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let inputs = vec![
("Alice (valid)", RawInput {
name: "Alice".into(), age: "30".into(),
email: "alice@example.com".into(), role: "admin".into()
}),
("Bob (valid)", RawInput {
name: "Bob".into(), age: "25".into(),
email: "bob@corp.io".into(), role: "viewer".into()
}),
("X (short name)", RawInput {
name: "X".into(), age: "25".into(),
email: "x@test.com".into(), role: "editor".into()
}),
("Multi-error case", RawInput {
name: "A".into(), age: "999".into(),
email: "not-an-email".into(), role: "superuser".into()
}),
("Bad age format", RawInput {
name: "Charlie".into(), age: "twenty".into(),
email: "c@d.org".into(), role: "viewer".into()
}),
];
println!("{:-<60}", "");
// Run pipeline using State monad + Env comonad
let mut state = PipelineState::default();
let mut results: Vec<(String, Result<ValidatedUser, Vec<String>>)> = Vec::new();
for (label, raw) in &inputs {
let computation = process_one(cfg.clone(), raw.clone());
let (result, new_state) = computation.run(state);
state = new_state;
results.push((label.to_string(), result));
}
for (label, result) in &results {
match result {
Result::Ok(user) => {
println!("โ {}", label);
println!(" User: name={}, age={}, email={}, role={:?}",
user.name, user.age, user.email, user.role);
}
Result::Err(errors) => {
println!("โ {}", label);
for e in errors {
println!(" Error: {}", e);
}
}
}
println!();
}
println!("{:-<60}", "");
println!("Pipeline statistics:");
println!(" Processed (valid): {}", state.processed);
println!(" Failed (with errors): {}", state.errors);
println!(" Total inputs: {}", inputs.len());
// โโ Profunctor demonstration โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
println!();
println!("โโ Profunctor: adapt validators โโ\n");
// Validator<String, String> for name
let name_validator = Validator::new(|name: String| validate_name(&ValidationConfig::default(), &name));
// lmap: accept &str instead of String (contravariant)
let name_validator_str = name_validator.lmap(|s: &str| s.to_string());
// rmap: uppercase the valid name (covariant)
let name_upper = name_validator_str.rmap(|n: String| n.to_uppercase());
println!("Profunctor dimap (name validator, &str -> uppercase):");
match name_upper.apply("alice") {
Validated::Ok(n) => println!(" \"alice\" -> Ok(\"{}\")", n),
Validated::Err(_) => println!(" Error"),
}
match name_upper.apply("X") {
Validated::Ok(n) => println!(" \"X\" -> Ok(\"{}\")", n),
Validated::Err(es) => println!(" \"X\" -> Err({})", es[0].message()),
}
// โโ Summary of CT concepts used โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
println!();
println!("Category theory concepts demonstrated:");
println!(" Sum types โ ValidationError (coproduct), Role");
println!(" Product types โ ValidatedUser (tuple/record), ValidationConfig");
println!(" Validated โ applicative functor (accumulates errors via coproduct)");
println!(" validated_apply โ product of validations (limit construction)");
println!(" Profunctor โ Validator<A,B> with lmap/rmap for adapting validators");
println!(" Env comonad โ ValidationConfig as read-only environment");
println!(" State monad โ PipelineState tracking processed/error counts");
println!(" Adjunction โ State = GโF for Product โฃ Exponential");
println!(" Curry-Howard โ each validator IS a proof (fn A -> Validated B)");
}
#[cfg(test)]
mod tests {
use super::*;
fn default_cfg() -> ValidationConfig { ValidationConfig::default() }
#[test]
fn test_valid_user() {
let raw = RawInput {
name: "Alice".into(), age: "30".into(),
email: "alice@example.com".into(), role: "admin".into(),
};
let env = Env::new(default_cfg(), raw);
let result = validate_with_env(&env);
assert!(result.is_ok());
}
#[test]
fn test_name_too_short() {
match validate_name(&default_cfg(), "X") {
Validated::Err(es) => assert!(matches!(&es[0], ValidationError::TooShort { .. })),
_ => panic!("expected error"),
}
}
#[test]
fn test_multiple_errors_accumulate() {
let raw = RawInput {
name: "A".into(), age: "999".into(),
email: "bademail".into(), role: "superuser".into(),
};
let env = Env::new(default_cfg(), raw);
match validate_with_env(&env) {
Validated::Err(es) => assert!(es.len() >= 3, "expected 3+ errors, got {}", es.len()),
_ => panic!("expected errors"),
}
}
#[test]
fn test_state_pipeline() {
let cfg = default_cfg();
let raw_ok = RawInput { name: "Bob".into(), age: "25".into(), email: "b@c.com".into(), role: "viewer".into() };
let raw_err = RawInput { name: "Z".into(), age: "999".into(), email: "bad".into(), role: "x".into() };
let prog = process_one(cfg.clone(), raw_ok)
.bind(|_| process_one(cfg.clone(), raw_err));
let (_, final_state) = prog.run(PipelineState::default());
assert_eq!(final_state.processed, 1);
assert_eq!(final_state.errors, 1);
}
#[test]
fn test_profunctor_validator() {
let v = Validator::new(|n: String| validate_name(&ValidationConfig::default(), &n));
let v_upper = v.rmap(|n: String| n.to_uppercase());
match v_upper.apply("alice".to_string()) {
Validated::Ok(s) => assert_eq!(s, "ALICE"),
_ => panic!("expected Ok"),
}
}
#[test]
fn test_env_comonad_extend() {
let env = Env::new(default_cfg(), "Alice".to_string());
let extended = env.extend(|e| {
let name = e.extract();
let cfg = e.ask();
name.len() >= cfg.min_name_len
});
assert!(extended.extract());
}
}