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.
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
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
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…
promises 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.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