Skip to content

Instantly share code, notes, and snippets.

@pesterhazy
Last active October 24, 2021 00:55
Show Gist options
  • Star 14 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save pesterhazy/74dd6dc1246f47eb2b9cd48a1eafe649 to your computer and use it in GitHub Desktop.
Save pesterhazy/74dd6dc1246f47eb2b9cd48a1eafe649 to your computer and use it in GitHub Desktop.
Promise chains in ClojureScript and the problem of previous values
(ns my.promises
"Demo to show different approaches to handling promise chains in ClojureScript
In particular, this file investigates how to pass data between Promise
callbacks in a chain.
See Axel Rauschmayer's post
http://2ality.com/2017/08/promise-callback-data-flow.html for a problem
statement.
The examples is this: based on n, calculate (+ (square n) n), but with each step
calculated asynchronously. The problem for a Promise-based solution is that the
sum step needs access to a previous value, n.
Axel's solution 1 is stateful and not idiomatic in Clojurescript.
Solution 1 (nested scopes) is implemented in test3.
Solution 2 (multiple return values) is implemented in test1 and test2.
For reference, a synchronous implementation is implemented in test0."
(:refer-clojure :exclude [resolve]))
(enable-console-print!)
;; helpers for working with promises in CLJS
(defn every [& args]
(js/Promise.all (into-array args)))
(defn soon
"Simulate an asynchronous result"
([v] (soon v identity))
([v f] (js/Promise. (fn [resolve]
(js/setTimeout #(resolve (f v))
500)))))
(defn resolve [v]
(js/Promise.resolve v))
;; helpers
(defn square [n] (* n n))
;; test0
(defn test0
"Synchronous version - for comparison
The code has three steps:
- get value for n
- get square of n
- get sum of n and n-squared
Note that step 3 requires access to the original value, n, and to the computed
value, n-squared."
[]
(let [n 5
n-squared (square 5)
result (+ n n-squared)]
(prn result)))
;; test1
(defn square-step [n]
(soon (every n (soon n square))))
(defn sum-step [[n squared-n]] ;; Note: CLJS destructuring works with JS arrays
(soon (+ n squared-n)))
(defn test1
"Array approach, flat chain: thread multiple values through promise chain by using Promise.all"
[]
(-> (resolve 5)
(.then square-step)
(.then sum-step)
(.then prn)))
;; test2
(defn to-map-step [array]
(zipmap [:n :n-squared] array))
(defn sum2-step [{:keys [n n-squared] :as m}]
(soon (assoc m :result (+ n n-squared))))
(defn test2
"Accumulative map approach, flat chain: add values to CLJS map in each `then` step, making
it possible for later members of the chain to access previous results"
[]
(-> (resolve 5)
(.then square-step)
(.then to-map-step)
(.then sum2-step)
;; Note: `(.then :result)` doesn't work because `:result` is not
;; recognized as a function. So we need to wrap it in an anon fn.
;; This could be easily fixed by adding a CLJS `then` function that
;; has a more inclusive notion of what a function is.
(.then #(:result %))
(.then prn)))
;; test3
(defn square-step-fn [n]
;; This could be called a "resolver factory" fn. It's a higher-order function
;; that returns a resolve function. `n` is captured in a closure.
(fn [n-squared]
(soon (+ n n-squared))))
(defn square-and-sum-step [n]
(-> (soon (square n))
;; note that square-step-fn is _called_ here, not referenced, in order to
;; provide its inner body with access to the previous result, `n`.
(.then (square-step-fn n))))
(defn test3
"Nested chain approach: instead of a flat list, use a hierarchy, nesting one Promise chain in another.
Uses a closure to capture the intermediate result, `n`, making it available to the nested chain."
[]
(-> (resolve 5)
(.then square-and-sum-step)
(.then prn)))