Passing `opts` with identical defaults into main function and all subsequently called functions

Hi everybody,

I’m trying to do something like this:

; on Clojure 1.11.0-alpha1
(defn example2
  [& {:keys [first
             second
             third
             fourth]
      :or   {first  11
             second 22
             third  33
             fourth 44}
      :as opts}]
  (str "example1: " first second third fourth))

(defn example1
  [& {:keys [first
             second
             third
             fourth]
      :or   {first  11
             second 22
             third  33
             fourth 44}
      :as opts}]
  (str "example1: " first second third fourth
       ", "
       (example2 opts)))

(defn main-function
  [& {:keys [first
             second
             third
             fourth]
      :or   {first  11
             second 22
             third  33
             fourth 44}
      :as opts}]
  (str "main-function: " first second third fourth
       ", "
       (example1 opts)))

(main-function)
; "main-function: 11223344, example1: 11223344, example1: 11223344"

(main-function :first "aa")
; "main-function: aa223344, example1: aa223344, example1: aa223344"

And as you can see, the defaults in or: are always the same. So it makes sense to do something like this:

; define defaults
(def default-opts
  {:first  11
   :second 22
   :third  33
   :fourth 44})

(defn main-function2
  [& {:keys [first
             second
             third
             fourth]
      :or   default-opts
      :as opts}]
  (str "main-function: " first second third fourth))
; Syntax error macroexpanding clojure.core/defn at (REPL:1:1).
; default-opts - failed: map? at: [:fn-tail :arity-1 :params :var-params :var-form :map-destructure :or] spec: :clojure.core.specs.alpha/or
; {:keys [first second third fourth], :or default-opts, :as opts} - failed: simple-symbol? at: [:fn-tail :arity-1 :params :var-params :var-form :local-symbol] spec: :clojure.core.specs.alpha/local-name
; {:keys [first second third fourth], :or default-opts, :as opts} - failed: vector? at: [:fn-tail :arity-1 :params :var-params :var-form :seq-destructure] spec: :clojure.core.specs.alpha/seq-binding-form
; & - failed: vector? at: [:fn-tail :arity-n :bodies :params] spec: :clojure.core.specs.alpha/param-list

From the error I understand that the map keys should be symbols. I’m assuming default opts is a common practice so I’d like to ask experienced people, how are you handling this?

Thank you :slight_smile: .

Ralf

Bit silly but how about

(defn main [opts] ,,,) ;;; with all the destructuring
(defn -main [opts] (main (merge default-opts opts)))

Thank you bsless. The problem is that you have to then remember to pass a lot of default opts when you call those subsequent functions directly outside of the main function since they wouldn’t have any defaults in :or specified…

I tried

(def default-opts
  {`first  11
   `second 22
   `third  33
   `fourth 44})

(defn main-function2
  [& {:keys [first
             second
             third
             fourth]
      :or   default-opts
      :as opts}]
  (str "main-function: " first second third fourth))

which I think should work but no - it doesn’t…

I’m thinking about including those default-opts in a macro…

