
声明:本文全属口嗨。
群友又在讨论在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便是编译器帮你自动完成该工作的一个语法糖。
xxxxxxxxxxstruct 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之类的可以表示结合律:
xxxxxxxxxxeff.flat_map(f).flat_map(g).run() == eff.flat_map(|x| f(x).flat_map(g)).run()// 右结合 左结合