Thread-at macro

macros

#1

Composing forms using -> and ->> suddenly stops being fun when you encounter mismatch in “data-parameter” (?) position. So what do you think of thread-at macro?

looks like this:

(defmacro .>
  [x & forms]
  (loop [x x, forms forms]
    (if forms
      (let [[form & other] forms
            threaded  (with-meta
                        (map (fn [f] (if (= '% f) x f)) form)
                        (meta form))]
        (recur threaded other))
      x)))

used like this:

(.> 1
  (+ % 4)
  (- 6 %))

(and expands to (- 6 (+ 1 4)) )

https://repl.it/@NicktaelRawieo/clj-sbox


#2

Isn’t it what the core macro as-> is for?

–edit: fix link


#3

Wow, thank you! That is awesome.

(as-> {:a 1} m
  (assoc m :b 2 :c 3)
  (dissoc m :a))
=> {:b 2, :c 3}

(The link in your comment is broken, @DjebbZ, lacking the > part. Funny ”painting” of Rich Hickey though.)


#4

Thank you very much. That’s very helpful. I didn’t even know such a thing exists


#5

I’ve just fixed it, it seems that Discourse doesn’t like links with angle brackets inside…


#6

Works now anyway. :smile:


#7

Because I finally really fixed it :wink:


#8

Wow, this is the first time I see that embedded repl.it in discourse. It’s super sweet! I want to use it everywhere now :slight_smile:


#9

Also good to know that as-> is made to be nested inside ->. That’s why it takes the symbol as its second arg.

Oh and, what you did is called an anaphoric macro: https://en.m.wikipedia.org/wiki/Anaphoric_macro

And I don’t recommend you use the % symbol for the anaphore. It’ll clash with its use inside short lambda notation #(* % %). Normally it or <> are more common choices as anaphore.

That said, technically, any choice could have a conflict, which is why you generally want to limit the number of anaphoric macros you write, and instead offer a choice of the symbol to the user as an argument.

Another downside is that each anaphoric macro can mean you need to remember what secret symbols it uses, which is why people tend to stick to common ones like it or <>.

For your use case though, I think people do, I’ve seen a few it-> or -<> arrow macros in the wild, and I personally use the latter one. It’s nice for non nested cases, or use inside a ->>.


#10

There is also https://github.com/rplevy/swiss-arrows which has the -<> and -<>> which are like a combo of -> and as-> defaulting to <> as the symbol, examples in the README here https://github.com/rplevy/swiss-arrows#a-generalization-of-the-arrow


#11

-> and ->> nest, you want -> on the outside

(defn decode-basis [basis-str]
  (-> basis-str
      decode-rfc3986-pchar
      (clojure.string/split #",")
      (->> (map reader/read-string))
      (->> (partition 2))
      (->> (map (juxt (comp ->URI first) second)))))

Here is a use case of as-> to inspect the intermediate value as you thread:

(defn apply-defaults [fiddle]
  (as-> fiddle fiddle
        (update fiddle :fiddle/links (partial map auto-link))
        (update fiddle :fiddle/type #(or % ((:fiddle/type fiddle-defaults) fiddle)))
        (cond-> fiddle
                (= :query (:fiddle/type fiddle)) (update :fiddle/query or-str ((:fiddle/query fiddle-defaults) fiddle))
                (= :entity (:fiddle/type fiddle)) (-> (update :fiddle/pull or-str ((:fiddle/pull fiddle-defaults) fiddle))
                                                      (update :fiddle/pull-database or-str ((:fiddle/pull-database fiddle-defaults) fiddle))))
        (update fiddle :fiddle/markdown or-str ((:fiddle/markdown fiddle-defaults) fiddle))
        (update fiddle :fiddle/renderer or-str ((:fiddle/renderer fiddle-defaults) fiddle))))

#12

There is <- to demote ->> into -> in Plumbing:

(defmacro <-
  "Converts a ->> to a ->
   (->> (range 10) (map inc) (<- (doto prn)) (reduce +))
   Jason W01fe is happy to give a talk anywhere any time on
   the calculus of arrow macros"
  [& body]
  `(-> ~(last body) [email protected](butlast body)))

so you can write:

(defn decode-basis [basis-str]
  (->> basis-str
       decode-rfc3986-pchar
       (<- (clojure.string/split #","))
       (map reader/read-string)
       (partition 2)
       (map (juxt (comp ->URI first) second))))

#13

Although I would definitely be inclined to write that as:

(defn decode-basis [basis-str] 
  (-> basis-str 
      (decode-rfc3986-pchar)
      (clojure.string/split #",") 
      (->> (map reader/read-string) 
           (partition 2) 
           (map (juxt (comp ->URI first) second)))))

#14

Generally I think that if I need to mix threading modes in a single run I’m probably doing something wrong. thread-first is for things, thread-last is for collections. Just as in languages that have method chaining, you typically want to keep the “subject” of the same ilk during a statement.

That said, I have often nested a thread-last inside a thread first when it simplifies things, particularly with the clojure.java.jdbc query/insert/update/execute/etc commands, since I “build” the query above that and process the results below it.


#15

I find that interesting (as the maintainer of clojure.java.jdbc) since nearly all of the functions there take the db-spec first, followed by the SQL+params vector, followed by the (optional) options hash map – does that mean you are rarely using the options hash map argument?

I ask because in next.jdbc, I plan to remove most (nearly all) of the options because I’ve come to believe people aren’t using them at the per-statement level (and would most likely only want to use them at the db-spec level.


#16

Yeah, I know you’re the maintainer, which is why I used it as an example.

Quite honestly I had forgotten about the options argument. Looking at it now, I don’t see much that I would use.

I prefer to transform column names in my own code – such that the difference between keywords :foo/bar, :foo-bar and column name foo_bar is in part something I consider to be something that should be in my code.

I’ve also taken to performing row transformation after the results come back, typically via map, but sometimes via core.async channels or whatnot.

Typically we’ll build up a query with say, honeysql in thread-first until it gets to the [querystring, …variables] stage and then thread-last that through jdbc and processing


#17

That’s excellent feedback @mattly thank you!

The current option handling code – and transformation of column names to keywords – is most of the performance overhead in clojure.java.jdbc compared to bare Java code. Using reducible-query with no identifier transformation can get pretty close to you pretty close to Java performance but isn’t anywhere close to the default behavior – something I’m looking at changing in next.jdbc.