Exploring Core Async

2021-09-06 19:20

I would really like to better understand Core Async with Clojure; specifically, I want to remove my self from callback hell to see if I can make my clojurescript backend for a new project to be a bit clearer.

I have only looked at core.async briefly before, and like with many other concepts I previously found complicated, as I've amassed experience bit by bit, on returning to it for the second time I find that it is a bit less confusing. With that said, I have yet to find an article or answer online that specficially demonstrates fixing callback hell with core async.

In my current project Trunk I'm building a "full stack" clojurescript application with electron, using SQLite as the database. Sqlite on node is built with callbacks in mind, and so some of my code requiring multiple database calls is starting to look pretty hairy:

(defn article-get
  "Fetches an article, and computes the `:word-data` for it. Sets `last_opened` value before fetching."
  [id cb]
  (let [q1-sql    "UPDATE articles SET last_opened = ? WHERE article_id = ?"
        q1-params (array (js/Date.now) id)
        q2-sql    "SELECT * FROM articles WHERE article_id = ?"
        q2-params (array id)]
    (.run db q1-sql q1-params
          (fn [err]
            (.get db q2-sql q2-params (fn [err row]
                                    (words-get-for-article  (js->clj row :keywordize-keys true) cb)))))))

The above function is making two database calls: the first is to update the article Trunk is about to fetch, and update it's "last opened" value to the current time. Then I can go ahead and select the value back in the second command.

So, we have callbacks (and errors that I'm not sure how to handle yet). I'd really love to see how core.async could (if it can) be used to clean this up?

So far, I've found the chapter on core.async in Clojure for the Brave and True to be helpful but I haven't gotten it adapted to work yet for this solution.

The example it provides looks like this:

(defn upper-caser
  (let [out (chan)]
    (go (while true (>! out (clojure.string/upper-case (<! in)))))

(defn reverser
  (let [out (chan)]
    (go (while true (>! out (clojure.string/reverse (<! in)))))

(defn printer
  (go (while true (println (<! in)))))

(def in-chan (chan))
(def upper-caser-out (upper-caser in-chan))
(def reverser-out (reverser upper-caser-out))
(printer reverser-out)

(>!! in-chan "redrum")

(>!! in-chan "repaid")

Here, there are several defined global variables - which I won't want to do. So, I'll end up perhaps making some functions that perform the single db calls, but instead of returning the value, will return the channel they are happening on, and then I'll string them all up in a single function?

(defn update-last-opened
  (let [id     (<! in) ;; <1>
        sql    "UPDATE articles SET last_opened = ? WHERE article_id = ?"
        params (array (js/Date.now) id)
        out    (chan)]
    (.run db sql params (fn [err]
                          ;; ... <2>

(defn get-article
  (let [id     (<! in)
        sql    "SELECT * FROM articles WHERE article_id = ?"
        params (array id)
        out    (chan)]
    (.run db sql params (fn [err row]
                          ;; ... <3>

(defn article-get
  "Fetches an article, and computes the `:word-data` for it. Sets `last_opened` value before fetching."
  [id cb]
  (let [top-level-chan (chan)]
    (go ; .... <4>

So in the above, I've broken out the functions, but I'm not really sure what to do next. At <1> I'm not entirely sure where I'm going to put the id onto a channel - since it's not an async call - it's already existing from when I kick off the db call from the handler. At <2> and <3>, I guess I need to use the callback to put the value onto the out channel? Then, at <4>, maybe I run each function asynchronously? Not sure but it's getting clearer. In the first example of the existing callback hell you'll see that there is still another db call (words-get-for-article) that is not detailed above...

Anyway, writing this out has actually helped a bit. I will post again if I get this working.

Update: this is confusing. I feel like I am using go blocks everywhere...