Skip to main content

Crate raskell

Crate raskell 

Source
Expand description

Haskell-style functional programming for Rust.

hdo! turns a sequence of binds into a chain of monadic operations over Option, Result and Vec. The README lists the combinators, type classes and types that are 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));

§Statements

SyntaxMeaning
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 expressionLifts a plain value into the block’s monad. Must come last.
expressionA final expression without ;, already monadic. Must come last.

§The block’s result

A block ends either in pure value, which lifts a plain value into the block’s monad, or in a final expression without a semicolon, which is already an Option, Result or Vec and is returned as it is:

use raskell::hdo;

let lifted: Option<i32> = hdo! {
    x <- Some(10);

    pure x + 1
};

let monadic: Option<i32> = hdo! {
    x <- Some(10i32);

    x.checked_add(1)
};

assert_eq!(lifted, monadic);

The ; is what tells the two apart: a last statement that keeps it is an action, which leaves the block without a result and fails to compile.

§Errors of different types

Plain <- keeps the error type as it is. Use <-? to convert it through From, the way ? does:

use raskell::hdo;

#[derive(Debug, PartialEq)]
struct Inner;

#[derive(Debug, PartialEq)]
struct Outer;

impl From<Inner> for Outer {
    fn from(_: Inner) -> Self {
        Outer
    }
}

fn inner() -> Result<i32, Inner> {
    Err(Inner)
}

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

    pure x * 2
};

assert_eq!(result, Err(Outer));

§Refutable patterns

A refutable pattern filters an Option block:

use raskell::hdo;

let result = hdo! {
    Some(x) <- Some(Some(10));

    pure x * 2
};

assert_eq!(result, Some(20));

In a Result block there is no obvious error to produce, so throw is required:

use raskell::hdo;

#[derive(Debug, PartialEq)]
enum Error {
    Missing,
}

let result: Result<i32, Error> = hdo! {
    Some(x) <- Ok(None::<i32>) throw Error::Missing;

    pure x * 2
};

assert_eq!(result, Err(Error::Missing));

§Lists

A Vec bind is a list comprehension, and guard filters it:

use raskell::hdo;

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

§Type annotations

Rust infers the bound value’s type from the expression. When the expression carries no type (Err(..) says nothing about the Ok side), annotate the pattern:

use raskell::hdo;

#[derive(Debug, PartialEq)]
enum Error {
    Failed,
    Missing,
}

let result: Result<i32, Error> = hdo! {
    Some(x): Option<i32> <- Err(Error::Failed) throw Error::Missing;

    pure x * 2
};

assert_eq!(result, Err(Error::Failed));

§Actions

A bare statement expression is sequenced, not discarded, so anything monadic in statement position short-circuits the block:

use raskell::hdo;

#[derive(Debug, PartialEq)]
struct Error;

fn check() -> Result<(), Error> {
    Err(Error)
}

let result: Result<i32, Error> = hdo! {
    check();

    pure 1
};

assert_eq!(result, Err(Error));

Bind it to _ with a plain let to opt out:

let mut empty: Vec<i32> = Vec::new();

let result = hdo! {
    x <- Some(10);

    let _ = empty.pop();

    pure x
};

assert_eq!(result, Some(10));

§Async

Wrapping the statements in async { .. } expands the block into an async move block, so .await is allowed anywhere inside it: in bind expressions, guard conditions and pure alike. The result is a plain future: .await it, or spawn it.

use raskell::hdo;

#[derive(Debug, PartialEq)]
enum ApiError {
    Store,
    TooSmall,
}

#[derive(Debug, PartialEq)]
struct StoreError;

impl From<StoreError> for ApiError {
    fn from(_: StoreError) -> Self {
        ApiError::Store
    }
}

async fn fetch(value: i32) -> Result<i32, ApiError> {
    Ok(value)
}

async fn from_store() -> Result<i32, StoreError> {
    Err(StoreError)
}

let block = hdo!(async {
    x <- fetch(10).await;
    y <-? from_store().await;

    guard x > 5 throw ApiError::TooSmall;

    pure x + y
});

let result: Result<i32, ApiError> = pollster::block_on(block);

assert_eq!(result, Err(ApiError::Store));

A final expression ends an async block the same way, .await included:

use raskell::hdo;

async fn fetch(value: i32) -> Option<i32> {
    Some(value)
}

let block = hdo!(async {
    x <- fetch(10).await;

    fetch(x * 2).await
});

assert_eq!(pollster::block_on(block), Some(20));

Futures are not awaited implicitly: write the .await yourself. The block’s type has to be known, either from an annotation on the awaited value or from the enclosing function’s signature.

§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.

Modules§

prelude
The common imports.

Macros§

hdo
Haskell-style do notation over Option, Result and Vec.