(toiminnot)

hwechtla-tl: Better way to thread in clojure

Kierre.png

Mikä on WikiWiki?
nettipäiväkirja
koko wiki (etsi)
viime muutokset


(nettipäiväkirja 31.01.2018) I found a handy way to use the Clojure threading macros (->, ->>, etc). Just to give some background, these macros provide a very nice clojure-flavoured alternative to the ($) operator of haskell, i.e. they allow for writing nested function calls without the nesting. The following are equivalent:

(foo (bar (baz (quux buux) zoom)) boom)

; and

(-> buux quux (baz zoom) bar (foo boom))

i.e. the (->) macro always nests what it has thus far as the first argument of its next subexpression. This has the nice byproduct of having all of the arguments of a function quite near the function name.

The problem is that Clojure is not consistent with its argument ordering. Functions that handle "data" take their "datum" as the first argument; e.g. functions that update maps (assoc, dissoc, select-keys, ...) take the map first, and then any descriptions of the update. OTOH, functions that handle collections (map, filter, reduce, ...) take the collection last. This division does not really work, since some things (such as maps) are sometimes singular data, sometimes iterated over.

Because of this inconsistency, there is (->>), which wraps the result of calculation thus far as the last argument. The problem, of course, is that within the same data processing pipeline, you sometimes want (->) and sometimes (->>). For this reason, there is another macro, as->, which lets you use a placeholder name for where the nested expression goes. For instance, to neatify this expression:

(parse (:body (http/get api-url {:query-params {:token (get-token user)} :as :json})))

; becomes

(as-> user val
  (get-token val)
  {:token val}
  {:query-params val}
  (assoc val :as :json)
  (http/get api-url val)
  (:body val)
  (parse val))

This is very unsatisfactory, since it makes the expressions longer, is basically a form of (let*) without the ability to pick descriptive names, and especially, does not work anyway with some more clever forms of threading macros such as (cond->) or (some->).

Another thing is that (->) is somewhat magical as it comes to the interpretation of subexpressions. Lists are treated as forms to nest expressions in, as shown above. Single symbols are promoted to one-symbol lists, so it doesn't matter whether a function is within parentheses or not:

=> (macroexpand '(-> foo (bar) baz (quux) buux))
(buux (quux (baz (bar foo))))

Anonymous functions, whether introduced by (fn [...] ...) or #(...), just break, they don't work at all. Also, if you already happen to have the function you want, but it happens to be a list (such as (partial / 1) for reciprocal), it doesn't work since it's a list. Maps and sets are treated as functions, which is in sharp contrast to (as->) above which treats them as map/set constructor expressions. And of course, no way that this were properly documented.

But! You can force something to be treated as a function by putting it within yet another list. This works for both functions returned by something and anonymous functions.

=> (-> 8
=>   ((partial / 1)) ; take reciprocal
=>   inc ; add one
=>   (/ 2) ; divide by two
=>   (#(* % %)) ; square
=>   )
0.31640625

This is extremely important, because it also means that you can use (->) all the time, and resort to (#(...)) when the assumption of nesting in the first argument happens to be false. For instance, now this expression:

(parse (:body (http/get api-url {:query-params {:token (get-token user)} :as :json})))

; becomes

(-> user
  get-token
  (#(hash-map :query-params {:token %}))
  (assoc :as :json)
  (#(http/get api-url %))
  :body
  parse)

This has the best sides of both worlds, and also works with (cond->) and (some->).

atehwa: Tuli vastaan vielä yksi tapa tehdä "poikkeuksia" threading-makroon. Koska (->) laittaa argumentin ensimmäiseen kohtaan, sille voi antaa muita threading-makroja:

(-> message
    (get-in [:history :events])
    (->> (map :eventTime)
         (reduce max-time))
    (before? archival-time)
    (if :outdated :ok))


kommentoi (viimeksi muutettu 14.09.2018 10:56)