1 个不稳定版本
| 0.1.0 | 2023 年 4 月 2 日 |
|---|
#778 in 数据结构
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>>);
为了获得实际的 Monad,Functor 需要实现 Clone。这是为了 Apply 的实现,在 Free Monad 中需要调用 bind() 对另一个 Free Monad 中的每个 Pure 节点,但 bind(),如 higher crate 中所述,消耗 self。其他特性(Functor、Bind、Pure)不需要可克隆性,但没有 Apply 类型就不是 Applicative,没有 Applicative 类型就不是 Monad。
如果Functor的fmap(f)的结果的生命周期依赖于传入它的函数f的生命周期,则宏需要知道哪个生命周期参数受到影响。例如,在下面的更复杂的例子中,宏调用如下:free!(<'a>, FreeSaturday<'a, A>, Saturday<'a,FreeSaturday<'a, A>>);,因为Saturday的fmap(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