We previously built Wercker, a container native CI/CD platform that was acquired by Oracle.
Our current open positions:
- Rust Engineer https://fiberplane.dev/careers/rust-engineer
- Frontend Engineer https://fiberplane.dev/careers/frontend-engineer
- Solution Engineer https://fiberplane.dev/careers/solution-engineer
- Developer Advocate https://fiberplane.dev/careers/developer-advocate
- Customer Experience Specialist https://fiberplane.dev/careers/customer-experience-specialis...
Want to join us? Email martina(at)fiberplane.com. You're more than welcome to get in touch if you're interested in what we're building but don't match the above open positions.
At Fiberplane (https://fiberplane.dev), we're redefining the future of collaboration for infrastructure teams, starting with collaborative notebooks for resolving incidents. Our vision is to put a programmable SRE environment at the fingertips of engineers everywhere.
We previously built Wercker, a container native CI/CD platform that was acquired by Oracle.
Our current open positions:
- Rust Engineer https://fiberplane.dev/careers/rust-engineer
- Solution Engineer https://fiberplane.dev/careers/solution-engineer
Want to join us? Email martina(at)fiberplane.com. You're more than welcome to get in touch if you're interested in what we're building but don't match the above open positions.
https://youtube.com/playlist?list=PLrw6a1wE39_tb2fErI4-WkMbs...
But I only agree that the async part of that is unfortunate. Making dynamic dispatch have a little extra friction is a feature, not a bug, so to speak. Rust's raison d'être is "zero cost abstraction" and to be a systems language that should be viable in the same spaces as C++. Heap allocating needs to be explicit, just like in C and C++.
But, I agree that async is really unergonomic once you go beyond the most trivial examples (some of which the article doesn't even cover).
Some of it is the choices made around the async/await design (The Futures, themselves, and the "async model" is fine, IMO).
But the async syntax falls REALLY flat when you want an async trait method (because of a combination-and-overlap of no-HKTs, no-GATs, no `impl Trait` syntax for trait methods) or an async destructor (which isn't a huge deal- I think you can just use future::executor::block_on() and/or use something like the defer-drop crate for expensive drops).
Then it's compounded by the fact that Rust has these "implicit" traits that are usually implemented automatically, like Send, Sync, Unpin. It's great until you write a bunch of code that compiles just fine in the module, but you go to plug it in to some other code and realize that you actually needed it to be Send and it's not. Crap- gotta go back and massage it until it's Send or Unpin or whatever.
Some of these things will improve (GATs are coming), but I think that Rust kind of did itself a disservice with stabilizing the async/await stuff, because now they'll never be able to break it and the Pin/Unpin FUD makes me nervous. I also think that Rust should have embraced HKTs/Monads even though it's a big can of worms and invites Rust devs to turn into Scala/Haskell weenies (said with love because I'm one of them).
I don't think this a reasonable in Rust (or in C/C++). I 90% of the pain of futures in Rust is most users don't want to care about memory allocation and want Rust to work like JS/Scala/C#.
When using a container containing a function, you only have to think allocating memory for the function pointer, which is almost always statically allocated. However for an async function, there's not only the function, but the future as well. As a user the language now poses a problem to you, where does the memory for the future live.
1. You could statically allocate the future (ex. type Handler = fn(Request<Body>) -> ResponseFuture, where ResponseFuture is a struct that implemented Future).
But this isn't very flexible and you'd have to hand roll your own Future type. It's not as ergonomic as async fn, but I've done it before in environments where I needed to avoid allocating memory.
2. You decide to box everything (what you posted).
If Rust were to hide everything from you, then the language could only offer you (2), but then the C++ users would complain that the futures framework isn't "zero-cost". However most people don't care about "zero-cost", and come from languages where the solution is the runtime just boxes everything for you.
I kinda feel like there's this false dichotomy here: either hide and be like Java/Go or be as explicit as possible about the costs like C/C++. Is there maybe a third option, when I as a developer aware of the allocation and dispatch costs, but the compiler will do all the boilerplate for me. Something like `async dyn fn(Request) -> Result<Response>`? :)
Sure, Go hides all that, but as a result it’s also possible to have memory leaks and spend extra time/memory on dynamic dispatch without being (fully) aware of it.
type Handler = fn(Request<Body>) -> Result<Response<Body>, Error>;
let mut map: HashMap<&str, Handler> = HashMap::new();
map.insert("/", |req| { Ok(Response::new("hello".into())) });
map.insert("/about", |req| { Ok(Response::new("about".into())) });
Sure, using function pointer `fn` instead of one of the Fn traits is a bit of a cheating, but realistically you wouldn't want a handler to be a capturing closure anyway.But of course you want to use async and hyper and tokio and your favorite async db connection pool. And the moment you add `async` to the Handler type definition - well, welcome to what author was describing in the original blog post. You'll end up with something like this
type Handler = Box<dyn Fn(Request) -> BoxFuture + Send + Sync>;
type BoxFuture = Pin<Box<dyn Future<Output = Result> + Send>>;
plus type params with trait bounds infecting every method you want pass your handler to, think get, post, put, patch, etc. pub fn add<H, F>(&mut self, path: &str, handler: H)
where
H: Fn(Request) -> F + Send + Sync + 'static,
F: Future<Output = Result> + Send + 'static,
And for what reason? I mean, look at the definitions fn(Request<Body>) -> Result<Response<Body>, Error>;
async fn(Request<Body>) -> Result<Response<Body>, Error>;
It would be reasonable to suggest that if the first one is flexible enough to be stored in a container without any fuss, then the second one should as well. As a user of the language, especially in the beginning, I do not want to know of and be penalized by all the crazy transformations that the compiler is doing behind the scene.And for the record, you can have memory leaks in Rust too. But that's besides the point.
Deleted Comment
It was not nearly as obvious 15 years ago, as it is today, but all the signs were there. We (I include myself here) were just not listening closely enough to the countries that knew Russia the best: Estonia, Latvia, Lithuania, Poland, Ukraine.
The decision is also amplified by the meaningless end to the German nuclear program.
After living in Western Europe for almost a decade it's still shocking to me how much people here do not understand Russia.