Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

I'm not totally sure what the author is asking for, apart from refcounting and heap allocations that happen behind your back. In my experience async Rust is heavily characterised by tasks (Futures) which own their data. They have to - when you spawn it, you're offloading ownership of its state to an executor that will keep it alive for some period of time that is out of the spawning code's control. That means all data/state brought in at spawn time must be enclosed by move closures and/or shared references backed by an owned type (Arc) rather than & or &mut.

If you want to, nothing is stopping you emulating a higher-level language - wrap all your data in Arc<_> or Arc<Mutex<_>> and store all your functions as trait objects like Box<dyn Fn(...)>. You pay for extra heap allocations and indirection, but avoid specifying generics that spread up your type hierarchy, and no longer need to play by the borrow checker's rules.

What Rust gives us is the option to _not_ pay all the costs I mentioned in the last paragraph, which is pretty cool if you're prepared to code for it.



I'm a giant Rust fanboy and have been since about 2016. So, for context, this was literally before Futures existed in Rust.

But, I only work on Rust code sporadically, so I definitely feel the pros and cons of it when I switch back and forth to/from Rust and other languages.

The problem, IMO, isn't about allocations or ownership.

In fact, I think that a lot of the complaints about async Rust aren't even about async Rust or Futures.

The article brings up the legitimate awkwardness of passing functions/closures around in Rust. But it's perfectly fair to say that idiomatic Rust is not a functional language, and passing functions around is just not the first tool to grab from your toolbelt.

I think the actual complaint is not about "async", but actually about traits. Traits are paradoxically one of Rust's best features and also a super leaky and incomplete abstraction.

Let's say you know a bit of Rust and you're kind of working through some problem. You write a couple of async functions with the fancy `async fn foo(&x: Bar) -> Foo` syntax. Now you want to abstract the implementation by wrapping those functions in a trait. So you try just copy+pasting the signature into the trait. The compiler complains that async trait methods aren't allowed. So now you try to desugar the signature into `fn foo(&x: Bar) -> impl Future<Foo>` (did you forget Send or Unpin? How do you know if you need or want those bounds?). That doesn't work either because now you find out that `impl Trait` syntax isn't supported in traits. So now you might try an associated type, which is what you usually do for a trait with "generic" return values. That works okay, except that now your implementation has to wrap its return value in Box::pin, which is extra overhead that wasn't there when you just had the standalone functions with no abstraction. You could theoretically let the compiler bitch at you until it prints the true return value and copy+paste that into the trait implementation's associated type, but realistically, that's probably a mistake because you'd have to redo that every time you tweak the function for any reason.

IMO, most of the pain isn't really caused by async/await. It's actually caused by traits.


also a long-time rust user, and I buy this. one of the things it took me longest to realize when writing rust is to reach for traits carefully/reluctantly. they can be amazing (e.g. serde), but I've wasted tons of time trying to make some elegant trait system work when I could have solved the problem much more quickly otherwise.


Exactly. Which is unfortunate, because the fact that Rust has true type classes is absolutely awesome.

But when dealing with traits, you have to remember the orphan rules, and the implicit object-safety rules- which sucks, because you might not have planned on using trait objects when you first defined the trait, but only tried to do so later.

Async definitely makes it even more painful.


> The article brings up the legitimate awkwardness of passing functions/closures around in Rust.

That's a hard problem when you have linear/affine types! Closures don't work so neatly as in Haskell; currying has to be different.


I wonder if most of the pain is actually caused by Rust async being an MVP, so things like async trait functions (which would be very nice) don't exist... yet.

I don't know if anybody has shown that they can't ever exist, it's just that they weren't considered necessary to get the initial async features out of the door. Rather like how you can't use impl Trait in trait method signatures either (there's definitely some generics-implications-complexity going on with that one).


This macro goes a very long way toward solving the problem: https://github.com/dtolnay/async-trait


^this

IMO, Rust already provides a decent amount of way to simplify and skip things. Reference counting, async, proc_macro, etc.

In my experience, programming stuffs at a higher-level-language where things are heavily abstracted, like (cough) NodeJS, is easy and simple up to a certain point where I have to do a certain low-level things fast (e.g. file/byte patching) or do a system call which is not provided by the runtime API.

Often times I have to resort to making a native module or helper exe just to mitigate this. That feels like reinventing the wheel because the actual wheel I need is deep under an impenetrable layer of abstraction.


This is where Scala (and JVM based languages) would shine in theory, the JVM has a well defined memory model, provides great low-level tools, etc. (But JVM-based software is always very bulky to deploy, both in terms of memory and size, so this shining is rarely seen in practice.)


Memory usage has been my main issue with JVM, running an instance of it is very costly compared to languages that compiles to near abstract machines (or at least have minimal runtime code like Go).

Anyway on abstraction, it's just hard, because everyone has different concept of what abstraction is. For some it's just combining couple of function calls into one, for some it is providing defaults, for some it is providing composable functions with controllable continuations.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: