Hacker News new | ask | show | jobs
by ironmagma 1925 days ago
In a systems context, where performance and memory ostensibly matter, why wouldn’t you want to be made aware of those inefficiencies?

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.

3 comments

I think Rust is also able to hide certain things. Without async things are fine:

    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.

>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

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.

Thanks for the suggestion. I didn't think of (1), although it's a pity that it's not as ergonomic as async fn.

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>`? :)

In this example rust doesn't just make me aware of the tradeoffs. It almost feels like the language is actively standing in the way of making the trade offs I want to make. At least as the language is today. I think a bunch of upcoming features like unsized rvalues and async fns in traits will help.
> In a systems context, where performance and memory ostensibly matter, why wouldn’t you want to be made aware of those inefficiencies?

Perhaps, but a bigger problem is that lots of folks are using Rust in a non-systems context (see HN frontpage on any random day).