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.