Monads are the wrong abstraction IMHO. Functors are all you need to have "monadic bind" syntax, what monads give you is two more natural transformations (per monad) that let you collapse any number of levels of nesting of your monad down to one level. eg. The pure
and flatten
operations on Option
allow you to convert:
Option<Option<...<Option<T>>...>> --> Option<T>
^~~~~~~~ n times ~~~~~~~^
Which is what allows you to write try
blocks (for Option
) with n
uses of the ?
operator for any n
.
These aren't the only important natural transformations though. For instance, using ?
on a Result
will also convert the error type using Into
. That is, it uses a generic natural transformation:
Result<Result<T, E0>, E1> --> Result<T, E>
where
E0: Into<E>,
E1: Into<E>,
This is more general than the flatten
operation that you get from Result<_, E>
being a monad.
Another example: If you want to be able to use ?
inside async
blocks then you also need to be able to transform impl Try<Ok = impl Future<Output = T>>
into impl Future<Output = impl Try<Ok = T>>
, but you don't get this from monads.
So I think if you want to support "monadic" syntax the way to do it is to ditch monads completely and work directly at the level of functors and natural transformations.
How I imagine this working is: for any functor (any trait with a map
method) you can give it its own block/bind psuedo-keywords (eg. async
/await
in the case of Future
). You also have to provide an "into" trait which is implemented on types that can be converted into your functor. eg. For Future
this would be IntoFuture<T>
which is implemented for types that can be converted into an impl Future<Output = T>
. The implementations of this trait provide the natural transformations, so you need to at least have T: IntoFuture<T>
and F: IntoFuture<T> for F: Future<Output = G>, G: Future<Output = T>
if you want the monadic pure
and flatten
operations. But you can also provide other implementations such as the one that swaps the order of Try
and Future
above.
When you use your new block syntax all the binds get translated into invocations of map
and the entire body of the block is passed to a call to your "into" trait. For example, if we had the following block:
async {
let b = a.await;
let c = b?;
let x: T = foo(c);
x
}
This gets translated into (something equivalent to):
IntoFuture::<T>::into({
Future::map(a, |b| {
Try::map(b, |c| {
let x: T = foo(c);
x
})
})
})
Note that it can't literally be translated like this due to borrowing, but if you wanted to implement this with macros you can probably expand it to something equivalent using generators and unsafe
hackery.