5 ms·
This is good info, and will be very useful for people porting code from other languages like Javascript. But I'm still in mourning that async took over the worl
by zackmorris 5y ago
This is good info, and will be very useful for people porting code from other languages like Javascript. But I'm still in mourning that async took over the world.
I grew up with cooperative multitasking on Mac OS and used Apple's OpenTransport heavily in the mid-90s before Mac OS X provided sockets. Then I spent several years working on various nonblocking networking approaches like coroutines for games before the web figured out async. I went about as far down the nonblocking IO rabbit hole as anyone would dare.
But there's no there there. After I learned Unix sockets (everything is a stream, even files) it took me to a different level of abstraction where now I literally don't even think about async. I put it in the same mental bin as mutexes, locking IO, busy waiting, polling, even mutability. That's because no matter how it's structured, async code can never get away from the fact that it's a monad. The thing it's returning changes value at some point in the future, which can quickly lead to nondeterministic behavior without constant vigilance. Now maybe my terminology here is not quite right, but this concept is critical to grasp, or else determinism will be difficult to achieve.
I think a far better programming pattern is the Actor model, which is basically the Unix model and piping immutable data around. This is more similar to how Go and Erlang work, although I'm disappointed in pretty much all languages for not enforcing process separation strongly enough.
Until someone really understands everything I just said, I would be very wary of using async and would only use it for porting purposes, never for new development. I feel rather strongly that async is something that we'll be dealing with and cleaning up after for the next couple of decades, at least.
- mamcx 5y agoRust is hard at the start, but easy after. But I feel async keep it hard. The sad part is that async is SO infectious that you are forced to move all on it to align with the rest of the ecosystem. I also believe the way all of this is presented is not the right abstraction. Actors + CSP is probably the best way. Plus, even if concurrency <> parallelism I think the parallelism idioms make more sense (pin to the "thread", do fork-joins, use ring-buffer for channels, etc). However, I suppose the whole issue is that async as-is is easier for the mechanical support that work for the compilers and allows to squeze the performance/resource usage, that is important for Rust. But maybe keep it hidden and surface another kind of api?
- Zababa 5y ago> The sad part is that async is SO infectious that you are forced to move all on it to align with the rest of the ecosystem. That's the problem with monadic stuff in general. One solution to that might be to keep the async part on the "edge" of your programs (a bit like the functional core, imperative shell pattern or the hexagonal architecture), write all your logic without async and use async only on the edge.
- zackmorris 5y agoThanks for that, I wasn't familiar with hexagonal architecture. I think there's a fundamental concept here about dealing with IO in a pure functional programming (FP). For me, stuff like monads make reasoning about IO in FP languages like Haskell really difficult. But I haven't really encountered that difficulty with ClojureScript. It pauses and resumes endlessly alongside Javascript, and uses that stop-the-world mechanism to provide and accept data for IO without using monads. So we can write all of the pure functional ClojureScript we want, blissfully unaware that monads even exist. Whereas, other FP languages seem to think of IO as this thing that happens while your program is running, and get lost in the weeds. Where this is important is for static analysis. Without mutability, we can take the whole syntax tree and turn it into intermediate code (I-code) and transform that tree in all kinds of fun ways with concepts from Lisp. But once we have a mutable variable, that entry/exit point of the logic has to be carried along like an imaginary number, which creates forks in the road that are more difficult to analyze because every fork doubles the analysis required, which eventually leads to an explosion of complexity that limits how far we can optimize or even understand imperative programming (IP) languages. Now imagine an IP language like C, with its myriad of mutable variables on almost every line. If we transpiled that to an FP language, we'd see countless entry/exit points around pure functional code, with intractable complexity around the mutable state stored in the variables. To the point that it can't really be statically analyzed. Then we get excited about fractional improvements in performance, without realizing that we missed out on orders of magnitude higher gains with parallelization and other transformations that could have happened. To me, once programmers see this, they can't really unsee it. Our whole world is built on imperative code that we just don't understand. And I am starting to feel that this mutable/monadic/async behavior (whatever we want to call it) is an anti-pattern. We should be trying to get to programming that works more like a spreadsheet, where we can play with the inputs and see the results of the logic in real time without side effects.
- rdtsc 5y ago> But I'm still in mourning that async took over the world. I agree, it does seem like a step backwards in general. However, for Rust it makes sense. There is no runtime, so there is nothing to preempt the green threads/lightweight processes etc. But yeah, with higher level languages like Python, I was disappointed to see how async was emphasized in 3.x over green threads which were already used by a number of projects.