Now, what do you do when you want to return from that function after loadRealImage() finishes?
function loadImage(src) {
loadRealImage(src).then(img => /*... return img from loadImage ...?*/);
}
"Just make it an async function." Well yes, but then the async nature contaminates the rest of your code. You have to make sure to set up a toplevel try-catch for every one of your async chains, for example.
What do you do when you want to have an async generator? That is, you want to both await on something, and yield N values from the function. JS makes that difficult.
There are all kinds of limitations like this, and everyone has their own favorite hacks around them. But they're hacks, not unification. And yes, hacks can be effective, but a unified framework can be tactical.
In this case, I'd like to write blocking-style code.
longjmp, however, is a crucial missing facility in JS. Emacs relies on it heavily in its design -- it's how catch / throw work, and it's why you can do things like
Without longjmp, you can't do that. Just as you can't in JS.
So what? Well, that means you can't write emacs, because you're limited to what JS provides you. An entire class of software is beyond your ability to write, because you cannot provide the same features that other runtimes give.
This gets me started on the lack of any kind of reasonable error definitions in JS. In elisp, you define errors. Imagine you want to write some code that parses some parens -- it turns "(a b (c))" into ["a", "b", ["c"]]. What do you do when your program encounters "(a b" and then the end of the string? Throw a scan error!
Not in JS. It's considered poor manners to throw errors to the people using your library. Worse, it's a pain in the ass for users to catch and respond to errors. If you use someone's library, you usually don't expect to have to wrap it in a try-catch. And the code is massive:
try { operation } catch (e) { if (e instanceof ScanError) { do something else } }
Contrast that with elisp:
(condition-case nil
operation
(scan-error do something else))
There's no contest. It's way easier to write the latter than to use the tools JS gives you. But it's a cultural difference, and culture is slow to change.
That's why webassembly at least gives an escape hatch.
function foo() {
return doSomething();
}
async function doSomething() {
...
throw ScanError();
}
Spot the bug? That's an async function. Every JS programmer worth their salt will tell you how many times they've been annoyed to discover a missing await, and that the promise is failing mostly-silently (or worse, it works by accident until it doesn't, since that code will work fine most of the time).
Well JS does provide this via exceptions.
Second it's totally crazy that emacs depends on longjmp
It's not crazy.
function bar() {
[1,2,3].forEach(x => {
if (x == 2) /* return from bar...? */
}
}
Why can't you write this code? I mean you "can":
function bar() {
let tag = [];
try {
[1,2,3].forEach(x => {
if (x == 2) { tag.value = x; throw tag; }
}
} catch (e) {
if (e === tag) {
return e.value;
}
}
But holy crap that's terrible. EDIT: I also forgot to rethrow the error, showing just how easy it is to screw up.
Just write it as a for loop, then. Well sure, except for the hundreds of libraries that don't support that style. What do you do when you want to return prematurely from the iterator functions you pass to them? Now you can't just make a for loop unless they provide Symbol.iterator. And 9 times out of 10 they give you a promise, meaning you're forced to convert your code to async style.
It's a huge mess, and longjmp saves you a lot of headaches in disciplined situations. Every tool has its place, and it's strange to argue that a computer should be able to do less, not more.
The goal is to save you time in the long run. And those 5 lines better be exactly right, or you'll waste a lot.
Yes, but it can’t easily interop with the host language. Elisp saves the stack, which you can’t do in JS. That means you can’t cross JS -> elisp boundaries with your longjmp without some pretty awful hacks.
You cannot longjmp across 'native' stackframes (which mightor might not be a big limitation), but otherwise interop should be fine. The native interpreter stack is always around to be used to run native function calls.
But sillysaurus3 has a valid complaint about this line of code, that the very next line is completely blocked until multiValuedReturn() completes. A better solution is to not use `await` here, and instead use .then and Promise.all to control execution of several callbacks, while running other code synchronously.
I think the problem sillysaurus3 is facing is that he wants complete convenience of the kind that async/await bring, meaning being able to execute code regardless if it finishes now or later, but always pretending that it finishes now. That's just not possible, and for good reason: some things happen now, and some things happen later, and if you need ultra-fine-grained control over what happens when, then you need to be very explicit about what's happening and how and when. It's messy and ugly, and we're working towards cleaning that up, with async/await being a great step forward, but this is inherent to the concepts of sync/async control flow, and the assembly solution is much more of a hack to solve this (if I even understand the solution correctly) than Promise/async/await/etc.
That's just not possible, and for good reason: some things happen now, and some things happen later, and if you need ultra-fine-grained control over what happens when, then you need to be very explicit about what's happening and how and when.
Green threads. No silver bullet, but sometimes you upgrade your pistol.
JS needs greenthreads. There's no reason you shouldn't just spin up a thread for every blocking context. This is arguably what async/await already does, but you don't have control over the toplevel loop. Point out where your while (userIsOnWebsite) { ... } loop is. :)
Having a toplevel loop is very important for simplicity -- but even moreso for what you mention: having ultra fine-grained control, and not constantly fighting with the underlying scheduler / ecosystem.
I've seen a lot of debate over this, but I find greenthreads so much simpler to work with and reason about, on top of keeping my code clean and completely interoperable with my synchronous code.
This is off-topic from the current JavaScript discussion, but even though Python introduced "first-class" async support a while ago, I still exclusively use gevent [1] for all of my concurrency needs. It provides full greenthread support for Python. It's extremely performant on top of being simple and clean to integrate. In contrast, Python's asyncio and async/await feels needlessly verbose and tortuous, often with full rewrites or replacements of libraries required to actually take advantage of the features (e.g. https://github.com/requests/requests vs. https://github.com/aio-libs/aiohttp). With gevent, I can just put whatever code I want into a greenthread and get instant asynchronicity (excluding hiccups with a few 3rd party libraries that have network I/O in native extensions, which is rare).
The one downside is it achieves its magic with standard library monkeypatching, which is pretty hideous, but it's done very seamlessly. Developers are able to write the exact same code with or without monkeypatching and not worry about what it's doing. I've never encountered an issue with the monkeypatching when using any standard or 3rd party library.
How does gevent handle spawning kernel threads? Last time I tried Go I found that it spawned unlimited kernel threads. For example, a thousand goroutines calling stat() would result in as many kernel threads.
I ended up tracking down which goroutines were likely to spawn kernel threads (gross global reasoning), and applied a rate-limiter to that set.
Unfortunately, Python is still bound by the GIL, so to my understanding, gevent will never spawn any kernel threads. This effectively means there is no true parallelism possible; context switches between greenthreads pretty much only occur when waiting on I/O. I believe this is the case for all other Python concurrency libraries as well, including the threading library in Python's standard lib.
Most people, including myself, use gevent for I/O bound applications (where it excels), so this usually doesn't pose any issues. For CPU bound tasks where performance is important, I'd probably use something other than Python (likely Go, personally).