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