5 ms·
This is a nice demonstration of the tradeoff inherent in the (confusingly named) "zero-cost abstractions" philosophy: it brings accidental complexity front and
by pron 7y ago
This 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.
- pron 7y agoOf course it is -- when you have continuations, which each and every imperative language has, although usually without explicit control over them. With explicit access to continuations, everything expressible with async is expressible without it. Or, more precisely, whether something is "async" or not is not a feature of a certain piece of code but of a certain execution. Essentially, it means whether some computation may want to suspend itself and resume execution later. That you must express that capability as a type-system enforced property of some syntactic element -- a subroutine -- is accidental complexity. To get an intuitive feel for why that is so, try to imagine that the OS were able to give you threads with virtually no overhead, and you'll see how anything that's expressible with async would be expressible without it. Over the years we've so internalized the fact that threads are expensive that we forgot it's an implementation detail that could actually be fixed. Computations in Rust can suspend themselves without declaring themselves to be async: that's exactly what they do when they perform blocking IO. They only need to declare themselves async when they want a particular treatment from the Rust compiler and not use the continuation service offered by the runtime, which in Rust's case is always the OS.
- mwcampbell 7y ago> Of course, in constrained environments like embedded systems or other circumstances where control over resources is crucial I wonder if we should think of end-user machines as such a constrained environment. Certainly, complaining about software bloat and inefficiency is a popular pastime here on HN and on related message boards. Maybe we have an ethical obligation to our users to make the most efficient possible use of their resources, regardless of the extra complexity we have to deal with. Of course, economic realities prevent us from really doing that in many cases.
- pron 7y agoAnd maybe, if software does good, we have an ethical responsibility to deliver software more cheaply and quickly, (and if it doesn't then maybe we shouldn't write it at all)? Ethics are complex, even more than programming, and you're not going to get a definitive answer to such questions. Rust makes certain choices -- more control, more accidental complexity -- as it targets certain domains, while other languages target other domains and make other choices. It's your responsibility to figure out which domain your application belongs in, and neither ethics nor anything else can give a universally applicable answer here.