Welcome! Please see the About page for a little more info on how this works.

0 votes
in Clojure by

This is related to a number of other asks/jira tickets but I haven't found one for this specifically.

Because :or is destructured into not-found values of the get calls for the destructured keys/symbols/etc, if you bind an object with :as and include defaults in :or, the object won't have the defaults:

(defn example [{:keys [a b]
                :or {a 1 b 2}
                :as opts}]
  (println a b opts))

(example {:a 5})
;=> 5 2 {:a 5}

This has led to subtle bugs in our codebase where we expected the defaults to exist and then they didn't, or we used {:pre [some-pred]} to check an invariant which was violated when we modified the function to use :as in addition to the specific values.

To merge them, you'd want to build the map and then opts (conj defaults-map opts), but that requires iterating over the keys/syms/strs a second time, selecting only the names in the :or map and requires creating another map, or maybe calling assoc repeatedly.

I think it's doable, it would just be a little annoying to write.

I'm curious, is this just posted for informational purposes or is it asking for a change in Clojure?

(changing how :or behaves in destructuring would be a breaking language change -- :as specifically binds the original map value to the symbol)
edited by
I'd love for it to be changed. I find it confusing and it's come up among folks I've taught Clojure to as an awkward and surprising behavior. But I expect most requests to be denied as most of Clojure's existing behavior is fixed. Better to check.

1 Answer

+1 vote
selected by
Best answer

This is the intended behavior from the language. :as is about the value provided to destructure. :keys or other destructuring is about binding values. :or is about providing defaults for missing bound values. If you have "always include" defaults, then I think you want to do that before you destructure via something like:

(defn example [opts]
  (let [defaults {:a 1 :b 2}
        defaulted-opts (merge defaults opts)
        {:keys [a b]} defaulted-opts]
    (println a b defaulted-opts)))

You could collapse some of that, just trying to explicit.

Thanks for the answer. It's implied (by using `init-expr`) in this page (https://clojure.org/reference/special_forms#binding-forms) that `:as` binds earlier than `:or` but it's not explicit and there are no examples showing that they don't work. Would you accept a patch showing that behavior?
I'd rather have an issue about the question than a PR in this case
Will do, thanks.