always-be-clojuring / issues

16 stars 0 forks source link

Async programming #2

Open alex-dixon opened 6 years ago

alex-dixon commented 6 years ago

What’s our story here for people getting started?

Core.async? Futures? Promises? Callbacks?

JavaScript developers are becoming accustomed to async/await and promises instead of callbacks. The latest Node supports async/await and any JavaScript API that supported promises (a lot of them) now supports it. That generally makes async programming pretty simple.

const myFn = async () => { const value = await otherAsyncFn() return value + 1 }

ericnormand commented 6 years ago

This must be about ClojureScript, because we don't do much Async on the JVM.

My recommendation is core.async. I think we should emphasize that there are only a few important things to know (<! >! go and chan, that go blocks can't cross fn boundaries).

The other thing to emphasize is that channels have a richer structure than promises, and the means we have for combining them are richer (alts! vs Promise.race, for example).

I don't think we can compete with it being built-in. But we can make the setup as easy as possible. I'm not sure what the current CLJS compiler does with macros nowadays. It used to be that you had to require and require-macros.

There's also the additional benefit that it works with callback-based solutions in addition to promise-based solutions.

That said, I think JS is closing a lot of gaps with the advantages CLJS had a few years ago, and this is one of them.

BTW, do browsers support async/await?

alex-dixon commented 6 years ago

@ericnormand I think Babel still adds a polyfill anywhere you use async/await, but browser support seems mostly there (didn't realize this): https://caniuse.com/#search=await

Interesting thoughts here on the subject of core.async vs async/await: https://groups.google.com/forum/#!topic/clojurescript/LBy0yiZiWrA

ericnormand commented 6 years ago

Well, I've been thinking about this all day. And then an async/await podcast showed up in my player. I've been thinking about the canonical simple example for where csp differs from promises.

Imagine we are building a web app that has some pretty hard timing constraints. We want to show whatever we have very quickly, let's say within 100 ms. So we have a cache, which is basically instantaneous, with an old value. And we may be able to get the new value from the server within that time, so we should try. But if we are waiting too long, we need to just go ahead and show the old value.

(defn best-value-in-ms [ms]
  (let [alarm (async/timeout ms) ;; start your stopwatch
         ajax-chan (ajax/get "backend-server.com/up-to-date-value") ;; returns a channel
         old-value (get-cached-value)] ;; instantaneous
    (async/go
      (let [[v c] (async/alts! [alarm ajax-chan])]
        (if (nil? v) ;; the timeout fired
          (do
            ;; well, the response did not come back in time. drats! let's have something wait for it
            ;; so we can update the cache whenever it does
            (async/go (set-cached-value (async/<! ajax-chan)))
            ;; but immediately we'll return the old value
            old-value)
          (do
             (set-cached-value v)
             v))))))

Discussion

I choose this example because it is very reasonable to expect to do this. You want to do this all the time. It's even reasonable to explain in English. But can it be done with Promises? You can get far with Promise.race. But what happens to the slow ajax response? It's thrown away! You can't use the value of that very expensive network call and now you're going to have to do it again next time anyway.

Let's try it in JavaScript:

function bestValueInMs(ms) {
  // just get the cached value now
  var oldValue = getCachedValue();
  // we need a custom promise
  return new Promise(function(resolve, reject) {
    // keep track of whether we're done.
    var done = false;
    // here's our alarm, it resolves to the old value if we're not done
    setTimeout(function() {
      if(!done) {
        done = true;
        resolve(oldValue);
     }
    }, ms);
    // now we can get our value
    ajaxGet("url").then(function(newValue) {
      setCachedValue(newValue);
      if(!done) {
        done = true;
        resolve(newValue);
      }
    });
  });
}
alex-dixon commented 6 years ago

@ericnormand Thanks for pushing the conversation forward.

We want to show whatever we have very quickly, let's say within 100 ms. So we have a cache, which is basically instantaneous, with an old value. And we may be able to get the new value from the server within that time, so we should try. But if we are waiting too long, we need to just go ahead and show the old value. [...] I choose this example because it is very reasonable to expect to do this. You want to do this all the time. From a less experienced-programmer's perspective this might not ring true. It doesn't to me, maybe just because I haven't been required to write code that does what you describe. To me 99.9999...% of async Javascript code just needs to get a value from a server before doing something else.

That said...this is beautiful (use of alts! etc). I like how you've provided us with an example of something CSP can do that Promises can't (would like to see more of this). But....I think the paramount issue for newcomer JS devs wrt async isn't whether they can do things they haven't thought of or haven't been asked to, but how to do in CLJS what they do all the time in JS.

A comment from the above Google thread by Andrey Antukh:

I think you are comparing apples with oranges. CSP and async/await can't be compared directly. Async/await works with a promise (one value) abstraction and csp works with channel abstraction (sequence).

It seems is an anti-pattern use channels as promises because them does not has the notion of error. I remember that Timothy Baldridge have said something similar about this:

"A sort of anti-pattern I see a lot is creating a lot of one-shot channels and go blocks inside every function. The problem, as you see is that this creates a lot of garbage. A much more efficient plan is to stop using core.async as a RPC-like system, and start using it more like a dataflow language: Identity data sources and sinks, and then transform and flow the data between them via core.async.

It's interesting to note that core.async started as something that looked a lot like C#'s Async/Await, but that was dropped in favor of CSP pretty quickly. So there's reasons why the language isn't optimized for this sort of programming style. "

What I'd like to be able to say to Javascript programmers:

  1. You can write async code in CLJS the same basic way you do in Javascript. Here's the translation. e.g. https://gist.github.com/shaunlebron/d231431b4d6a82d83628
  2. We also have CSP/core.async. It's different from anything you probably know. You don't have to learn it -- you can use what you know from JS 'til the end of days. But if you want something different, it's there.
  3. Here's what might make core.async "better" than Promises or value-based abstractions. Insert examples here.

Unfortunately, 1) seems untrue. It would require using core.async, which straight away makes avoiding the introduction of new things like channels, takes and puts difficult. Further (and more problematically), to the extent you can use core.async to mimic async/await, you create a lot of one-off channels, which isn't performant.

So maybe I can't say what (I think) I want to say to a JS programmer. Instead:

  1. We don't really have an equivalent for async/await. We have no need for it.
  2. We have Promises and callbacks just like JS. We don't see that as a "step backward" from async/await. They work just fine for .
  3. We have something "new", different, and "better" than any of these (channels, core.async). Just like Golang.

Have to get to work but... @ericnormand thanks for making me think that maybe we shouldn't shy away from core.async.

Food for future thought: Article comparing async/await in C# vs. Golang wrt perf. There are probably others that deal with differences in the paradigms on a more conceptual level. https://medium.com/@alexyakunin/go-vs-c-part-1-goroutines-vs-async-await-ac909c651c11 JS's proposal for async iterable? Foggy on the status of it and how it might change the perspective of JS devs coming to CLJS