More async/await adventures. I rewrote the go macro to just use async/await. All tests are passing, except those that need macro expansion in the go body, like alt! and stuff. But conceptually it works...
https://github.com/borkdude/core.async/commit/afb51951260e8f2b4b7e741c57ccb37d020b861b
So next step: macro-expansion. That's probably all that's needed in the go macro. (famous last words...)
Ran 45 tests containing 210 assertions.
0 failures, 0 errors.Got most tests working now, with macroexpansion. Some things to figure out, but it's going to work. Spent 1 day on it now. 1 more day should do it... Ran 56 tests containing 192 assertions. 3 failures, 0 errors. cc @dnolen
you can view (WIP) changes here. https://github.com/borkdude/core.async/compare/main...async-await
wow! 🙂
Ran 56 tests containing 228 assertions.
0 failures, 0 errors.works now. I have a few special forms to implement but this is trivial. probably untested in the test suite.
If you want to try out my CLJS with async/await support + core.async based on async/await instead of state machine (probably better perf and less bundle size):
org.clojure/clojurescript {:git/url ""
:git/sha "2ecd2ebd8b79a5ad04c568bc348b4881ddedb4d7"}
org.clojure/core.async {:git/url ""
:git/sha "dc9650c0205dca6c0af1b41f198c0fd3226e6f15"}
Please give it a spin! :)Successful tests on CI: https://github.com/borkdude/core.async/actions/runs/21048351978/job/60528470570
@dnolen it seems to go macro in CLJS didn't have support for letfn analysis. Is that something you would want? Easy to do, but since the old one didn't have it, I might just leave it out
I think I'll fix it then
why keep any of the IOC stuff at all?
what exactly do you mean?
you can view the diff here: https://github.com/borkdude/core.async/compare/main...async-await I removed a lot of IOC stuff. Now it's just a pretty basic rewrite of the body to await + some helper fn
I don't think anything needs to be rewritten anymore? you just turn ( into an alias for (await (take-from-chan-and-returns-promise c)). so and >! and alts! just become a macro that emits an await
at least my instinct says thats enough? I actually think generators would be better than async/await, but haven't played with either
That is basically what happens but you need to • keep track of locals while rewriting this. • expand macros since they can expand into <! etc.
One thing I did that I'm not 100% sure of. If take! returns a value immediately I skip await. I think that conforms to how go worked previously: no task switching when the value is immediately available. That was just a small change I did the simple approach you like suggested before with a clojure.walk, and 90% of the tests passed, but it's not enough.
I really need some time to work on test support. I never run tests in for cljs code but the experience is horrible 😛
incomplete sentence?
are you asking how to run the tests for this branch? not sure what you meant
just a comment, not related to anything you are doing. just ran the tests via shadow-cljs :browser-test and its real bad 😛
this is how I run the tests:
rm -rf out-simp-node/; clj -M:cljs-test:simp && node out-simp-node/tests.jsok super rough quick and dirty
(defmacro go
"Asynchronously executes the body, returning immediately to the
calling thread. Additionally, any visible calls to <!, >! and alt!/alts!
channel operations within the body will block (if necessary) by
'parking' the calling thread rather than tying up an OS thread (or
the only JS thread when in ClojureScript). Upon completion of the
operation, the body will be resumed.
Returns a channel which will receive the result of the body when
completed"
[& body]
`(run-async-go
(^:async fn []
~@body)))
(defmacro <! [chan]
`(await (cljs.core.async/promise-<! ~chan)))
(defmacro >! [chan val]
`(await (cljs.core.async/promise->! ~chan ~val)))
(defmacro alts! [chans & opts]
`(await (cljs.core.async/promise-alts! ~chans ~@opts)))thats the entire macro side, none of the ioc stuff
Ran 56 tests containing 228 assertions.
0 failures, 0 errors.seems to work?
https://github.com/thheller/core.async/commit/71f892460298b79a22cc9ffbddfd547af5b20fcf pressed reformat code, so the diff is way larger than it needs to be
but again, I think generators would be better. Requires a basically full rewrite though, not as easy as just async/await
I'm assuming w/ async/await you have to through promises - wondering if there's any perf implications here for the streaming style patterns
yeah wondering that too. I don't think I have ever actually used generators anywhere, so no clue how it compares
@thheller That's a great alternative. I didn't think of that because <! were functions but this is even better :)
updated: https://github.com/borkdude/core.async/tree/async-await-simpler based on @thheller’s suggestion (diff view: https://github.com/borkdude/core.async/compare/main...async-await-simpler)
nice part is that then just also works inside regular async functions. so interop between channels/promises should be easier.
could also just make <! smarter to detect promises, so it just works with channels and promises seamlessly
@borkdude why did you remove the run-async-go part of the go macro? your variant generates "a lot more code" that is just repetitive and could be handled by a helper function?
I didn't remove, I preserved. But good point, I can make it a helper functionL on it
@thheller one benefit of keeping the current is approach is that it doesn't do any task switching when the body contains sync parts. e.g.
(go (do-something)
(
(do-something) will still be in the same task
Do you think this matters? This is also why my previous expansion was like this:
(let [ret (go-take! ..)]
(if (instance? js/Promise ret) (await ret) ret))
This helps not switching to another task in between if the value is available immediatelyno clue if that matters, but thats the part that gets much easier with generators I'd assume
it's not like this is not easy, it's just one line more
true, just seems dirty 😉
generator functions is something that's pretty easy to add after the async support is merged
yes, thats not the hard part. rewriting core.async to make use of it properly is 😉
how would you roughly sketch out how it would work with gen fns? (pseudo-code ok)
although probably also not that bad, given that there will still be no ioc macro rewrite that made everything go related to complicated
well basically you flip back to an inversion-of-control style implementation. so instead of <! doing a await it does a yield and the return value is a "placeholder" saying "I want to take from X"
the "go" implementation loop then works similar to the current IOC thing. so a state machine that calls .next() and does whatever depending on the return value
and what would be the benefit of that?
in theory "less" async stuff. so almost no promises need to be created and stuff like that. not sure thats actually a benefit though.
I like the idea of core.async being able to decide when and how to go async. instead of leaving that control to async/await of the runtime.
stacktraces are probably more useful with async/await though
yeah that's definitely a benefit of this async/await approach
Even after review I think this one will need to stew so we can collect information about the implications. I definitely think this is the right direction, but some due diligence required
FWIW maybe its best to just do all this in a new dedicated namespace, not touching the original stuff at all. could also just exist as a separate library. there is enough API exposed so that no modification to core.async are needed for this to work. would allow extensive testing without breaking any existing stuff.
Right
Someone gave me this example that he used to test comparing missionary with core.async.
(let [n 1000
cs (repeatedly n a/chan)
begin (js/performance.now)]
(doseq [c cs] (go (a/>! c "hi")))
(go
(dotimes [i n]
(let [[v c] (a/alts! cs)]
(assert (= "hi" v))))
(println "Read" n "msgs in" (- (js/performance.now) begin) "ms")))
With the original core.async, I get around 120ms in a REPL.
With my non-simple branch I get around the same... but sometimes 200ms.
With the simple branch I get around 200ms.
That could be the async boundary prevention in my non-simple branch, which is easy to port to the simple branch.
I should probably make an advanced compiled version of that and run it with bigger numbers. So just FWIW. JS benchmarking is hard...Oh yeah