Haskell-style functional programming for Rust: combinators, type classes and types.
Implemented: hdo!, do notation over Option, Result and Vec, with an
async mode; and view! / set! / over!, which read and write through a
path into nested data. See the roadmap for what is planned.
use raskell::hdo;
let result = hdo! {
x <- Some(10);
y <- Some(20);
let sum = x + y;
guard sum > 5;
pure sum * 2
};
assert_eq!(result, Some(60));hdo! chains binds over Option, Result and Vec.
| Syntax | Meaning |
|---|---|
pattern <- expression; |
Bind. Short-circuits on None / Err, iterates over a Vec. |
pattern <-? expression; |
Bind a Result, converting the error through From. |
pattern <- expression throw error; |
Bind a Result whose pattern may fail to match. |
pattern: Type <- expression; |
Bind with the bound value's type spelled out. |
guard condition; |
Yield None (or drop the list element) unless condition holds. |
guard condition throw error; |
Yield Err(error) unless condition holds. |
let pattern[: Type] = expression; |
Plain let, no monad involved. |
expression; |
An action, sequenced like a bind whose value is discarded. |
pure expression |
Lifts a plain value into the block's monad. Must come last. |
expression |
A final expression without ;, already monadic. Must come last. |
A block has to end in a result: either pure value, which lifts a plain value,
or a final expression without a semicolon, which is already an Option,
Result or Vec and is returned as it is.
let lifted: Option<i32> = hdo! {
x <- Some(10i32);
pure x + 1 // an i32, so `pure` wraps it
};
let monadic: Option<i32> = hdo! {
x <- Some(10i32);
x.checked_add(1) // already an Option<i32>, returned as it is
};A last statement that keeps its ; is still an action, so the block is missing
its result and fails to compile.
let result: Result<i32, Outer> = hdo! {
x <-? inner(); // inner() -> Result<i32, Inner>, Outer: From<Inner>
pure x * 2
};let pairs: Vec<(i32, i32)> = hdo! {
x <- vec![1, 2, 3];
y <- vec![10, 20];
guard x + y > 12;
pure (x, y)
};
assert_eq!(pairs, vec![(1, 20), (2, 20), (3, 10), (3, 20)]);Wrapping the statements in async { .. } expands the block into an async move
block, so .await works anywhere inside it. The result is a plain future:
.await it or spawn it.
let result: Result<i32, ApiError> = hdo!(async {
x <- fetch(10).await;
y <-? from_store().await; // ApiError: From<StoreError>
guard x > 5 throw ApiError::TooSmall;
pure x + y
})
.await;A final expression works there too, .await included:
let user: Result<User, Error> = hdo!(async {
id <-? get_id().await;
fetch_user(id).await
})
.await;Futures are not awaited implicitly; write the .await yourself.
- An
asyncblock bindsOptionandResultonly: a list bind cannot short-circuit a future. - Lists are bound as
Vec; other iterators need.collect()first.
view!, set! and over! walk a path into nested data, replacing a chain of
get and and_then:
// before
let enabled = settings
.get("settings")
.and_then(|s| s.get("mention_prefix"))
.and_then(|v| v.as_bool())
.unwrap_or(false);
// after
let enabled = view!(settings, ["settings"]["mention_prefix"].as_bool()?).unwrap_or(false);| Syntax | Meaning |
|---|---|
.name |
A struct field. The leading . is left out on the first segment. |
.name(args) |
A method call, whose value the rest of the path continues from. |
[key] |
A lookup through get / get_mut. Always fallible. |
? |
Flattens the Option at this point. |
A [key] lookup covers everything with an inherent get: serde_json::Value,
HashMap, BTreeMap, Vec. Raskell does not depend on serde_json, and
there is no trait to implement.
The path is relative to the target, and view! yields an Option when any
segment can miss, and the value itself otherwise:
let username: Option<&String> = view!(user, profile?.username?);
let name: &String = view!(user, name);There is no view_or!: the result is a plain Option, so unwrap_or,
unwrap_or_default, unwrap_or_else, map_or and ok_or all apply.
set! writes through a path, over! replaces the value with a function of it.
Both answer true when the path existed, and do nothing when it did not:
set!(user, profile?.username?, "grace".to_string());
over!(user, profile?.username?, |name| name.to_uppercase());
if !set!(config, ["limits"]["max"], json!(10)) {
// the key was not there
}over! is given a &T and returns the new value, so the target is never
cloned.
A path is syntax, not a value: it expands to plain field access and get
calls, with nothing boxed, allocated or type-erased. In exchange it cannot be
stored in a variable, passed to a function or composed — that is what real
Lens and Prism types are for, and they are not here yet.
A method call cannot be written through, since its value is a temporary, so
set! and over! reject one. Neither creates a missing key.
See the crate documentation for the full reference.
map, filter, fold, compose, curry, flip, zip_with, maybe,
either.
Functor, Applicative, Monad, Foldable, Traversable, Semigroup,
Monoid, as plain traits that compose with Iterator and the std traits.
Once Monad exists, hdo! should be defined in terms of it instead of the
per-type HdoBind impls it dispatches on today.
Either, NonEmpty, later Reader, State, Writer.
First-class Lens and Prism values that compose, with view! and friends
kept as the shorthand for the common case.
MIT