hado-rshado — A little macro for writing haskell-like do expressions without too much ceremony

Related tags

Utilities hado-rs
Overview

hado

Monadic haskell-like expressions brought to rust via the hado! macro

What?

A little macro for writing haskell-like do expressions without too much ceremony

Why?

Rust is a very explicit language when it comes to errors (via Option and Result types) but it can get cumbersome to handle all of them, so this library brings the composable monad pattern from haskell like languages.

Let's show an example: We will try to do simple math and compose possible failures.

First we define our return type, this type will represent the failure or success of the functions.

type MathResult = Option<f64>;
  • Division: if the divisor is 0 we fail
  fn div(x: f64, y: f64) -> MathResult {
      if y == 0.0 {
          None
      } else {
          Some(x / y)
      }
  }
  • Square root: if we get a negative number, we fail
  fn sqrt(x: f64) -> MathResult {
      if x < 0.0 {
          None
      } else {
          Some(x.sqrt())
      }
  }
  • Logarithm: again if we get a negative number, we fail
  fn ln(x: f64) -> MathResult {
      if x < 0.0 {
          None
      } else {
          Some(x.ln())
      }
  }

Now we want to get two numbers, divide them, get the sqrt of the result and then get the logarithm of the last result

The naive way

  fn op(x: f64, y: f64) -> MathResult {
      let ratio = div(x, y);
      if ratio == None {
          return None
      };
      let ln = ln(ratio.unwrap());
      if ln == None {
          return None
      };
      return sqrt(ln.unwrap())
  }

Even though this code works it's hard to scale, and it isn't idiomatic rust, it looks more like code you'd see in Java where None is NULL.

The better way

  fn op(x: f64, y: f64) -> MathResult {
      match div(x, y) {
          None => None,
          Ok(ratio) => match ln(ratio) {
              None => None,
              Ok(ln) => sqrt(ln),
          },
      }
  }

This example is more rustic but it still looks like too much noise, and still it's very hard to scale

The FP way

  fn op(x: f64, y: f64) -> MathResult {
      div(x, y).and_then(|ratio|
      ln(ratio).and_then(|ln|
      sqrt(ln)))
  }

This way look almost like the special thing that we want to do but those and_then and closures seem unnecessary

The hado macro way

  fn op(x: f64, y: f64) -> MathResult {
      hado! {
          ratio <- div(x, y);
          ln <- ln(ratio);
          sqrt(ln)
      }
  }

Here we have a very obvious way of declaring out intent without no sign of error handling of any kind, we needed to add a trait Monad to Option (which is already defined by default in this library)

Error type agnostic

Now some more fancy stuff, you may be thinking but what about the try! macro, it would certainly make things better, right? and my answer would be yes but the try macro only works on the Result type so there is no way of changing the type of the error (or use it with our Option based functions).

But now we need to know what was the error that made of computation fail. So we change the MathResult alias to be a Result of f64 or a custom type MathError

  #[derive(Debug)]
  pub enum MathError {
      DivisionByZero,
      NegativeLogarithm,
      NegativeSquareRoot,
  }

  type MathResult = Result<f64, MathError>;

So now we need to change each function because now all the None and Some constructors are a type error

For example div turns into

  fn div(x: f64, y: f64) -> MathResult {
      if y == 0.0 {
          Err(MathError::DivisionByZero)
      } else {
          Ok(x / y)
      }
  }

note that the only changes are:

  • None Err(MathError::DivisionByZero)
  • Some Ok (x / y)

and now we check out the op function for each implementation:

  • Naive way: Change all the constructors, the failure checker, luckily rust's type inference saves us from changing too many type declarations.
  • Better way: Slightly better, same constructors, but failure checkers are replaced by match statements but still very verbose.
  • FP way: a lot better, the only worry we have is that the new error type has some sort of and_then and everything else should work.
  • hado way: Similar to the last, but now there is nothing to change, the computation has no concern over the failure framework.

How can I use it?

The macro can do some basic stuff based on a trait defined inside the crate Monad which has implementations for Option and Result by default

Let's take some examples from the rust book and rust by example and translate them into hado format.

Example from Result type reference

Here is the original try based error handling with early returns

  fn write_info(info: &str) -> io::Result<()> {
      let mut file = try!(File::create("my_best_friends.txt"));
      println!("file created");
      try!(file.write_all(format!("rating: {}\n", info.rating).as_bytes()));
      Ok(())
  }

And here is the hado based

  fn hado_write_info(string: &str) -> io::Result<()> {
      hado!{
          mut file <- File::create("my_best_friends.txt");
          println!("file created");
          file.write_all(format!("string: {}\n", string).as_bytes())
      }
  }

Note that the ign keyword is special, it means that the inner value is discarded but in the case of failure the whole expressions will short circuit into that error. Since there is no arrow (<-) the return of the println is completely discarded so you can have any non failing statements in there (including let and use)

Multi parameter constructor

let's say we have a Foo struct that has a new method

  fn new(a: i32, b: f64, s: String) -> Foo {
      Foo {
          a: i32,
          b: f64,
          s: String
      }
  }

