And later I found out that I needed to do some async operations in there. First of all, it took me a bit to learn how to do it, but also somehow it ended up being way less readable construction.
I believe this is something that's being looked into, it just hasn't been implemented yet (remember, Rust is still pretty young! async/await have only been around for a year or two)
It's more complicated because the syntax async fn (String) -> String implies that we are talking about a single type for async function pointers, but such a single type can't exist. Every future is actually an entirely separate type (they just implement the same trait), and therefore no two async fns have the same return type.
The generics let you accept any function pointer that returns any type that implements the Future trait.
Generics will duplicate the function for any combination of types its used with. So e.g. your baz will be compiled once for every call with a different return type.
Of course such type can exist (not with today's compiler, of course, but in principle)! You can handle that is the exact same way str is handled: sure every feature is different, like every string literal type is kinda-sorta different, but that difference only matters to the code which touches that future, that is, the actual implementation of said async fn, everything else may just treat said future as an opaque chunk of memory and then a simple fat pointer would work for async fn.
I really wonder why async in Rust wasn't implemented that way.
Is that really that important from performance POV?
I just wonder if anyone have tried to actually implement async in a way where async fnis a type (like &str is a type).
I understand that it's a lot of work, but I'm just curious if that was contemplated or not. Because there are really no need to make async function pointers special and force that generalization.
Was it just easier to implement that way or are there hidden reasons not to do that?
Well, okay, you're right, such a function pointer can certainly exist, and it does. It's just that the return value is (approximately) a Box<dyn Future> (i.e. a fat pointer). The reason Rust doesn't use this type by default for async fns is that it requires a Box, which involves a heap allocation. One of the major factors in the design of async/await is that it should not require heap allocations.
Yeah, that's the band-aid currently used to turn templates into generics. In all places in the languages and in async, too.
It requires Box precisely because it's just a trait and we have no idea about it's actual implementation.
But all these types are, in reality, generated by a compiler. Compiler can easily include size of the Future in the AsyncFn pointer.
Sure, to actually use that pointer you would need to allocate memory somewhere (on heap or may be in some kind of arena, or in some data structure, maybe) — but that's true anyway: actual code in async fn requires some actual space allocated before you can execute it.
Yes, I understand that. If you call async functions directly and don't pass pointers to such functions around you may avoid heap allocation. But if you do want to pass them around… and call and use arbitrary async functions… at this point memory allocation is basically, a strong requirement and it would be nice not to pay price for the monomorphization when code is structured like that.
If the answer is “we wanted to first make code which can be implemented without heap allocations possible and then, maybe, think about other case” then I'm happy to accept that: async implementation ideas are not as established as “normal” functions implementations (which were, basically, finished half-century ago), and they are easy for managed languages, but hard for unmanaged ones. It's still unclear what's the best way to implement async in non-managed language.
I mean, what you've described here is more or less exactly what fn() -> Box<dyn Future> already is. If not monomorphizing your code is what you're looking for, then go for that.
However you'll need to call it with a smaller helper wrapper that puts it in a box:
baz(|s| Box::pin(foo(s)));
Note that I've used the BoxFuture alias in the futures crate to deal with some details regarding pinning and send in the actual type that aren't worth worrying about.