Looking at the tests for Alpha 4 for :defaults, it seems that the defaults map ends up with just those all keys from :or that match keys that were not provided in the input map—is that correct?
> Added a new :defaults name directive at top level to bind name to a map of defaults, key→val. Binding symbols in the :or map are transformed to the key value in the :defaults map. :defaults without :or is an error.
Is the intent that the user would explicitly check the /cc @alexmiller @fogus:defaults map to figure out whether a key was provided as input or not?
And it's a change from Alpha 3 that :select maps now include defaults from :or...
(let [{:keys [a b] :or {b 2} :select m :defaults d} {:a 1}] [m d])
;;=> [{:b 2, :a 1} {:b 2}]
Very nice!Ah, no... it seems the :defaults map is everything from :or, not just the unused ones (that wasn't clear from the tests):
user=> (let [{:keys [a b] :or {a 1000 b 2} :select m :defaults d} {:a 1}] [m d])
[{:b 2, :a 1} {:a 1000, :b 2}]huh when is defaults helpful? just to get the :or vals?
Yeah, I've asked in #clojure-dev but it seems like this is intended to avoid having to specify the default k/vs in two places: in the :or and then later in a merge call. This way, you'd have the default k/vs in just one place—the destructuring—and reference it via the :defaults name in subsequent code.
Good instinct!
ah yeah, that makes sense. funny to add it now that we have :select which does the merge for us, but it's good to have
Well, :select only includes the keys mentioned in the destructuring, so :defaults is useful with :as when you want the full map, but with the :or defaults applied.
If you want to be able to merge the defaults with the provided input, you can merge :defaults and :as, which you can send on to downstream functions
The way I'm remembering it:
• :as contains what was passed to destructuring
• :select contains what is bound or required in destructuring
• `:defaults` contains what is in defaults except bound symbols
• :defaults contains what is in :or, but with any bindings resolved to keys they refer to
I don't think that latter bullet is right?
Example to illustrate :defaults:
(let [{:or {a 2 :b 3} :defaults d} {:a 10}] d) ;;=> {:b 3}a is there just for binding.
user=> (let [{:keys [a] :or {a 1} :defaults dfts :as m :select s} {}] [dfts m s])
[{:a 1} {} {:a 1}]
user=> (let [{:keys [a] :or {a 1} :defaults dfts :as m :select s} {:a 2}] [dfts m s])
[{:a 1} {:a 2} {:a 2}]dfts is {:a 1} even when :a is provided (and bound).
user=> (let [{:or {a 1 :b 2 'c 3 "d" 4} :defaults d} {:a 10}] d)
{:b 2, c 3, "d" 4}I thought Rich said that was supposed to be an error (you can't use :or for keys that are not mentioned in :keys or :keys!)...?
Ah yes. Error forthcoming.
Presently, in Clojure 1.12, you can have default values for bindings in :or that don't actually get referenced in destructuring. As you mentioned, that will become an error.
Clojure 1.12.5
user=> (let [{:or {a 1}} nil])
nilNo error yet.
It seems strange that the presence/absence of :keys for a symbol affects :defaults:
user=> (let [{:keys [a] :or {a 1} :defaults dfts :as m :select s} {:a 2}] [dfts m s])
[{:a 1} {:a 2} {:a 2}]
user=> (let [{:or {a 1} :defaults dfts :as m :select s} {:a 2}] [dfts m s])
[{} {:a 2} {}]
But if the latter becomes an error, I'd be happy 🙂Clojure 1.13.0-master-SNAPSHOT
user=> (let [{:or {a 1}} nil])
nilIf there is no binding, then there’s no anchor back to the key.
My example there is extreme, but the situation can come up when you delete a binding but not the default for that binding. A code modification kind of issue. I'd love an error for that too.
Just to make it clear, that code doesn't create a binding for a on 1.12 or 1.13:
user=> (let [{:or {a 1}} nil] a)
Syntax error compiling at (REPL:1:1).
Unable to resolve symbol: a in this contextRight. It's just the default value for the a binding if it would have been created (which it wasn't).
BTW. I updated my 3 bullet:
:defaults contains what is in :or, but with any bindings resolved to keys they refer to
:keys [a] is the same as saying {a :a} … both are ways to create a binding name receiving the value of a look up using the key. If there is no binding telling you how to get the value with a key, then there’s no way to build a default map from :or because you have no way to know what the key is.
Ah, and part of the issue with {:or {a 1} :defaults dfts} is that without a directive present, you don't know what type of key a should represent! If :strs [a] was present, dfts would be {"a" 1}:
user=> (let [{:or {a 1} :defaults dfts} nil] dfts)
{}
user=> (let [{:or {a 1} :strs [a] :defaults dfts} nil] dfts)
{"a" 1}Rounding out more versions:
user=> (let [{:or {a 1} :syms [a] :defaults dfts} nil] dfts)
{a 1}
user=> (let [{:or {a 1} :keys [a] :defaults dfts} nil] dfts)
{:a 1}
user=> (let [{:or {a 1} :foo/keys [a] :defaults dfts} nil] dfts)
#:foo{:a 1}Same :or, but the directive determines what ends up in :defaults. Makes perfect sense now.
Yeah. It's all logical, but it's hard to say in words. 😅
We are still wordsmithing this. 😁
@fogus Feel free to appropriate any of my words above if you find them helpful.
Each of these pieces has a semantic. Those semantics combine. If you learn the semantics you don't need a ton of examples and speculations as to the mechanics. So:
:as refers to the map being destructured. You need it when you are destructuring an argument, else there would be no way to talk about the actual argument.
:or is a map you write to supply default values for keys (aka the defaults), used when those keys are sought but not present in the input map when a) initializing a binding b) constructing a :select result. You may specify values either via explicit keys or binding names. In the latter case the default is for the key associated with that binding name. If you haven't yet used that binding name in a binding elsewhere, it is not associated with any key and the entry is a no-op.
:defaults binds a name to a map, created during destructuring, of keys to their default values. It is based on what you've said in :or per above. You would use this when you want those same default values for some purpose in the body, and don't want to restate them (DRY).
:select binds a name to a map, created during destructuring, containing only those keys from the input map, or the defaults if not present in the input, that are used anywhere in the destructuring form (other than in :or), including in nested destructuring. Thus it is a (deep) subset of the input augmented by defaults.
Applying defaults to :as is one use-case. But I think the more common use-case will be if you want to validate the values and default them if invalid. Likely in the case of a nil value, but possibly also just an out of range or inappropriate value.
The expansion of :or to include arbitrary key/value pairs that can end up in :select and/or :defaults is intriguing. I think I spotted a great use for that new feature in one of our apps last night (but haven't had a chance to dig into it today yet). Will report back.
just note that presence in :or alone will not bring anything into :select
Thanks for that clear explanation, Rich! I could imagine it as part of the official documentation verbatim.
Ah, I misread parts of that. The arbitrary key/value data in :or ends up in :defaults, but only the :keys-related k/vs end up in :select. Thanks.
user=> (let [{:keys [b] :or {:a 1 b 0} :select m :defaults d} {}] [m d])
[{:b 0} {:a 1, :b 0}]
user=> (let [{:or {:a 1} :select m :defaults d} {}] [m d])
[{} {:a 1}](so that "great use" in our code at work just evaporated 🙂 )
@seancorfield You can just add the keys after the ampersand no? Then you'll get the :or defaults for them included.
Yeah, that does work... hmm... will "hammock" this some more...