9 ms·
It's not really all that crazy. Sure, if you consider languages like C#, it's much more verbose than those, but that's because Rust tries to deal with async wit
by GlitchMr 7y ago
It's not really all that crazy. Sure, if you consider languages like C#, it's much more verbose than those, but that's because Rust tries to deal with async without garbage collection. This isn't a typical code you would write in Rust, in fact, in this case, this type was generated by a library.
Dealing with this "ridiculous" type step by step:
Future<Output = User>
This is a future. It returns an user.
dyn
However, because the exact `Future` implementation can vary, dynamic dispatch is necessary. Use of `dyn` keyword explicitly says that a vtable is necessary.
+ Send
Rust is supposed to make programs free from data races. It's very much possible to have objects which aren't allowed to change the thread they are in. For instance, `Rc<T>` (single-thread reference-counting pointer) cannot be sent to other threads due to it using non-atomic reference count.
In this case, using `+ Send` says that whatever future this method returns must be possible to send to other threads. This prevents use of objects like `Rc` within the futures, and allows the future to be used in multi-threaded executors.
+ '_
The method borrows `self` (as seen by the use of `&self`). Saying `+ '_` means that the future should borrow `self`. This is necessary due to lifetimes.
Box<...>
Because dynamic dispatch types (note `dyn` before) can have varying sizes (one future can be smaller, another one can be bigger), it's necessary to wrap them somehow so that they could be represented in memory. In this case, the simplest heap-allocation type (`unique_ptr` in C++) is being used. There are other options, like `Arc<...>`, but in this case, `Box` is sufficient.
Pin<...>
Futures can store pointers to its local fields. This is going to cause issues if the allocation could be moved somewhere else in the RAM - the future would be moved to the new location, but the pointers would still refer to RAM in the old position. Using `Pin` prevents the user from moving the contents of `Box` to another place in the RAM.
- pron 7y agoThis is a nice demonstration of the tradeoff inherent in the (confusingly named) "zero-cost abstractions" philosophy: it brings accidental complexity front and center. This accidental complexity is often viral, propagating through the call stack in the type signature, and made so pervasive in the program that it becomes its focus (even if the types are hidden and inferred) and the element that is hardest to change rather than easiest. C++ is most famous for this philosophy, but Rust doubles down on it because of its commitment to memory-safety soundness through the type system. Look at how many details having to do not with any algorithm but with the code the compiler emits are stated here: that the call is made in an asynchronous context, that an object requires a vtable, that an object is able to change threads, the object's lifetime, boxing, and that the object cannot be moved in RAM. The only part that's related to the algorithm here is `User`. Of course, in constrained environments like embedded systems or other circumstances where control over resources is crucial -- the domains Rust is designed to target -- accidental complexity becomes essential as control over resources is an important component of the problem. Some more here, and an alternative (with it's own tradeoffs) that could be called "zero-cost use" (although that's equally as bad and confusing as "zero-cost abstractions"): https://news.ycombinator.com/item?id=19932753 https://news.ycombinator.com/item?id=19932753
- GlitchMr 7y agoAgreeing here. However, worth noting a lot of this accidental complexity is visible due to dynamic dispatch. If `get_user` wouldn't have to be dynamically dispatched, it would be simply written as: async fn get_user(&self) -> User { ... } Dynamic dispatch complicates a lot of things in Rust, as then the compiler cannot simply check the returned type to determine the necessary guarantees - as the type can be anything.
- likeliv 7y agoRight. Is it really accidental complexity? This is the implementation detail that the users of the language doesn't really need to know as they would just write 'async fn' (and #[async_trait] while waiting for the issues mentioned in this blog post get solved).
- pron 7y agoOh, whether or not it's accidental depends on the domain, and Rust is designed for domains where such detailed control over resources could well be said to be essential, but it is nonetheless complexity, even if it is entirely inferred, as the compiler would block uses that don't comply with a very specific contract that is all about specific code generation. Even here the need to say that the call is "async" is not required from the algorithmic point of view, and in languages with a different philosophy and access to a JIT -- like, say, Java -- this is an implementation detail that could be worked out by the compiler entirely on a use-site basis (i.e. the same function could be "async" or not depending on how it's used in a particular callsite, not how it's defined). But there's definitely a very big tradeoff -- in both approaches -- between fine-grained control over resources and "non-algorithmic complexity" if you'd like to call it that.
- zzzcpan 7y ago"async" is required from the algorithmic point of view, so the algorithm can be expressed in terms of specific concurrency model. And in Rust, it's not just "async" that's required, but also a specific executor, which can affect concurrency model as well. Concurrency model can't be an implementation detail.