Create a Option constructor from a normal constructor with minimal hassle

  fn opt_new(a: Option<i32>, b: Option<f64>, s: Option<String>) -> Option<Foo> {
      a <- a;
      b <- b;
      s <- s;
      ret new(a, b, s)
  }

Note that only by changing the type signature you can change Option to any other monadic type.

You can also do some custom validation without much boilerplate

  fn opt_new(a: i32, b: f64, s: String) -> Option<Foo> {
      a <- validate_a(a);
      b <- validate_b(b);
      s <- validate_s(s);
      ret new(a, b, s)
  }
You might also like...
Macro for Python-esque comprehensions in Rust

Cute Macro for Python-esque list comprehensions in Rust. The c! macro implements list and hashmap comprehensions similar to those found in Python, all

Rust crate that provides a convenient macro to quickly plot variables.
Rust crate that provides a convenient macro to quickly plot variables.

Debug Plotter This crate provides a convenient macro to quickly plot variables. Documentation For more information on how to use this crate, please ta

Yet another geter/setter derive macro.

Gusket Gusket is a getter/setter derive macro. Comparison with getset: gusket only exposes one derive macro. No need to derive(Getters, MutGetters, Se

A proc-macro to get Vecu8 from struct and vise versa

byteme A proc-macro to convert a struct into Vec and back by implemeting From trait on the struct. The conversion is Big Endian by default. We have ma

Rust macro to make recursive function run on the heap (i.e. no stack overflow).

Decurse Example #[decurse::decurse] // 👈 Slap this on your recursive function and stop worrying about stack overflow! fn factorial(x: u32) - u32 {

Rust macro that uses GPT3 codex to generate code at compiletime

gpt3_macro Rust macro that uses GPT3 codex to generate code at compiletime. Just describe what you want the function to do and (optionally) define a f

No-nonsense input!(...) macro for Rust

No-nonsense input!(...) macro for Rust

Derive macro for encoding/decoding instructions and operands as bytecode

bytecoding Derive macro for encoding and decoding instructions and operands as bytecode. Documentation License Licensed under either of Apache License

Derive macro implementing 'From' for structs

derive-from-ext A derive macro that auto implements 'std::convert::From' for structs. The default behaviour is to create an instance of the structure

Comments
  • Change documentation to markdown

    Change documentation to markdown

    I can see you're using orgmode, and that's totally awesome!

    However, it appears that crates.io doesn't support orgmode syntax in its documentation; so it looks like an absolute mess.

    I would advise you to convert it to markdown just for the sake of those unfamiliar with orgmode notation

    opened by ElectricCoffee 2
Owner
Lucas David Traverso
Lucas David Traverso
Rust experiments involving Haskell-esque do notation, state, failure and Nom parsers!

Introduction As a long time Haskell developer recently delving into Rust, something I've really missed is monads and do notation. One thing monadic do

Kerfuffle 23 Feb 28, 2022
Optimize floating-point expressions for accuracy

Herbie automatically improves the error of floating point expressions. Visit our website for tutorials, documentation, and an online demo. Herbie has

Herbie Project 611 Dec 19, 2022
A simple wrapper for the detour-rs library that makes making hooks much more concise

A simple wrapper for the detour-rs library that makes making hooks much more concise

Khangaroo 6 Jun 21, 2022
prelate-rs is an idiomatic, asynchronous Rust wrapper around the aoe4world API. Very much a WIP at this stage.

prelate-rs is an idiomatic, asynchronous Rust wrapper around the aoe4world API. Very much a WIP at this stage. Project Status We currently support the

William Findlay 4 Dec 29, 2022
Proc. macro to generate C-like `enum` tags.

Continuous Integration Documentation Crates.io #[derive(EnumTag)] This crate provides a proc. macro to derive the EnumTag trait for the given Rust enu

Robin Freyler 5 Mar 27, 2023
🐦 Friendly little instrumentation profiler for Rust 🦀

?? puffin The friendly little instrumentation profiler for Rust How to use fn my_function() { puffin::profile_function!(); ... if ... {

Embark 848 Dec 29, 2022
A little CLI written in rust to improve your dirty commits into conventional ones.

Simple commits A little CLI written in rust to improve your dirty commits into conventional ones. ?? Demo (coming soon) ✨ Features Fully conventional

romandev 5 Mar 16, 2024
A low-ish level tool for easily writing and hosting WASM based plugins.

A low-ish level tool for easily writing and hosting WASM based plugins. The goal of wasm_plugin is to make communicating across the host-plugin bounda

Alec Deason 62 Sep 20, 2022
This crate allows writing a struct in Rust and have it derive a struct of arrays layed out in memory according to the arrow format.

Arrow2-derive - derive for Arrow2 This crate allows writing a struct in Rust and have it derive a struct of arrays layed out in memory according to th

Jorge Leitao 29 Dec 27, 2022
A rust library that makes reading and writing memory of the Dolphin emulator easier.

dolphin-memory-rs A crate for reading from and writing to the emulated memory of Dolphin in rust. A lot of internals here are directly based on aldela

Madison Barry 4 Jul 19, 2022