I’m going to make a somewhat bold statement: core.async
does not work with ClojureScript. And, in this post, I’m going to show some examples why this is true, at least for the current versions of core.async.
So let’s start by understanding a little bit about the runtime: Javascript is a single-threaded runtime that implicitly runs an event-loop. So, for example, when you ask to read a file, you can do it synchronously or asynchronously. If you decide to run in that asynchronously, it means that as soon as you issue the fs.readFile
command, you need to register a callback and the control is returned to the “main thread”. It’ll keep running until it runs out of commands to execute, then the runtime will wait the result from the callback; when it returns, the function that you registered will be called with the file contents. When the function ends, the JS runtime will await to see if there’s any other pending call, and it’ll exit if there’s nothing else to do.
The same thing happens in browser environment, but in this case the callbacks are events from the DOM: like clicking on buttons or listening for changes in some elements. The same rules apply here: the runtime is single threaded and when something happens it will first execute everything that needs to be executed, then it will be called back with the event that happened.
So maybe we can change these callbacks with core.async
channels right? But the answer is no, because core.async
s go
blocks will not run in different threads (because, again, the runtime is single-threaded). Instead, it creates a state machine and it’ll control of when each of these go
blocks will be called, at what time, eventually replacing the event-loop that Javascript environment already have.
So what you get is essentially a worse version of Javascript event-loop. It’s worse first because the original is optimized for the JS environment (and have years and years of experimentation, fail, retry, bug fixes, performance issues, support from multiple developers and browsers; instead, core.async have few years and it’s only being used scarcely by the Clojure community), and second because if you have exceptions or any kind of errors in the events that happened, the JS event-loop allows you to catch then globally, or you can let the environment handle it – most of the time, JS just prints an error and keeps the environment running.
The same will not happen with core.async’s state machine – and believe me, I already broke the Atom editor multiple times while I was developing Chlorine because some exception happened in some go
block, that were captured by the main thread of the runtime, and at that time the state machine decided that my block was running on the exact same thread/fiber/whatever JS uses to separate contexts than the main editor one…
What about promises? Again, does not solve the problem because channels do not have the concept of an error – they just return a single value. In Javascript, promise
s are really close to the either monad: they are asynchronous (always), and they have a “left” (that you handle with .catch
) and a “right” (that you handle with .then
). There’s no such concept in core.async, so you have to check every single channel if its result is an exception, and assume that this is a failure (you need to assume because you can’t be sure – you can return an error as a “right” value, and you can fail a promise with any arbitrary object like a string). Also, you can compose promises (with Promise.all
or Promise.race
or others) and they will return what you expect (a failure if one of then failed in Promise.all
, for example). They also short-circuit – a pipeline of .then
functions will stop with a failure without needing to check every single function result on the pipeline. Now, try this with promise-chan
… let’s not forget that you can catch pipelines of errors, or use .finally
to cleanup resources after a failure or success, even after a pipeline, without needing to check, again, every single return value of every promise.
You can’t even use transducers, because there’s no way to reliably control backpressure on channels in ClojureScript! Obviously, you can drop messages or create an unbounded buffer, but these are not solutions if you can’t loose messages and don’t want your code to consume all your machine’s RAM. The code below does not work, for example:
(defn read-from-socket [host port] (let [chan (async/chan)] (.. socket (connect host port) (on "data" #(async/go (async/>! chan (str %))))) chan))
Because it’ll blow the upper limit of pending messages on the channel c
. You can’t put go
outside the callback (for example, right before the socket connection) because go
-blocks will stop working when you define new functions (even anonymous ones). And there’s simply no way to make it work – there’s no way to “park” (using the channel’s nomenclature) the thread (for whatever thread means in JS) because you’re creating a new go
block every time. And there’s no way to detect how many pending operations you have right now and call .pause
on the socket, and .resume
after it did process some code – it literally created a problem that do not exist on JS at all…
In the end – there are two ways of using core.async on ClojureScript: one, is to replace promises with promise-chan
, and lose all composability, error treatment, interop with Javascript, pipelines and functional constructions (and also risk crashing the whole Javascript VM), and the second is to replace callbacks and losing the ability to control backpressure, have to manually handle memory, buffers, and also creating a whole new world of problems that you simply do not have if you don’t use core.async.
UPDATE: please see the latest post on this subject, when I try to address some of the proposed solutions that people asked me to do.