Status: Accepted
Clojure destructures a seq as a map by treating it as key/value pairs, so
(defn f [& {:keys [a]}] a) and (let [{:keys [a]} '(:a 1)] a) both see a map.
clojure.core/destructure does this with one runtime test, in front of every
map destructuring:
(if (seq? gmap) (seq-to-map-for-destructuring gmap) gmap)
squint had neither the conversion nor the support fn, so every kwarg bound to nil (issue #975).
Two things make Clojure's rule not portable as written:
seq? is iterator-based (x[Symbol.iterator]), so it is true for a
vector, a js/Map, a Set, a string, and any iterable JS class instance.
Clojure's is an ISeq test, true for none of them. seq? is not a usable
guard: it converts a vector Clojure leaves alone, breaks
(let [{:keys [a]} (js/Map. ...)] a), and slurps an iterable host object into
a map. The last one is not theoretical - a seq?/associative? guard OOMed
the clojure-mode lib test, which destructures CodeMirror objects.Two mechanisms, because squint has two situations where Clojure has one.
A runtime guard on every map destructuring, for values that are seqs:
(if (sequential? gmap)
(if (vector? gmap) gmap (seq-to-map-for-destructuring gmap))
gmap)
sequential? minus vector? is squint's ISeq test. sequential? is
array-or-lazy-or-IVector, so it admits lists, lazy seqs and cons cells while
rejecting js/Map, Set, strings and host objects; vector? then removes the
plain arrays. This covers anything reaching the destructuring at runtime,
including through a local or a parameter the compiler cannot see into.
A compile-time marker on the binding form after &, for rest args.
mark-rest-args tags an associative binding form in rest position, and pmap
then converts unconditionally (nil-guarded, so no args stays nil as in Clojure).
The marker sits on the binding form, not the call site, so it covers every path
that reaches it: direct calls, apply, and vector destructuring.
seq-to-map-for-destructuring is added to core with the cljs.core name and
contract, and squint's representation (a plain object, not a
PersistentArrayMap).
(f 1 :b 2), the trailing map (f 1 {:b 2}), the mixed
(f :a 1 {:b 2}), apply, multi-arity, & {:keys [...]} in vector
destructuring, and (let [{:keys [a]} '(:a 1 :b 2)] a).(defn f [& args] (let [{:keys [a]} args] a)). Clojure gives 1, squint gives
nil. args there is a plain array, indistinguishable from the vector in
(let [{:keys [a]} [:a 1]] a), which must stay nil. The two cannot both be
right while rest args are arrays.seq? alone, as in Clojure: converts vectors, and breaks js/Map
destructuring. Rejected.seq? + associative?: rules out vectors and js/Map, but not an iterable
class instance, which seq? still admits. OOMed the clojure-mode lib test by
realizing a CodeMirror object into a map. Rejected.sequential? alone: also matches plain arrays, so a vector is wrongly
converted. Rejected.apply hands over the caller's array,
so tagging it mutates a value the caller still holds), (concat rest), and an
array-seq view over the array. All of them work and give full parity, but
they put an allocation on every variadic call and cost core's array fast paths
in whatever the body does with the rest arg. Measured against a native rest
array: (fn [& xs] (count xs)) 12.8 -> 40.4 ms per 2M calls, str/join over
the rest 3x, (fn [& xs] 1) 1.1 -> 16.5 ms. count/nth could be given back
via an ICounted/IIndexed ArraySeq, but the class pins LazyIterable into
every bundle and breaks the DCE floor test (1016B against an 800B cap).
Rejected: every variadic fn in every program pays, to fix one destructuring
shape.Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |