2
votes

Does Clojure/Script offer a way to build a destructured map out of the arguments plus filled-in defaults in case the keys weren't supplied in the call?

Consider this example (that doesn't quite do what the code implies by a quick glance). Does clojure provide a way to build the map prompt with these four keys and values either from the caller or the defaults. I hate to think I have to repeat these key names two more times to get what I am after.

(re-frame/reg-event-db
 :open-prompt
 (fn [db [_ {title :title
             text :text
             label-yes :label-yes
             label-no :label-no
             :or {title "Confirm"
                  text "Are you sure?"
                  label-yes "Ok"
                  label-no "Cancel"}
             :as prompt}]]
   (-> db
       (update :state conj :modal-prompt)
       (assoc :prompt prompt))))
3

3 Answers

2
votes

After reviewing the official documentation page about destructuring, I don't think that Clojure proposes a more convient way of doing that.

But just by curiosity, I was wondering what is the code generated by destructuring, because I'm expecting it relies on macro stuff. Let consider this toy example:

(def my-map {:text "Some text"})
(let
  [{title :title
    :or {title "Confirm"}
    :as prompt} my-map]
  (str "I got " title " from " prompt))
;; => "I got Confirm from {:text \"Some text\"}"

(macroexpand '(let
                  [{title :title
                    :or {title "Confirm"}
                    :as prompt} my-map]
                (str "I got " title " from " prompt)))
;; => (let*
;;     [map__12555
;;      my-map
;;      map__12555
;;      (if
;;          (clojure.core/seq? map__12555)
;;        (clojure.lang.PersistentHashMap/create
;;         (clojure.core/seq map__12555))
;;        map__12555)
;;      prompt
;;      map__12555
;;      title
;;      (clojure.core/get map__12555 :title "Confirm")]
;;     (str "I got " title " from " prompt))

So as you can see, after a macro expansion, the :or mechanism which allows to specifies default value relies on clojure.core/get.

In this particular example, title is affected by (clojure.core/get map__12555 :title "Confirm") form. It's a way to avoid repeating the title variable, but does it worth it?

You can also check the source code of the destructuring macro to get full details about it, but personally I found it pretty difficult to handle ^^'.

2
votes

it is doable, maybe not very practical though, but nice for self education:

let's begin with making up the function what would be special binding case.

let's say, we want to pass vectors of length 2 or 3, where vector of 2 will represent the simple binding map key-value pair like [:as abc] or [a :a], and the vector of size 3 would be k-v-default triple: [a :a "my default"]. The example of it's usage:

(bindings-preproc [['a 1 "asd"]
                   ['b 2 "ddd"]
                   [:or {'x 10}]
                   [:as 'whole]])

resulting to

{a 1, b 2, :or {x 10, a "asd", b "ddd"}, :as whole}

this function could look like this:

(defn bindings-preproc [decls]
  (let [defaults (into {} (keep (fn [decl]
                                  (when (and (not (keyword? (first decl)))
                                             (= 3 (count decl)))
                                    (let [[nm _ default] decl]
                                      [nm default])))
                                decls))
        all-kvs (apply assoc {} (mapcat (partial take 2) decls))]
    (update all-kvs :or merge defaults)))

(this one doesn't include error checks for the sake of illustrative simplicity)

The next thing is to employ it inside the binding macros. The idea to make bindings-preproc a macro fails, because binding forms are checked for validity before the inner macros are evaluated.

But still we have a feature, that would help, namely reader tags. They are used for example when you use #inst syntax. Since these reader tags are processed at read-time, before any macros are getting expanded, we can plug our preprocessor in.

(here i will use actual reference update, to demonstrate it from repl, but in real projects you would declare these tags in a special file)

user> (alter-var-root
       #'default-data-readers
       assoc 'my/reader #'user/bindings-preproc)

;;=> {uuid #'clojure.uuid/default-uuid-reader,
;;    inst #'clojure.instant/read-instant-date,
;;    my/reader #'user/bindings-preproc}

so, now we can try to make it work:

(defn f [#my/reader [[a :a 10]
                     [b :b 20]
                     [z :z]
                     [:keys [k1 k2 k3]]
                     [[c1 c2 & cs] :c]
                     [:or {z 101
                           k3 :wooo}]
                     [:as whole]]]
  {:a a :b b :c1 c1 :c2 c2 :cs cs :z z :k1 k1 :k2 k2 :k3 k3 :whole whole})

user> (f {:a 1000 :c [:one]})
;;=> {:cs nil,
;;    :c2 nil,
;;    :z 101,
;;    :c1 :one,
;;    :k3 :wooo,
;;    :b 20,
;;    :whole {:a 1000, :c [:one]},
;;    :k1 nil,
;;    :k2 nil,
;;    :a 1000}


user> (let [a 10
            b 20
            #my/reader [[x :x 1]
                        [y :y 2]
                        [z :z 100]] {:z 432}]
        [a b x y z])
;;=> [10 20 1 2 432]
1
votes

I like to make a map of all default values, then use into or similar to fuse the user-supplied values into the map of default values. For example:

(ns tst.demo.core
  (:use tupelo.core tupelo.test) )

(def stuff-default {:a 1 :b 2})

(defn apply-defaults
  [arg]
  (let [stuff (glue stuff-default arg)] ; or use `into`.  Last one wins, so put defaults first
    (with-map-vals stuff [a b]
      (newline)
      (spyx a)
      (spyx b))
    stuff))

(dotest
  (is= (apply-defaults {}) ; no inputs => all default values
    {:a 1, :b 2})
  (is= (apply-defaults {:a 100}) ; some inputs => partial defaults
    {:a 100, :b 2})
  (is= (apply-defaults {:a 100, :b 200}) ; all inputs => no defaults used
    {:a 100, :b 200}))

Here glue is like into but with more error checking. We also use tupelo.core/with-map-vals to destruct the map, with less repetition than native Clojure destructuring (vals->map does the reverse).

The output is:

-------------------------------
   Clojure 1.10.1    Java 14
-------------------------------

a => 1
b => 2

a => 100
b => 2

a => 100
b => 200

Ran 2 tests containing 3 assertions.
0 failures, 0 errors.