Welcome! Please see the About page for a little more info on how this works.

0 votes
in core.async by

I'd like to see a variant of the "go" macro that executes immediately until the first parkable operation instead of dispatching (effectively calling "setTimeout" twice). This is primarily for CLJS since CLJ can get away with blocking operations but it still might be useful there as well.

My Use-Case:

I currently use core.async to orchestrate CSS animations together and while it works perfectly fine with "go" it does require careful attention to detail on the order of operations since "go" will yield control back to the browser which may decide to reflow/repaint the screen although the animation wasn't properly started yet.

Example:

`
(defn show-toast [message]
(let [toast (create-toast-dom message)]

(dom/append js/document.body toast)
;; X - PROBLEM: REPAINT HAPPENS NOW
(go (<! (anim/slide-in-from-bottom animation-time toast))
    (<! (timeout 3000))
    (<! (anim/fade-out animation-time toast))
    (dom/remove toast)
    )))

`

This example tries to create a "Toast" (link: 1) DOM node which is animated in from the bottom and removed after 3 seconds. A real implementation would require handling clicks and so forth but the example is easy enough to describe the problem. The {{(anim/...)}} functions set the appropriate CSS and return a {{(async/timeout time)}} channel which lets us wait for the "completion".

The problem at X is that the go is dispatched and control is given back to the browser. Therefore the "setup" part of the animation is delayed and the toast dom node is rendered by the browser as is. Usually this means that the browser will do a repaint. This will have a "flicker" effect as the node appears and then disappears again and animates (depending on the default CSS).

The simple solution is to move the {{dom/append}} call into the go block, which is easy enough in this example but hard for more complex. Another solution is to start the animation outside the go:

`
(defn show-toast [message]
(let [toast (create-toast-dom message)]

(dom/append js/document.body toast)
(let [slide-in (anim/slide-in-from-bottom animation-time toast)]
  ;; REPAINT, but slide-in animation already started so no "flicker"
  (go (<! slide-in)
      (<! (timeout 3000))
      (<! (anim/fade-out animation-time toast))
      (dom/remove toast)
      ))))

`

Again, simple enough here, hard for more complex things.

What I would prefer to see is a "go!" variant which instead of dispatching the go block just starts executing it.

`
(defn show-toast [message]
(let [toast (create-toast-dom message)]

(dom/append js/document.body toast)
(go! (<! (anim/slide-in-from-bottom animation-time toast)) ;; REPAINT ON <!
     (<! (timeout 3000))
     (<! (anim/fade-out animation-time toast))
     (dom/remove toast)
     )))  

`

The subtle difference is that the browser does not get a chance to do anything before we are ready and would wait anyway. After fine-tuning a few of those animation effects myself it always requires moving stuff out of or into go blocks as it stands now.

I'm aware that this is a rather specific use-case and core.async might not even be the best way to do this at all but so far I had great success with it and the code is very easy to reason about.

I have not looked too much into the internals of core.async but go! should be a pretty simple addition. If this change is accepted I would start looking into things and create a patch, just wanted to make sure there were no objections before doing so.

Hope my intent is clear. CSS animations can be a bit counter-intuitive which might not make it obvious where the problem actually lies.

(link: 1) http://www.google.com/design/spec/components/snackbars-toasts.html#snackbars-toasts-specs

3 Answers

0 votes
by
_Comment made by: thheller_

I looked into the issue to find out what needed to be done on the core.async side to facilitate this change, turns out nothing.


(defmacro go!
  "just like go, just executes immediately until the first put/take"
  [& body]
  `(let [c# (cljs.core.async/chan 1)
         f# ~(ioc/state-machine body 1 &env ioc/async-custom-terminators)
         state# (-> (f#)
                    (ioc/aset-all! cljs.core.async.impl.ioc-helpers/USER-START-IDX c#))]
     (cljs.core.async.impl.ioc-helpers/run-state-machine state#)
     c#))


Works perfectly fine so far and can live perfectly well inside a library, I'm beginning to think that this might be a better "default" for CLJS since there are no "threads" that could start running the "go".

With the normal "go" the flow of execution is:

code before go
setTimeout/nextTick
code inside go until first take/put
setTimeout/nextTick

With go! it is:

code before go!
code inside go! until first take/put
setTimeout/nextTick

Given the non-blocking nature of Javascript the extra setTimeout does not hurt it doesn't provide much benefit either.

Since no changes to core.async are required I can simply keep this macro in my library. My use-case is rather specific so feel free to close the issue if there is no interest to make this "common".
0 votes
by

Comment made by: lgs32a

As a default this would violate the async execution guarantee of go which would be a breaking change. Some observations from my user perspective: For readability I'd prefer both examples you proposed for usage of go over go-now because go conveys async execution of its entire body whereas in go-now I'd have to scan the body for a parkable operation to determine the part which executes synchronously. I find it difficult to convince myself that there could really be problems so complex that go-now would be preferable over refactoring go. (I don't think you can safely rely on impl namespaces in library code :-))

0 votes
by
Reference: https://clojure.atlassian.net/browse/ASYNC-131 (reported by thheller)
...