#monads #functor #category-theory #haskell #free-monad

higher-free-macro

一个基于用户提供的 Functor 创建 (原始) Free Monad 类型的宏。它使用来自 "higher" crate 的特性。此宏是 Edward Kmett 的 "free" Haskell 包中 Control.Monad.Free 部分的移植。

1 个不稳定版本

0.1.0 2023 年 4 月 2 日

#778 in 数据结构

MPL-2.0+

49KB
512

免责声明

这是一个概念验证。请勿在生产代码中使用此宏,除非您先进行仔细审查。可能存在大量错误。欢迎提交 pull request。

描述

这是 Haskell 的 Control.Monad.Free 包部分的一个简单移植到 Rust,使用了 higher crate 中的特性。此 crate 使用宏为每个用户提供的 Functor 生成唯一的 Free Monad 类型。

用法

使用非常简单。首先,您创建您的 Functor 类型,然后调用 free! 宏来根据它创建实际的 Free Monad 类型。例如

use higher_free_macro::free;
use higher::*;

#[derive(Clone,Functor)]
enum TestFunctor<A>{
    Variant(A)
}

free!(FreeTestFunctor<A>, TestFunctor<FreeTestFunctor<A>>);

为了获得实际的 MonadFunctor 需要实现 Clone。这是为了 Apply 的实现,在 Free Monad 中需要调用 bind() 对另一个 Free Monad 中的每个 Pure 节点,但 bind(),如 higher crate 中所述,消耗 self。其他特性(FunctorBindPure)不需要可克隆性,但没有 Apply 类型就不是 Applicative,没有 Applicative 类型就不是 Monad

如果Functor的fmap(f)的结果的生命周期依赖于传入它的函数f的生命周期,则宏需要知道哪个生命周期参数受到影响。例如,在下面的更复杂的例子中,宏调用如下:free!(<'a>, FreeSaturday<'a, A>, Saturday<'a,FreeSaturday<'a, A>>);,因为Saturdayfmap(f : F)具有F : Fn(A)->B + 'a

为什么要使用宏呢?

这主要是因为没有非生命周期绑定,这似乎在实现完全泛型实现时是不可能的,这是需要表达Functor<A>::Result<T>的类型不应依赖于A的约束。此外,对于泛型实现来说,永不类型也会很好。

我实际上已经使用Nightly实现了一个概念验证实现,用于构建完全泛型的Free类型,但我没有进一步跟进,因为这在稳定的工具链中还不工作。

一个更复杂的例子

上面的简单例子并没有真正展示多少。一个更复杂的例子可能是构建一个内嵌的领域特定语言。下面的代码计划在一个晚上在不同的酒吧喝酒,啤酒质量各不相同。然而,在规划时,我们并不知道每个酒吧的啤酒质量。这是我们在解释计划时才能获得的信息(即,这是实际上去那里品尝啤酒的副作用)。

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).
//Note that the lifetime of the continuation function for DrinkABeer depends on the lifetime of f : Fn(Next) -> B + 'a.
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
//The result of fmap(f) depends on the lifetime of f, 'a. That's why we pass this to the macro as first parameter.
free!(<'a>, 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);
}

一个更复杂的例子

源代码库中的"examples/text-adventure"文件夹包含一个(非常)简短的文本冒险游戏,用于说明Free Monads作为内嵌领域特定语言的用法。它还展示了在创建基于Free Monad的eDSL时应注意的一些潜在障碍。

关于此代码的来源的说明

此项目的工作始于stillalive studios。最初的目标是了解Free Monads在游戏开发中的应用潜力,但这个项目已经超出了最初计划,并成为Rust中Free Monads的完整概念验证实现,主要由我(Andreas Grois)在业余时间开发。

许可证和版权

版权(c)2023 Andreas Grois和stillalive studios GmbH。保留所有权利。

本软件受Mozilla公共许可证v. 2.0的条款约束。如果没有随此文件分发MPL副本,您可以在http://mozilla.org/MPL/2.0/获得一份。

依赖关系

~1.5MB
~37K SLoC