aboutsummaryrefslogtreecommitdiff
path: root/README.md
diff options
context:
space:
mode:
Diffstat (limited to 'README.md')
-rw-r--r--README.md145
1 files changed, 145 insertions, 0 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b23080e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,145 @@
+# Disclaimer
+This is a proof-of-concept. Please do not use this for production code, unless you do a careful review first.
+There probably are tons of bugs. Pull requests are more than welcome.
+
+# Description
+This is a simple (and possibly wrong) port of Haskell's [Control.Monad.Free](https://hackage.haskell.org/package/free/docs/Control-Monad-Free.html) package to Rust, using the traits from [higher](https://crates.io/crates/higher).
+This crate uses macros to generate a unique Free Monad type for each Functor.
+
+# Usage
+The usage is rather straightforward. First, you create your Functor type, then call the `free!` macro to create the actual Free Monad type based on it. For example:
+
+```Rust
+use higher_free_macro::free;
+use higher::*;
+
+#[derive(Clone,Functor)]
+enum TestFunctor<A>{
+ Variant(A)
+}
+
+free!(FreeTestFunctor<A>, TestFunctor<FreeTestFunctor<A>>);
+```
+
+In order to get an actual `Monad`, the `Functor` needs to implement `Clone`. It is needed for the `Apply` implementation, which in a Free Monad needs to call `bind()` on `self` for each `Pure` node in the other Free Monad, but `bind()`, as written in the `higher` crate, consumes `self`. The other traits (`Functor`, `Bind`, `Pure`) do not need cloneability, but without `Apply` the type is not `Applicative`, and without `Applicative` it is not `Monad`.
+
+# Why macros though?
+ This is mainly because a fully generic implementation seems impossible without [Non-lifetime Binders](https://github.com/rust-lang/rust/issues/108185), which would be needed to express the bound that the type of `Functor<A>::Result<T>` should not depend on `A`. Also, the [Never Type](https://github.com/rust-lang/rust/issues/35121) would be nice to have for a generic implementation.
+
+ I've actually done a [proof-of-concept implementation](https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=cea687f9989dd313a993facfd96adac0) of the `Functor` trait for a fully generic `Free` type using Nightly, but didn't follow this further yet, because it doesn't work in the stable toolchain yet.
+
+ # A more involved example
+
+The trivial example above doesn't really show much. A more involved example would for instance be building an embedded Domain Specific Language.
+The code below plans a night of drinking at different bars, with varying beer quality. However, while planning we don't actually know the quality of the beer at each bar. That's a piece of information we only get while interpreting the plan (as in: that's a _side effect_ of actually going there, and tasting the beer):
+
+```Rust
+use std::rc::Rc;
+use higher_free_macro::free;
+use higher::*;
+
+#[derive(Debug, Clone, PartialEq)]
+enum BeerQuality{
+ Lukewarm,
+ Refreshing
+}
+
+#[derive(Clone)]
+enum Saturday<'a, Next>{
+ GoToBar{
+ name_of_bar : &'a str,
+ next : Next
+ },
+ DrinkABeer (Rc<dyn Fn(BeerQuality)->Next + 'a>) //Rc, because it's cloneable, dyn to keep it out of the type signature.
+}
+
+//Saturday is too complex to derive Functor for it (at the moment at least).
+impl<'a, Next : 'a> Functor<'a, Next> for Saturday<'a, Next>{
+ type Target<T> = Saturday<'a, T>;
+
+ fn fmap<B, F>(self, f: F) -> Self::Target<B>
+ where
+ F: Fn(Next) -> B + 'a {
+ match self {
+ Saturday::GoToBar { name_of_bar, next } => Saturday::GoToBar { name_of_bar, next: f(next) },
+ Saturday::DrinkABeer(continuation) => {
+ Saturday::DrinkABeer(Rc::new(move |x| f(continuation(x))))
+ },
+ }
+ }
+}
+
+//Here we create the Free Monad FreeSaturday over the Functor Saturday
+free!(FreeSaturday<'a, A>, Saturday<'a,FreeSaturday<'a, A>>);
+
+//Helpers, so we don't have to write FreeSaturday::lift_f() all the time
+fn go_to_bar(s : &str) -> FreeSaturday<'_, ()>{ FreeSaturday::lift_f(Saturday::GoToBar { name_of_bar: s, next: () }) }
+fn drink_a_beer<'a>() -> FreeSaturday<'a, BeerQuality>{ FreeSaturday::lift_f(Saturday::DrinkABeer(Rc::new(std::convert::identity))) }
+
+//The plan for a nice evening. If someone serves lukewarm beer, we go home. Assumes that beer quality is constant at each bar.
+fn a_nice_evening() -> FreeSaturday<'static,()>{
+ run! { //yes, higher has do-notation :-D
+ drink_a_beer(); //at home. Don't care if lukewarm.
+ go_to_bar("Sunken Norwegian");
+ x <= drink_a_beer();
+ if x != BeerQuality::Lukewarm { run! {
+ drink_a_beer(); //alredy know if the beer here was lukewarm or not.
+ go_to_bar("Le Rieur Sanglier");
+ x <= drink_a_beer();
+ if x != BeerQuality::Lukewarm { run! {
+ drink_a_beer();
+ go_to_bar("Coyote Ugly");
+ x <= drink_a_beer();
+ if x != BeerQuality::Lukewarm { run! {
+ drink_a_beer();
+ yield ()
+ }} else{ run! { yield () } }
+ }} else{ run! { yield () } }
+ } } else{ run! { yield () } }
+ }
+}
+//This wouldn't compile if a_nice_evening weren't a Monad. Sooo, it obviously is.
+fn _test_if_a_nice_evening_is_monad() -> impl Monad<'static, ()>{
+ a_nice_evening()
+
+}
+
+//The interpreter that counts how many beers were consumed at which bar is a bit more convoluted than it would need to be, because:
+//it can't match directly for box contents here, because https://github.com/rust-lang/rust/issues/87121 isn't implemented yet :-(
+//and also cannot match for Saturday::GoToBar using pattern guards, because if-let pattern-guards aren't stable either: https://github.com/rust-lang/rust/issues/51114
+fn count_beers_consumed_per_bar(evening : FreeSaturday<()>) -> Vec<(&str, u32)>{
+ //for this illustration let's just assume that get_beer_quality_of_location() is causing side effects.
+ fn get_beer_quality_of_location(l : &str) -> BeerQuality { if l == "Le Rieur Sanglier" {BeerQuality::Lukewarm} else {BeerQuality::Refreshing}}
+ fn interpret_evening_step<'a, 'b : 'a>(l : &'b str, mut v : Vec<(&'a str, u32)>, saturday : FreeSaturday<'b,()>) -> Vec<(&'a str, u32)>{
+ match (l,&*v,saturday){
+ (_,_,FreeSaturday::Pure(_)) => v,
+ (l, [.., (lo,co)], FreeSaturday::Free(f)) if *lo == l=> {
+ match *f {
+ Saturday::GoToBar { name_of_bar, next } => interpret_evening_step(name_of_bar, v, next),
+ Saturday::DrinkABeer(next) => {
+ v.last_mut().unwrap().1 = co+1;
+ interpret_evening_step(l,v,next(get_beer_quality_of_location(l)))
+ }
+ }
+ }
+ (l, _, FreeSaturday::Free(f)) => {
+ match *f {
+ Saturday::GoToBar { name_of_bar, next } => interpret_evening_step(name_of_bar, v, next),
+ Saturday::DrinkABeer(next) => {
+ v.push((l,1));
+ interpret_evening_step(l,v,next(get_beer_quality_of_location(l)))
+ }
+ }
+ }
+ }
+ }
+ interpret_evening_step("Home", Vec::new(), evening)
+}
+
+//Just to show this works. Prints [("Home", 1), ("Sunken Norwegian", 2), ("Le Rieur Sanglier", 1)].
+fn main() {
+ let the_free_monad = a_nice_evening();
+ let beers_per_bar = count_beers_consumed_per_bar(the_free_monad);
+ println!("{:?}", beers_per_bar);
+}
+``` \ No newline at end of file