The keys in default-opts are being turned into namespace qualified symbols because of the syntax quote (`) ie if you are in the user namespace, the keys are the symbols (not keywords) user/first, user/second instead of just :first, :second etc.

Thank you @xfthhxk , based on my understanding 'symbol should be the correct way to get symbols that are not namespace qualified. But that doesn’t work either:

(def default-opts
  {'first  11
   'second 22
   'third  33
   'fourth 44})

(defn main-function3
  [& {:keys [first
             second
             third
             fourth]
      :or   default-opts
      :as opts}]
  (str "main-function: " first second third fourth))
; Syntax error macroexpanding clojure.core/defn at (REPL:1:1).
; default-opts - failed: map? at: [:fn-tail :arity-1 :params :var-params :var-form :map-destructure :or] spec: :clojure.core.specs.alpha/or
; {:keys [first second third fourth], :or default-opts, :as opts} - failed: simple-symbol? at: [:fn-tail :arity-1 :params :var-params :var-form :local-symbol] spec: :clojure.core.specs.alpha/local-name
; {:keys [first second third fourth], :or default-opts, :as opts} - failed: vector? at: [:fn-tail :arity-1 :params :var-params :var-form :seq-destructure] spec: :clojure.core.specs.alpha/seq-binding-form
; & - failed: vector? at: [:fn-tail :arity-n :bodies :params] spec: :clojure.core.specs.alpha/param-list

default-opts - failed: map?

The value of :or has to be a map, you can’t pass in a symbol that resolves to a map.

So what you’re trying to do won’t work, you can’t substitute a map? for a variable pointing to a map?

Hi @ralf!

So having a reference for the :or is not something I thought of doing before. I get the same error as you do. Tried to isolate this a bit more by trying the following.

(def default-opts
  {'first  11
   'second 22
   'third  33
   'fourth 44})

(let [{:syms [first
              second
              third
              fourth]
       :or default-opts} {'first 12}])

Note I used {:syms [,,]} instead of {:keys [,,]} since default-opts has symbols.

It appears that the value for the :or key in the map destructuring must be a map literal based on the spec error. My guess is that the error is probably thrown at macro expansion time, at which time default-opts is a symbol and hence the error.

{:path [:bindings :form :map-destructure :or],
 :pred clojure.core/map?,  ;; <---------------------- this 
 :val default-opts,
 :via
 [:clojure.core.specs.alpha/bindings
  :clojure.core.specs.alpha/bindings
  :clojure.core.specs.alpha/binding
  :clojure.core.specs.alpha/binding-form
  :clojure.core.specs.alpha/binding-form
  :clojure.core.specs.alpha/map-binding-form
  :clojure.core.specs.alpha/map-special-binding
  :clojure.core.specs.alpha/or],
 :in [0 0 :or]}

I think @bsless’s suggestion might be a good way forward. You can call merge in each function where you have the repeated defaults.

Thank you @didibus - that was the first thing I tried - you can look at my first post where I tried a map with keyword keys.

Unfortunately that seems like the only solution now.

Macros should work with macros ;-).

(defmacro default-opts []
  `{first  11
    second 22
    third  33
    fourth 44})

(defn main-function4
  [& {:keys [first
             second
             third
             fourth]
      :or   (default-opts)
      :as   opts}]
  (str "main-function: " first second third fourth))
; Syntax error macroexpanding clojure.core/defn at (REPL:1:1).
; (default-opts) - failed: map? at: [:fn-tail :arity-1 :params :var-params :var-form :map-destructure :or] spec: :clojure.core.specs.alpha/or
; {:keys [first second third fourth], :or (default-opts), :as opts} - failed: simple-symbol? at: [:fn-tail :arity-1 :params :var-params :var-form :local-symbol] spec: :clojure.core.specs.alpha/local-name
; {:keys [first second third fourth], :or (default-opts), :as opts} - failed: vector? at: [:fn-tail :arity-1 :params :var-params :var-form :seq-destructure] spec: :clojure.core.specs.alpha/seq-binding-form
; & - failed: vector? at: [:fn-tail :arity-n :bodies :params] spec: :clojure.core.specs.alpha/param-list

but not in this case :wink:

Macros expand outside in, unlike functions which evaluate inside out, that means that defn runs before your macro, that’s what allows them to take over the evaluation order and rules.

The downside is that they don’t compose unless they are designed together with some form of composition in mind.

If you wanted the behavior you described, what you’d need to do is write a macro that replaces defn or wraps defn itself.

You’re probably better here to just use merge inside a let like someone else said before:

(def defaults
  {:first 1
   :second 2
   :third 3
   :fourth 4})

(defn example1
  [m]
  (let [{:keys [first second third fourth]} (merge defaults m)]
    ...))

(defn example2
  [m]
  (let [{:keys [first second third fourth]} (merge defaults m)]
    ...))

(defn main
  [m]
  (let [{:keys [first second third fourth]} (merge defaults m)]
    ...))

Hmm, maybe one day when I have more experience with macros. Thx!

I can’t explain why, but I don’t like it since I wanted to implemented a lot of functions like this and that seems like a lot of merging…

After a couple of trials I ended up with this idea:

(def defaults
  {:first 1
   :second 2
   :third 3
   :fourth 4
   :fifth 5
   :sixth 6
   :maybe-something-nested {:first 1
                            :second 2
                            :third 3}})

(defn main-function
  [& {:keys [first
             second
             fourth]
      :or   {first  (:first defaults)
             second (-> defaults :maybe-something-nested :second)
             fourth (:fourth defaults)}
      :as   opts}]
      ...
)

In my opinion easy to read and without all that merging it might actually performs better. I still would prefer to just insert all default-opts but this seems good enough.

Thank you @didibus and @xfthhxk