April 3, 2024

Manifold is your friend

I find myself writing async code fairly often. Whenever I'm calling an external service, usually using HTTP, I tend to use the async versions of the libraries that provide it, like Aleph or HttpKit. Actually, I like Aleph the most because it integrates nicely with Manifold. That's not a coincidence, they're both written by the same authors. The usual go-to solution in the Clojure world when it comes to async programming is of course core.async. I've used it several times, and since it's so often used, I try to use it whenever the "async need" arises. However, time and time again I find myself returning to Manifold.

This probably is because Manifold is a bit more Clojure-esque. The API is more idiomatic, they're just functions. Not those weird <! and >>! things core.async provides. Also, I just find it easier to work with. It allows you to use deref where you want, which is a well-known function and also well supported by other libraries. Another big advantage IMHO is that Manifold differntiates between a stream and a "deferred". The latter is a single value that may or may not be "realized" in the future. Usually the functions the lib provides are a mix of both. For example, when fetching a value from a stream using take! it returns another deferred. And very often a stream is not what you want, but a deferred value fits nicely. In core.async they solve this by returning channels (as it's called there) that only hold one value. Of course, you could also do this using Manifold but you don't have to.

Also, Manifold is a bit less magic-y, you can pass deferreds to other functions as arguments etc., where with go blocks you have to be more careful. And, should it be hard to choose, you can connectcore.async channels to Manifold streams!

Just take a look at this example:

;; Core async
(require '[clojure.core.async :as ca])

(def c (ca/chan))
;; Write to channel
(ca/go (ca/>! c "test value"))
;; Read it back, ensure it doesn't block forever
(def v (ca/alts!! [c (ca/timeout 1000)]))
;; Now we still have to inspect the result, see which of the channels it returned from
(if (= c (second v))
  (println "Got value:" (first v))
  (println "Timed out!"))

Whereas for Manifold, it would look like this:

;; Manifold
(require '[manifold.stream :as ms])

(def s (ms/stream))
;; Write to stream
(ms/put! s "test value")  ; Also accepts `nil` btw
;; Read it back, ensure it doesn't block
(def v (deref (ms/take! s) 1000 :timeout))
(if (not= :timeout v)
  (println "Got value:" v)
  (println "Timed out"))

To me, the Manifold code just seems more "natural". Having to use alts!! just seems clunky to me, with its weird return values. But of course, YMMV.

So should you ever take a look at the MonkeyCI code, now you know why we use Manifold and not core.async.

Tags: clojure