Stumbling Through Interop
》Surely I can just cargo cult this
After making so much process building a
progressive web app, I decided to take a detour into making some "simpler"
pages, without re-frame
. To pull in some EDN data to work with, I quickly
needed to figure out how to use fetch
. JavaScript interop looks like Java
interop, right? Objects, members, methods. What works in one place should
work in the other. Right?
》Enter promises and consternation
I started playing around just by seeing what fetch
returns:
(js/fetch edn-url ) ; => #object[Promise [object Promise]]
Getting a promise
object in return was not a surprise. Previous work taught
me that JS uses async code and promises heavily. Which makes sense — any
blocking of a page’s JS thread leads to a frozen page. Treating the promise
like a magic bottlecap, I reach for the handy old tools realized?
and deref
:
(realized? (js/fetch edn-url )); => :repl/exception!
(deref (js/fetch edn-url )); => :repl/exception!
Oh, no! This isn’t the same at all.
》Uncultured exploration
Once the naive approach failed, I tried to find CLJS examples that made sense to me. The best was at the end of this post.
;from https://nextjournal.com/fndriven/cljsjsinterop
(defn get-products-from-api []
(-> (js/fetch api-endpoint)
(.then #(.json %))
(.then #(js->clj % :keywordize-keys true))
(.then #(handler %))
(.catch #(error-handler %))))
But example also led to a new problem: I was fetching EDN
data, not JSON
. I’m
sure there’s no .edn
method in JS. My next stop was to read about the
fetch API.
A little digging and I started to understand that fetch
is returning a
Response inside a promise
.
And I can see there is a .text
method on that Response
that returns the
response body text, which I presume can be used with read-string
to turn into
a Clojure data structure.
Here was the next attempt:
(-> (js/fetch edn-url )
(.then #(.text %))
(.then #(println (str %)))
(.catch #(println (str "error: " %)))) ; => object[Promise [object Promise]]
Bah it’s a nested set of promises again. But my data printed out in the
console! It’s not EDN yet, but Success! Sorta. I’m missing something. Why did
this return the promises to the REPL? What’s happening with all these .then
methods? The example doesn’t explain it.
Similar JS examples all use await
but that is not implemented in CLJS. Though
there are a number of libraries which might simplify this, now was the time
to dig in so my understanding could be enhanced. Plus I wanted to attempt to avoid
pulling in extra dependencies for a single fetch
.
Next, I turned to the MDN guide to promises. They describe something similar to the mechanism in re-frame-http-fx where each fetch is described with a callback for successful fetches and another callback for failures.
But the fetch API does not expose this dual-callback interface! What’s going on?
》Cultivating promises
The deal is: fetch
returns a promise
containing the Response
object.
The other deal is: there’s no (simple) way to return the result of an async function in the REPL. The content of the promise is only available in the callbacks.
How can that Response
be used? We’re back to .then
. The then
method lets
you supply the success/failure functions which will have access to the contents
of the promise. Then
also functions like await
in JS, marking a place in
execution that will be handled later, asynchronously.
This is why the REPL is barred from accessing the content — it is working in a different context than those supplied functions. Instead, you have to use the content in context. For example, fire off another async event or stuff the result in an atom:
(def state (atom nil))
(-> (js/fetch edn-url)
(.then #(.text %))
(.then #(cljs.reader/read-string %))
(.then #(reset! state %))
(.catch #(println (str "error: " %))))
The other tricky bit was understanding that .text
is a method that returns
another darn promise. Which means the body text can only be accessed in
yet another .then
.
OK, you ask, why the third .then
in that example? I could have combined that
in one step, such as the example below, but wanted to illustrate an important point
that escaped me initially: .then
always returns a promise. Once you’re in
crazy mirror-universe .then
-world, you have to stay there. Kind of like dealing
with the Fae. So even the result of calling read-string
ends up embedded in
a promise.
;this works
(-> (js/fetch edn-url)
(.then #(.text %))
(.then #(reset! state (cljs.reader/read-string %)))
(.catch #(println (str "error: " %))))
;this feels natural, but fails
(-> (js/fetch edn-url)
(.then #(.text %))
(.then #(cljs.reader/read-string %))
(reset! state %)
(.catch #(println (str "error: " %))))
"You’ve eaten the food and now must stay in Faerie" is the standard JS async/await
paradigm. Only there’s no await
in CLJS. It’s just as simple and natural for
us to thread together those .then
statements instead.
And that .catch
? It’s the handler used if any of the async functions in the
chain fail.
If the page had needed more than a single fetch, then cljs.core.async would have been a better fit.
》Occult reasons
"Why, Nundrum, why are you writing all of this rambling?"
I suppose this is a bit of a rubber-ducking excercise. But another bit of it is to shine some light on where I see gaps in ClojureScript documentation.
The hardest part of getting started with Clojure is … integrating it with
your editor and learning to craft code with the repl. Really. Lein
or neil
can get you going quite quickly, not to mention babashka
! Then you get a
wonderful language that’s shockingly different from Java and you don’t have to
know any Java.
Clojure knowledge can be built up nicely. You can get quite a long way with
slurp
before you have to deal with any filesystem or network interop. But my
CLJS expereince is just not like that. I thought maybe it would help others to
see my struggles, and maybe give The Programmers That Be an idea of the rough
parts. If you have suggestions, please hit me up at the Mastodon address in
the link bar. Or find me as @Nundrum on Clojurians.