Skip to content

Latest commit

 

History

6 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

raskell

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.

Statements

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.

Errors of different types

let result: Result<i32, Outer> = hdo! {
    x <-? inner();   // inner() -> Result<i32, Inner>, Outer: From<Inner>

    pure x * 2
};

Lists

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)]);

Async

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.

Limitations

  • An async block binds Option and Result only: a list bind cannot short-circuit a future.
  • Lists are bound as Vec; other iterators need .collect() first.

Paths

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);

Segments

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.

Writing

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.

What this is not

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.

Roadmap

Functions and combinators

map, filter, fold, compose, curry, flip, zip_with, maybe, either.

Type classes, Rust-style

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.

Types

Either, NonEmpty, later Reader, State, Writer.

Optics

First-class Lens and Prism values that compose, with view! and friends kept as the shorthand for the common case.

License

MIT

About

Haskell-style functional programming for Rust

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages