5 ms·
Ah my pet peeve reaching the front page again… The “coloring problem” has nothing to do with async at all, it's just about making effects explicit or implicit
by littlestymaar 2y ago
Ah my pet peeve reaching the front page again…
The “coloring problem” has nothing to do with async at all, it's just about making effects explicit or implicit in the code.
In Go or Rust, returning an error is also a “color” that spreads out to the top of the call stack. Same for checked exception in Java. Unchecked exceptions are like blocking functions, it's invisible so you don't have to manually annotate your functions but as a result any function can now fail without the programmer knowing it.
JavaScript having async/await but unchecked exception may sound bit paradoxical in that regard, but every language has a combinations of both explicit and implicit effects: Rust has both async/await and explicit errors, but it also has panics (implicit errors) and implicit allocation for instance.
I personally live explicit effects, but at the same time when you have too many of them (for example I'd like to have effects like “pure”, “allocating” and “panics”) they start to cause combinatorial explosion of cases in libraries accepting closures as parameter.
Algebraic effects supposedly solve this particular problem but I'm not sure the additional conceptual learning effort is sustainable for a mainstream language, so in the meantime I think every language must keep its amount of explicit effects under a certain threshold.
- randomdata 2y ago> In Go or Rust, returning an error is also a “color” that spreads out to the top of the call stack. Not unless you're doing it wrong. Errors need to be handled. That may, in some cases, mean handling it with another error, but that isn't adding any "color". The new error is independent of any previous error state.
- littlestymaar 2y agoYou're misunderstanding: the fact that the new error is different from the original one doesn't change the fact that the function returns an error. Exactly like how async functions awaiting on a promise to create new promises doesn't change the fact that the function is an async one. Sure sometimes you can avoid propagating the error upward, but that's not the most common case and it also happens with async/await (if you don't need the result of the promise in the caller function you don't need to await and to make it a async function). In Rust the similarity is very flagrant because there are one postfix operators for both (? and .await).
- assbuttbuttass 2y ago> if you don't need the result of the promise in the caller function you don't need to await and to make it a async function Why call it at all, then? In Rust, futures don't run unless you poll them, so just calling an async function without polling it doesn't do anything. In contrast, for error returns it's very common to log an error but continue with some default value, or just skip processing one item and continue with the rest. There are a lot of cases where errors can be handled immediately, rather than always blindly bubbled up the call stack
- littlestymaar 2y ago> Why call it at all, then? In Rust, futures don't run unless you poll them, so just calling an async function without polling it doesn't do anything. Rust is a bit special as the futures are lazy and you need to call your future with `task::spawn` if you want this to work, but it still does. And in other languages it's going to work directly without doing anything in addition to calling the async function. > In contrast, for error returns it's very common to log an error but continue with some default value, or just skip processing one item and continue with the rest. There are a lot of cases where errors can be handled immediately, rather than always blindly bubbled up the call stackm It really is use-case dependent but I don't think it's more common than letting the request run in the background without waiting for its result. In fact your log example is a funny one because as soon as you use remote logging you're going to let the request run in the background and never await it (otherwise you'd get tons of gratuitous latency).
- deleted 2y ago[deleted]
- randomdata 2y agoYou're misunderstanding. async/await creates a dependence. errors do not – at least no more than any other type. Would you say your perspective on "coloring" is equally applicable to email addresses? I might buy into your idea the "?" operator colors functions. But that's something else entirely and obviously isn't what you were talking about originally as it isn't found in all of the languages originally mentioned.
- paldepind2 2y agoI think there's something important and correct in this take. As another example to your point, whether a function returns a list of foo's or a single foo is a also a color. You have to do completely different things when calling a function that returns a list compared to one that just returns a single value. And that's not just empty pedantry. Haskell let's you treat "list" as an effect (since it is a monad). Array languages like APL, J or R are colorblind with respect to "a list"/"not a list" as the difference is erased (to a large extend). So there really is a whole rainbow of function colors (basically types). On the other hand, this point of view doesn't really invalidate the original coloring argument at all. The effect encapsulated by "async" really is one that can be annoying and one that can actually be made implicit with a result that many would prefer.
- feoren 2y agoThis is exactly why I like all of this being handled in the return type. Return a Promise<T> or a Task<T> or a Stream<T> or an IEnumerable<T> as appropriate. Every function always returns immediately, but it might not do the important work immediately -- instead, it plans out the important work, which you can plug together as you see fit with other work and execute if you want, or simply return a composite plan for someone else to execute. All of its various "colors" are explicit in the return type. Combinatorial explosions are wrapped up in the Ts themselves. Sure, you might sometimes get a Stream<Array<Stream<T>>>, but if that's simply the appropriate type to describe what your function does, then who's complaining? As long as you have various ways to manipulate those types, like "Combine" (Array<Stream<T>> -> Stream<Array<T>>) and "Flatten" (Stream<Stream<T>> -> Stream<T>) then you can handle everything. And you'll find that getting your types correct increasingly implies that your entire code is correct, so the compiler is helping you out a lot.
- pistoleer 2y ago> In Go or Rust, returning an error is also a “color” that spreads out to the top of the call stack. That's wrong... Suppose A calls B and B returns a Result<T> (so it's colored as you say). A can match both the Some() and the Err() variants of B and return something other than the error in the latter case. So A can shed the color. There is no way to "shed" async-ness like that in javascript.
- littlestymaar 2y ago> There is no way to "shed" async-ness like that in javascript. In rust you can simply call `block_on` and call it a day. In JS there isn't such a construct by default because the blocking the thread would freeze the app, but it exists in every other language with async/await that I'm aware of.