raskell 0.1.0

Haskell-style functional programming for Rust
Documentation
use pollster::block_on;
use raskell::hdo;

#[derive(Debug, PartialEq)]
enum StoreError {
    Unavailable,
}

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

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

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

async fn value_of(value: i32) -> Result<i32, ApiError> {
    value
        .checked_mul(1)
        .map(Ok)
        .unwrap_or(Err(ApiError::Missing))
}

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

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

#[test]
fn awaits_inside_every_position() {
    let result: Result<i32, ApiError> = block_on(hdo!(async {
        x <- fetch(10).await;
        y <- fetch(20).await;

        let sum = x + y;

        guard fetch(sum).await? > 5 throw ApiError::TooSmall;

        pure sum
    }));

    assert_eq!(result, Ok(30));
}

#[test]
fn short_circuits_on_err() {
    let result: Result<i32, ApiError> = block_on(hdo!(async {
        x <- fetch(1).await;
        y: i32 <- Err(ApiError::Missing);

        pure x + y
    }));

    assert_eq!(result, Err(ApiError::Missing));
}

#[test]
fn try_bind_converts_error() {
    let result: Result<i32, ApiError> = block_on(hdo!(async {
        x <-? from_store().await;

        pure x
    }));

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

#[test]
fn option_block() {
    let result: Option<i32> = block_on(hdo!(async {
        x <- maybe(Some(10)).await;
        y <- maybe(Some(20)).await;

        guard x + y > 5;

        pure x + y
    }));

    assert_eq!(result, Some(30));
}

#[test]
fn option_block_short_circuits() {
    let result: Option<i32> = block_on(hdo!(async {
        x <- maybe(Some(10)).await;
        y <- maybe(None).await;

        pure x + y
    }));

    assert_eq!(result, None);
}

#[test]
fn guard_throws() {
    let result: Result<i32, ApiError> = block_on(hdo!(async {
        x <- fetch(1).await;

        guard x > 5 throw ApiError::TooSmall;

        pure x
    }));

    assert_eq!(result, Err(ApiError::TooSmall));
}

#[test]
fn refutable_pattern_with_throw() {
    async fn nickname() -> Result<Option<String>, ApiError> {
        Ok(None)
    }

    let result: Result<String, ApiError> = block_on(hdo!(async {
        Some(name) <- nickname().await throw ApiError::Missing;

        pure name
    }));

    assert_eq!(result, Err(ApiError::Missing));
}

#[test]
fn refutable_pattern_in_option_block() {
    let result: Option<i32> = block_on(hdo!(async {
        Some(x) <- maybe(Some(10)).await.map(Some);

        pure x
    }));

    assert_eq!(result, Some(10));
}

#[test]
fn action_short_circuits() {
    async fn check() -> Result<(), ApiError> {
        Err(ApiError::Missing)
    }

    let result: Result<i32, ApiError> = block_on(hdo!(async {
        check().await;

        pure 1
    }));

    assert_eq!(result, Err(ApiError::Missing));
}

#[test]
fn unit_statements_do_not_short_circuit() {
    let result: Result<i32, ApiError> = block_on(hdo!(async {
        x <- fetch(10).await;

        println!("bound {x}");

        pure x
    }));

    assert_eq!(result, Ok(10));
}

#[test]
fn ascription_pins_the_bound_type() {
    let result: Result<i32, ApiError> = block_on(hdo!(async {
        Some(x): Option<i32> <- Err(ApiError::Missing) throw ApiError::TooSmall;

        pure x
    }));

    assert_eq!(result, Err(ApiError::Missing));
}

#[test]
fn the_block_is_a_plain_future() {
    // Deliberately not an `async fn`: the block is already a future.
    #[allow(clippy::manual_async_fn)]
    fn spawnable() -> impl Future<Output = Result<i32, ApiError>> {
        hdo!(async {
            x <- fetch(10).await;

            pure x
        })
    }

    assert_eq!(block_on(spawnable()), Ok(10));
}

#[test]
fn the_future_is_send_and_static() {
    fn assert_spawnable<F>(future: F) -> F
    where
        F: Future<Output = Result<i32, ApiError>> + Send + 'static,
    {
        future
    }

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

        guard x > 5 throw ApiError::TooSmall;

        pure x + y
    }));

    assert_eq!(block_on(future), Err(ApiError::Store));
}

#[test]
fn user_bindings_named_like_the_expansion_are_untouched() {
    let __raskell_value = 100;
    let __raskell_bound = 200;
    let __raskell_short = 300;

    let result: Result<i32, ApiError> = block_on(hdo!(async {
        Some(x) <- fetch(1).await.map(Some) throw ApiError::Missing;

        pure x + __raskell_value + __raskell_bound + __raskell_short
    }));

    assert_eq!(result, Ok(601));
}

#[test]
fn nothing_after_a_failure_is_awaited() {
    let calls = std::cell::Cell::new(0);
    // The block captures by value, so hand it a reference to the counter.
    let counter = &calls;

    let result: Result<i32, ApiError> = block_on(hdo!(async {
        x <- fetch(1).await;
        y: i32 <- Err(ApiError::Missing);

        counter.set(counter.get() + 1);

        pure x + y
    }));

    assert_eq!(result, Err(ApiError::Missing));
    assert_eq!(calls.get(), 0);
}

#[test]
fn option_actions_short_circuit() {
    let result: Option<i32> = block_on(hdo!(async {
        x <- maybe(Some(10)).await;

        maybe(None).await;

        pure x
    }));

    assert_eq!(result, None);
}

#[test]
fn try_bind_with_a_refutable_pattern() {
    async fn stored() -> Result<Option<i32>, StoreError> {
        Ok(None)
    }

    let result: Result<i32, ApiError> = block_on(hdo!(async {
        Some(x) <-? stored().await throw ApiError::Missing;

        pure x
    }));

    assert_eq!(result, Err(ApiError::Missing));
}

#[test]
fn a_sync_block_nests_inside_an_async_one() {
    let result: Result<i32, ApiError> = block_on(hdo!(async {
        x <- fetch(10).await;

        y <- hdo! {
            z <- Ok(2);

            pure z * 10
        };

        pure x + y
    }));

    assert_eq!(result, Ok(30));
}

#[test]
fn items_and_typed_lets_survive_expansion() {
    let result: Result<i64, ApiError> = block_on(hdo!(async {
        fn triple(value: i64) -> i64 {
            value * 3
        }

        x <- fetch(10).await;

        let y: i64 = triple(i64::from(x));

        pure y
    }));

    assert_eq!(result, Ok(30));
}

#[test]
fn an_async_block_without_binds_still_wraps_pure() {
    let result: Result<i32, ApiError> = block_on(hdo!(async {
        let x = 20;

        pure x
    }));

    assert_eq!(result, Ok(20));
}