2 unstable releases
| 0.1.0 | Jan 28, 2025 |
|---|---|
| 0.0.0 | Jan 10, 2025 |
#738 in Compression
694 downloads per month
Used in 3 crates
(2 directly)
25KB
911 lines
This crate provides abstractions over data and control functors,
as described in A Tale of Two Functors or: How I Learned to Stop Worrying and Love Data and Ccontrol.
The original article takes linear types into account, but in Rust we have to do with affine types.
This difference means Option and Result CAN be control functors, which are not in linear case.
Experimenting around Functorial pattern in Rust
This repository aims at exploring the followings in Rust:
- Affine Data/Control Functor hierarchy
- Multiplicative Functor hierarchy
- A port of
QualifiedDoin Haskell to Rust macro to use the above uniformly- Support
ApplicativeDoand MonadFail as well.
- Support
Showcase
use functo_rs::control::*;
use qualified_do::*;
let ans: Option<i64> = qdo! {Optioned {
i <- Some(5);
j <- Some(6);
let k = 7i64;
return i + j + k
}};
assert_eq!(ans, Some(18));
use functo_rs::control::*;
use qualified_do::*;
let ans: Option<i64> = qdo! {Optioned {
i <- Some(5);
j <- Some(6);
_k <- None::<i64>;
let k = 7i64;
return i + j + k
}};
assert_eq!(ans, None);
use functo_rs::nonlinear::*;
use qualified_do_macro::qdo;
let is = vec![1, 2, 3];
let js = vec![4, 5, 6];
let ans: Vec<i64> = qdo! {UndetVec {
i <- is.clone();
j <- js.clone();
let k = 100i64;
UndetVec::guard(i % 2 == 1);
return i + j + k
}};
assert_eq!(
ans,
is.into_iter()
.flat_map(|i| js.iter().cloned().flat_map(move |j| if i % 2 == 1 {
Some(i + j + 100)
} else {
None
}))
.collect::<Vec<_>>()
);
use functo_rs::data::*;
use qualified_do_macro::qdo;
let is = vec![1, 2, 3];
let js = vec![4, 5, 6];
let ans: Vec<i64> = qdo! {ZipVec {
i <- is.clone();
j <- js.clone();
let k = 100i64;
return i + j + k
}};
assert_eq!(
ans,
is.into_iter()
.zip(js)
.map(|(i, j)| i + j + 100)
.collect::<Vec<_>>()
);
use either::Either::*;
let a = vec![Some(1), None, Some(3)];
let b = vec![Left(4), Left(5), Right(6)];
let answer = {
let a = a.clone();
let b = b.clone();
qdo! { Iter {
Some(x) <- a;
Left(y) <- b.clone();
let z = 100i64;
return x + y + z
}}
.collect::<Vec<_>>()
};
let c = a
.into_iter()
.flatten()
.flat_map(|x| {
b.iter()
.cloned()
.flat_map(|x| x.left())
.map(move |y| x + y + 100)
})
.collect::<Vec<_>>();
assert_eq!(answer, c);
fn gen_expr() -> impl Strategy<Value = Expr> {
use qualified_do::qdo;
let leaf = any::<i32>().prop_map(Expr::Num).boxed();
leaf.prop_recursive(8, 256, 10, |inner| {
prop_oneof![
qdo! { BoxedProptest {
l <- inner.clone();
r <- inner.clone();
return Expr::Add(l.into(), r.into())
}},
qdo! { BoxedProptest {
l <- inner.clone();
r <- inner.clone();
return Expr::Mul(l.into(), r.into())
}}
]
})
}
Syntax
The qdo macro has the following syntax:
qdo!{ NAMESPACE {
stmt1;
stmt2;
...
last_stmt [;] // Last ; is optional and changes the return value
}}
NAMESPACE: module or type path to qualify control functions.stmts are do-statement, which should be one of the followings:-
let pat = expr;: let-statement for (non-effectful) local binding. -
return a: which wraps (pure) valueainto effectful context;- NOTE: This DOES NOT do any early return. It is interpreted as just a syntactic
sugar around
NAMESPACE::pure(a).
- NOTE: This DOES NOT do any early return. It is interpreted as just a syntactic
sugar around
-
[~]pat <- expr: effectful local binding. Corresponding roughly toNAMESPACE::and_then~is omittable; if~is specified, it tries to desugar into simple closure on infalliable pattern.
-
guard expr: guarding expression. Filters outexpris false. Desugared intoNAMESPACE::guard(expr). -
expr: effectful expression, with its result discarded.
-
last_stmtMUST either bereturn exprorexpr.- If there is no
;atfterlast_stmt, the final effectful value(s) will be returned. - If
last_stmtis followed by;, the values are discarded and replaced with()inside effectful context.
- If there is no
If pat is just a single identifier, it is desugared to a simple closure.
If the pat is falliable pattern, it desugars into closure with match-expression, with default value calls NAMESPACE::fail to report pattern-match failure.
Further more, if the following conditions are met, qdo-expression will be desugared in ApplicativeDo-mode, which desugars in terms of NAMESPACE::fmap, NAMESPACE::zip_with, and possibly NAMESPACE::pure:
- All
stmts butlast_stmtcontains NO varibale bound inqdo-context, - All binding patterns are identifiers, not a compound pattern,
- No
guardcondition instmtNcontains identifiers defined inqdo-context, and - The
last_stmtis of formreturn expr, whereexprcan refer to any identifier in scope including those bound in qdo.
In ApplicativeDo mode, all binding can be chained independently so they are chained with NAMESPACE::zip_with and finally mapped with fmap[^1].
[^1]: In Haskell, ApplicativeDo uses fmap, ap, and join. The reason we don't use join is that join needs nested container, which has less availability in Rust than Haskell.
ApplicativeDo utilises the independence of each binding, so in some cases you need less clone()s.
Dependencies
~470KB