声明:本文全属口嗨。
群友又在讨论在Rust中搞Monad了。于是写下了自己一些想法。
洋葱结构是Rust中十分常见的类型嵌套结构。洋葱结构即effect序列:类型T1<T2<T3<...Tn<A>...>>
表示带有effect序列T1
, T2
, T3
, ...,Tn
的A
类型的计算。
如果有Tn: Foo
满足结合律[1]且有单位effectimpl<A> Once<A> for Foo { type Output = A; ... }
,则Foo
是个Monad。
但由于lifetime只能由“洋葱”的外侧传到内侧,外层的(左边的)的effect依赖内层的(右边的)effect。
type Effects<'a, A:'a> = T1< T2< T3<... Tn< A >...>>;
// |'a ^ |'a ^ |'a ^ ... |'a ^
// | | | | | | | |
// +---+ +---+ +---+ +---+
所以在T1<T2<T3<...Tn<A>...>>
非'static
的情况下,只有右结合而没有左结合(外层的effect不能独立存在)。没有结合律就谈不上Monad了。
左结合出问题的情况,最常见的情况是在使用组合子式API时出现的lifetime问题。
x
struct A;
future::ready(()).then(|_| {
let a = A;
let p = &a;
foo().map(move |res| {
println!("{:p}", p);
res
}) // 实际上 Then的闭包返回了局部变量的引用
});
// 其effect结构大概是
type Effect = Then<Map<foo<...>>>
// ^'a |
// | | Map捕获了Then中的p,逻辑上造成了“自引用”。
// +---+
其本质就是左结合导致的自引用的问题(破坏了lifetime的传递规则,使内层依赖外层),这时无法通过组合子式的API来解决。这时候就需要手动将两个effect合并到一个结构里,并Pin
起来。async-await便是编译器帮你自动完成该工作的一个语法糖。
xxxxxxxxxx
struct A;
async {
let a = A;
let p = &a;
let res = foo().await;
println!("{:p}", p);
res
}
self
不带Pin
的根本连手动左结合都做不到,指Iterator
。
其实不应该说“不满足结合律”,而是有些需要左结合的 类型 其 项 根本不合法。这就大大限制了Monad在Rust的应用面(又没有通用的do,自动自引用)。不过Rust自带控制流和side-effect不香吗,future还有async-await的语法糖,干嘛非得搞组合子呢。噢对了,还有一个解决方向,就是提倡无lifetime编程(这是可以做到的)。
于是,由于lifetime的存在,Monad in Rust确实不是好文明。(但Functor是)
[1]: 结合律不知道在洋葱层面如何表达。但impl<In: Foo> Tn<In> for Foo { type Output = In::Output; ... }
应该天然表示右结合。或许trait FooFamily { type Foos<A>: Foo<Output = A> ...}
可以表达左结合?
不过用flat_map之类的可以表示结合律:
xxxxxxxxxx
eff.flat_map(f).flat_map(g).run() == eff.flat_map(|x| f(x).flat_map(g)).run()
// 右结合 左结合