"Expensive" is subjective and should be up to the programmer to decide.
And, it absolutely should not require creating two identical function definitions to get around the function coloring problem - that's completely indefensible.
> This is a good thing for programmers to easily see and know at the call site.
Yes, and the IDE can show you that information, exactly like it does the types of arguments. We don't redundantly write the types of arguments at the call site because it's a good thing for programmers to see (which it is), we let the IDE do that.
Yes, this is a great idea, and people would do it if there was widespread support for a binary structured format.
The difference between this incredibly ignorant sarcasm and my point is that IDEs already support the thing that I'm suggesting. You can already hover over a method and it'll give you information about the types of the arguments and return values that are specific to the call site.
> At the end it's a balance and subjective preferences
...and you completely missed the main point of my comment, which was not about what information to show, but avoiding duplication due to function coloring:
> And, it absolutely should not require creating two identical function definitions to get around the function coloring problem
I'm afraid I don't follow. It's impossible for you to have two identical functions (one sync and one async), since then they would both be sync. What did you mean?
Alternately, I want to be able to designate a computationally-intense function call (just that call, leaving the other calls alone) as async so that control yields to the event loop.
The main problem is that someone got it in their head that the function definition was the right place to designate whether a function was async or not, and it's not. The right place is the call site.
This unnecessarily trivializes the technical problems at hand. This wasn't just something someone got in their head, especially considering...
> that the function definition was the right place to designate whether a function was async or not, and it's not. The right place is the call site.
This is exactly what JavaScript did. The program only yields at `await`ed callsites and doesn't yield at all other callsites.
`async` only tells the VM to create the state machine necessary to resume the function after `awaited` calls return.
Making a language that automatically generates two versions of a function, one that is async and returns a Promise-wrapped object, and another that is sync, is not hard.
> This is exactly what JavaScript did.
No, it isn't. JavaScript requires you to declare functions as async at the function definition, and you can't call async functions from sync ones. This is exactly the thing I am arguing against, and is the opposite of "The right place is the call site."
> No, it isn't.
Yes, actually, it is. Async functions are a concept that only affects the internal structure of a function - to generate the state machine that allows yielding at `await` keywords and resuming after they resolve. Externally they are no different from "sync" Promise-returning functions.
So they only thing that async functions do is enable yielding at the callsite, the `await` keyword is what actually yields at the callsite and you can await anything, not just async functions.
Again:
> I want to be able to designate a computationally-intense function call (just that call, leaving the other calls alone) as async so that control yields to the event loop.
This is what `await` is - `await` yields. Non-await calls don't yield.
This comment shows a true departure from reality. Computing is filled to the brim with ideas that are easy but are not implemented for reasons other than difficulty.
> I suppose we should all await your backwards compatible proposal?
Your sarcasm merely helps to further demonstrate that you're not interested in a serious discussion.
> Yes, actually, it is.
Then it should be trivial for you to show me an instance of a sync function calling an async one, synchronously waiting for it, then getting back the return value, without clever hacks.
.NET doesn't really need to provide two separate utility methods like this though, because you can use Task.wait to block until the async task is done.
Their async variants call different OS APIs for asynchronous operation where available (Overlapped IO on Windows, on Linux it is specially scheduled (p)read and write calls).
A similar distinction applies to Socket where asynchronous calls use an internal epoll-based engine while synchronous ones are plain send/recv.
Generally speaking, in synchronous code there is no advantage to calling `.Wait()` or `.GetAwaiter().GetResult()` over synchronous APIs when such are offered, and if you can help it, it is best to update the code using them to be async too if your application is multi-threaded. Luckily it's quite easy in most situations unlike what HN public (that hates better languages like C#) would lead you to believe. But ff you do have to do block on waiting for a task, the impact on throughput is usually negligible if you do so within say threadpool worker - the implementation nowadays can cope with this very well and is more resilient to scenarios that used to be problematic.
Of course, in languages like Rust with multiple colors of async code (single-threaded or multithreading-capable), this would get very messy.
[0] As a major caveat, synchronous code also enforces various state transitions much more efficiently. Want to prove that IO is done (in the sense that the user code is done with it) before closing a file? This is pretty easy in single threaded blocking code.
Oh yes, absolutely. But the solution to that is stop using joke languages that can't do kind polymorphism.
> Yes, and the IDE can show you that information, exactly like it does the types of arguments.
How can it, if the language doesn't give it that information? Either you have a language that can reliably distinguish between sync and async calls in a standard way - which is to say, it has a type system that distinguishes between sync and async, whether it calls it that or not - or your IDE has an ad-hoc informally specified bug-ridden slow implementation of half of one.
This is very rarely necessary in app code. It's usually only a pattern for higher-level utility functions, and even then if you really wanted to you could unify implementations with generators.
Where do you see insanity like this?
If you were designing a language from the ground up, why would you implement a synchronous read operation? I would just assume that all code written in the language will treat async the way that Haskell programmers treat IO and make all IO operations async no matter what.
See https://news.ycombinator.com/item?id=41055328
This bifurcation is mostly a concern for the standard library. It’s not something you do this way in your regular application code.
IMO the more important part is that it can yield control. Half the advantage of cooperative multithreading is knowing where you may yield control makes sharing memory more or less workable.
I also like Trio’s model: if it’s async, it will yield control, even if the result is already available so it doesn’t technically have to. Makes it more difficult to accidentally starve other tasks. (I suppose I also have to mention Curio’s model: if it’s async, it’s a scheduler upcall, whether yielding or not. But for all that Curio deserves credit for freeing Python from asyncio hell, I just don’t find myself caring about things being scheduler upcalls or not.)
I feel stuck, our whole backend is legacy rails and I can’t escape.
It all depends on how the code is written. Eventually somebody managed to make the Erlang code faster than the baseline C, then someone else made the C version 8k % faster, which proves your point. However, how is that related to using sync/async vs message passing?
If you want to write high-performance code then you need to be able to write synchronous code and have control over what your yield points are. If you take the "all calls are potentially async, runtime does what it wants" approach (i.e. "no function colouring") then you just will not be able to do that.
What makes you think that?
> … Joe Armstrong … mentions message passing …
Where?
"4.5 Programming Notations Based on Message Passing" p33
"Concepts and Notations for Concurrent Programming", Gregory R. Andrews and Fred B. Scheider, Computing Surveys 15(1) March 1983, pp 3 - 43
[pdf] https://www.cs.cornell.edu/fbs/publications/LangSurv.pdf
The language and framework are both centered around developer happiness, which in my experience drops off around 10,000 lines. That's about when projects start getting difficult.
Ruby 12169
ERB 2339
Vue 24895
Js 4526
The Vue frontend is indeed more complex than the Rails backend, and in my experience Vue is much simpler than React. My customer organized the Rails app with models, controllers, api/v1/controllers, jobs, services (naming only the most important stuff). It's not bad to work with.I was quite shocked to read this, as in it never brushed my mind that it could be. I personally doesn't feel like it is and is one of the reasons why I try to avoid working with that stack at all cost.
I don't think the neon sign is a good excuse for the mess colored functions are. You easily can create a synchronous O(n^4) function, and there are probably tons of quick async functions. Moreover, your comment might be read as a free pass for using functions and methods that you're not really understanding the behavior off, it obviously doesn't sound like a good thing
The information is only hidden with colorless methods if you consider the documentation to be a place to hide information (o^o).
As far as the article goes, I think the pattern in Ruby is great. I prefer it to JS. But the JS approach works fine and the whole controversy about colored functions is a little silly.
I would be interested in understanding why JS can't enable await from inside non-async functions. I (maybe naively) wonder if the compiler couldn't just figure out which functions to treat as async rather than making the programmer do it.
Any function that calls another that returns a Promise will have to return a Promise to represent completion correctly. That's the color.
No, it doesn't, closures let you update values in the wcope of the parent in .then() callbacks and then you can loop in the function and wait for completion and return a non-Promise value.
So yes the compiler can figure it out, but only with Typescript. For javascript, a promise it a promise, but it doesn't know that a function will return a promise until it does. You can try compiling some async/await codebase to an old ES5 version and see the mess.
No, source code is where you hide the information, documentation is where you hide misinformation (or accurate information about how the function worked 3 versions ago, which is much the same thing).
Not having documentation just removes the ability to determine if what the code _does_ is what the code is _intended to do_.
Then I'd say the vast majority of Ruby libraries are buggy.
> Not having documentation just removes the ability to determine if what the code _does_ is what the code is _intended to do_.
But usually there isn't any intent when it comes to async-or-not. Library authors usually just write a method that results in the right value; whether that method can yield or not isn't even something they thought about, much less had a specific design in mind for.
Unlike Go or Ruby, JavaScript is a single-threaded language. Synchronization constructs such as mutexes and semaphores are uncommon and not part of any standard library. When you are calling a synchronous function, you can be completely assured that no race condition can develop while the function is running, but the same guarantee is not true for asynchronous functions.
That's why knowing which function is running asynchronously is even more useful for languages like JavaScript.
There is a parallel universe where JS added almost any other concurrency primitive and got a better trade-off than async/await.
For myself, I happen to agree with OP that single threaded plus async/await has honestly been my favorite concurrency model I've ever worked with for the types of programs I tend to write (IO-bound web apps with lots of network requests and no hot loops). The property that they described of having no race conditions of any kind except where you opt into them by calling out to an explicitly tagged async function is an enormous unnecessary mental burden removed.
In other contexts with higher compute demands, a full threading model or something like Go can make a ton of sense. But it's not worth the overhead for your average web app where what you really want is to just resume when that web request finishes.
Use the right tool for the job, don't get dogmatic about programming language features in the abstract.
Unfortunately, we do not live in that parallel universe, and JavaScript had to evolve the way it did. When callbacks have already been established as the standard concurrency mechanism, promises were a big improvement. When promises became standard, async/await was the natural evolution rather than breaking existing programs by introducing stackful coroutines.
Other languages which chose async/await (Rust, C#, Kotlin) had their own reasons. In all of the cases above, the language designers wanted to give the user more control over the scheduler (Colorless Stackful coroutines require a global scheduler).
In Rust's case, it's async/await state machines are just not wasting memory and unnecessarily copying it like stackful coroutines do (not to mention that Rust doesn't have a garbage collector and pointer rewrites, so it cannot just copy stacks around when they grow). In fact, Rust did have garbage collection and green threads, but they were both removed early on, as the language changed its focus to became a language where you can expect performance that are _at least_ on par with C or C++.
In case of Kotlin, this all mostly had to do with finding an abstraction that can run on pre-Loom JVMs, JavaScript targets and native targets. Kotlin does not own the runtime.
Saying everybody should follow Go here is silly. Not all languages share the same background and design constraints. Go-style Stackful coroutines require garbage collection, full control of the runtime and generally a language that is designed from scratch to have them - or a 6-year project to adapt your language and runtime to have them, like Project project Loom did.
Async/await imposes some costs, but the benefits (compatibility, performance, flexibility) often outweigh these. After working with both async functions and stackful coroutines in multiple languages, I cannot say I ever felt concurrency in Go to be more ergonomic than Kotlin. If anything, it was the reverse for me in that particular case (owing to Go's lack of reification for goroutines and verbose syntax). Deciding in advance what color is my function (i.e. whether it needs to deal with I/O), is pretty simple and it rarely changes during the lifetime of a program (and even when it does, refactoring is not hard unless your design is bad).
The only case where I did feel some pain using async/await, was Rust, and this has more to do with Pin, competing async runtimes (for a while), lack of support for async traits (for a while) and other areas where Async Rust still needs some work.
That's exactly the mindset of quite a number of programmers nowadays, sadly. Two anecdotes: First, there was a blog post (probably even featured on HN) about problems in the design of the API of... Python's requests library, I think? It was quite a bizarre reading, complaints made almost no sense to me until I got to about the middle of the article when it clicked: the author was trying to figure out how to use the API by just reading the names of the classes, methods, and parameter names and making guesses as how it all would connect together instead of, you know, reading the docs. Which do exist, by the way.
The second example happened just yesterday at my job. In the essence, some fellow programmer added a call to function foo() which he thought would do some particular thing and return a particular answer while in reality it did not quite do that, and the return value is essentially meaningless (the function basically always returns "yep, I've scheduled do_foo() to run asynchronously some time soon"). Now, this foo() dunction is not documented, and if you read it's source (3 lines of code), it's quite obvious that its return value is useless... so why did they think it works the way they wanted it to work? When asked about it, the answer was "Well, it's named foo(), so I just thought that's what it would do". Sadly, calling strange third-party functions and hoping them to work they way you'd like them to work is no basis for writing the working programs.
I kind of wish the languages I use had Haskell's IO monad too, to separate functions in terms of the type system, but that's slightly different.
You might like this article (which is my personal favourite about function colouring). https://www.tedinski.com/2018/11/13/function-coloring.html
Or wait for them to finish.
> It's a big neon sign that says "hey, this function call is expensive"
Async and expensive aren't the same thing.
> Also, a nitpick - you can call async functions from sync ones, you just can't access the return value.
That's a quirk of the particular async/await implementation in JS and not generally true of colored-function implementations. (Actually, since async/await is sugar for promises, you can actually, I’m pretty sure, both call and use syntactically async functions from ones which are syntactically not-async if you really want to, the code is just ugly.
It can be, but this fact doesn't always scale well to larger APIs over time.
If you see that a function is async, any of these could be true:
1. The function does some asynchronous work.
2. The function did some asynchronous work at some point in the past, but does no longer, but changing the signature is a breaking API change, so it still looks async.
3. The function does no asynchronous work, but it's a public API and the library maintainer thinks there is a good chance it may need to do async work in the future so made it async today to reserve the right to make that change without breaking the API.
4. The function may or may not do asynchronous work, but it's part of an interface that others are allowed to implement and the designer of the interface wants to give implementers the freedom to make their implementations async if they want.
5. The function is higher-order and wants to be able to accept callbacks that are themselves async.
There are probably others I'm forgetting.
This all sounds hypothetical but it's really not. If you maintain any widely-used long-lived package, you run into design questions like this all the time.
There is always a tension between an API consumer wanting to know what a function "really" does versus the API author wanting some abstraction so that implementation details can be changed over time without breaking users.
Asynchrony is one kind of effect or observable behavior that a user/maintainer of an API may or may not want to encapsulate, but it's not the only one. The C++ standard library documents and commits to the algorithmic complexity of most functions. Haskell function signatures pin down whether or not a function does IO. Statically typed languages pin down the types parameters must be.
Pinning down more of this stuff gives the API consumer some more information, but it's not always a pure win. It calcifies the behavior of the function in ways that can harm its evolution or interact poorly with higher-order code or polymorphism. There's no silver bullet.
Personally, I've never found "does this function suspend" to be a particularly interesting effect for a function to have to commit to and the fact that it poisons the entire callstack makes it very difficult to work with in practice.
Knowing ahead of time which functions are async is a feature.
It's a big neon sign that says "hey, this function call is expensive". This is a good thing for programmers to easily see and know at the call site.
If you make multiple calls with async/await in a row, the performance issues are plainly obvious at the call site. With "colorless" functions, this information is hidden in a deeper layer. You have to know what the function does on the inside to even know what its performance impacts are.
Also, a nitpick - you can call async functions from sync ones, you just can't access the return value. Sometimes, you don't need to.