Skip to content

Commit b03c5c2

Browse files
authored
Support trailing maps in keyword argument destructuring (#833)
Fixes #663 Fixes #834
1 parent cfd3887 commit b03c5c2

File tree

6 files changed

+286
-50
lines changed

6 files changed

+286
-50
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
* Added the `memoize` core fn (#812)
1616
* Added support for `thrown-with-msg?` assertions to `basilisp.test/is` (#831)
1717
* Added support for reading scientific notation literals, octal and hex integer literals, and arbitrary base (2-36) integer literals (#769)
18+
* Added support for passing trailing maps to functions which accept Basilisp keyword arguments (#663)
1819

1920
### Changed
2021
* Optimize calls to Python's `operator` module into their corresponding native operators (#754)
@@ -41,6 +42,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4142
* Fix a bug the variadic arg symbol was not correctly bound to `nil` when no variadic arguments were provided (#801)
4243
* Fix a bug where the quotient of very large numbers was incorrect (#822)
4344
* Fix a bug where `basilisp.test/is` may fail to generate expected/actual info on failures when declared inside a macro (#829)
45+
* Fix a bug where sequential destructuring bindings do not bind names correctly when nested within associative destructuring bindings (#834)
4446

4547
### Removed
4648
* Removed support for PyPy 3.8 (#785)

docs/concepts.rst

Lines changed: 151 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,155 @@ TBD
5757
Destructuring
5858
-------------
5959

60-
TBD
60+
The most common type of name binding encountered in Basilisp code is that of a single symbol to a value.
61+
For example, below the name ``a`` is bound to the result of the expression ``(+ 1 2)``::
62+
63+
(let [a (+ 1 2)]
64+
a)
65+
66+
In many cases this form of name binding is sufficient.
67+
However, when dealing with data nested in vectors or maps of known shapes, it would be much more convenient to bind those values directly without needing to write collection accessor functions by hand.
68+
Basilisp supports a form of name binding known as destructuring, which allows convenient name binding of values from within sequential and associative data structures.
69+
Destructuring is supported everywhere names are bound: :lpy:form:`fn` argument vectors, :lpy:form:`let` bindings, and :lpy:form:`loop` bindings.
70+
71+
.. note::
72+
73+
Names without a corresponding element in the data structure (typically due to absence) will bind to ``nil``.
74+
75+
.. _sequential_destructuring:
76+
77+
Sequential Destructuring
78+
^^^^^^^^^^^^^^^^^^^^^^^^
79+
80+
Sequential destructuring is used to bind values from sequential types.
81+
The binding form for sequential destructuring is a vector.
82+
Names in the vector will be bound to their corresponding indexed element in the sequential expression value, fetched from that type as by :lpy:fn:`nth`.
83+
As a result, any data type supported by ``nth`` natively supports sequential destructuring, including vectors, lists, strings, Python lists, and Python tuples.
84+
It is possible to collect the remaining unbound elements as a ``seq`` by providing a trailing name separated from the individual bindings by an ``&``.
85+
The rest element will be bound as by :lpy:fn:`nthnext`.
86+
It is also possible to bind the full collection to a name by adding a trailing ``:as`` name after all binding forms and optional rest binding.
87+
88+
.. code-block::
89+
90+
(let [[a b c & others :as coll] [:a :b :c :d :e :f]]
91+
[a b c others coll])
92+
;;=> [:a :b :c (:d :e :f) [:a :b :c :d :e :f]]
93+
94+
Sequential destructuring may also be nested:
95+
96+
.. code-block::
97+
98+
(let [[[a b c] & others :as coll] [[:a :b :c] :d :e :f]]
99+
[a b c others coll])
100+
;;=> [:a :b :c (:d :e :f) [[:a :b :c] :d :e :f]]
101+
102+
.. _associative_destructuring:
103+
104+
Associative Destructuring
105+
^^^^^^^^^^^^^^^^^^^^^^^^^
106+
107+
Associative destructuring is used to bind values from associative types.
108+
The binding form for associative destructuring is a map.
109+
Names in the map will be bound to their corresponding key in the associative expression value, fetched from that type as by :lpy:fn:`get`.
110+
Asd a result, any associative types supported by ``get`` natively supports sequential destructuring, including maps, vectors, strings, sets, and Python dicts.
111+
It is possible to bind the full collection to a name by adding an ``:as`` key.
112+
Default values can be provided for keys by providing a map of binding names to default values using the ``:or`` key.
113+
114+
.. code-block::
115+
116+
(defn f [{x :a y :b :as m :or {y 18}}]
117+
[x y m])
118+
119+
(f {:a 1 :b 2}) ;;=> [1 2 {:a 1 :b 2}]
120+
(f {:a 1}) ;;=> [1 18 {:a 1}]
121+
(f {}) ;;=> [nil 18 {}]
122+
123+
For the common case where the names you intend to bind directly match the corresponding keyword name, you can use the ``:keys`` notation.
124+
125+
.. code-block::
126+
127+
(defn f [{:keys [a b] :as m}]
128+
[a b m])
129+
130+
(f {:a 1 :b 2}) ;;=> [1 2 {:a 1 :b 2}]
131+
(f {:a 1}) ;;=> [1 nil {:a 1}]
132+
(f {}) ;;=> [nil nil {}]
133+
134+
There exists a corresponding construct for the symbol and string key cases as well: ``:syms`` and ``:strs``, respectively.
135+
136+
.. code-block::
137+
138+
(defn f [{:strs [a] :syms [b] :as m}]
139+
[a b m])
140+
141+
(f {"a" 1 'b 2}) ;;=> [1 2 {"a" 1 'b 2}]
142+
143+
.. note::
144+
145+
The keys for the ``:strs`` construct must be convertible to valid Basilisp symbols.
146+
147+
It is possible to bind namespaced keys directly using either namespaced individual keys or a namespaced version of ``:keys`` as ``:ns/keys``.
148+
Values will be bound to the symbol by their *name* only (as by :lpy:fn:`name`) -- the namespace is only used for lookup in the associative data structure.
149+
150+
.. code-block::
151+
152+
(let [{a :a b :a/b :c/keys [c d]} {:a "a"
153+
:b "b"
154+
:a/a "aa"
155+
:a/b "bb"
156+
:c/c "cc"
157+
:c/d "dd"}]
158+
[a b c d])
159+
;;=> ["a" "bb" "cc" "dd"]
160+
161+
.. _keyword_arguments:
162+
163+
Keyword Arguments
164+
^^^^^^^^^^^^^^^^^
165+
166+
Basilisp functions can be defined with support for keyword arguments by defining the "rest" argument in an :lpy:fn:`defn` or :lpy:fn:`fn` form with associative destructuring.
167+
Callers can pass interleaved key/value pairs as positional arguments to the function and they will be collected into a single map argument which can be destructured.
168+
If a single trailing map argument is passed by callers (instead of or in addition to other key/value pairs), that value will be joined into the final map.
169+
170+
.. code-block::
171+
172+
(defn f [& {:keys [a b] :as kwargs}]
173+
[a b kwargs])
174+
175+
(f :a 1 :b 2) ;;=> [1 2 {:a 1 :b 2}]
176+
(f :a 1 {:b 2}) ;;=> [1 2 {:a 1 :b 2}]
177+
(f {:a 1 :b 2}) ;;=> [1 2 {:a 1 :b 2}]
178+
179+
.. note::
180+
181+
Basilisp keyword arguments are distinct from Python keyword arguments.
182+
Basilisp functions can be :ref:`defined with Python compatible keyword arguments <basilisp_functions_with_kwargs>` but the style described here is intended primarily for Basilisp functions called only by other Basilisp functions.
183+
184+
.. warning::
185+
186+
The trailing map passed to functions accepting keyword arguments will silently overwrite values passed positionally.
187+
Callers should take care when using the trailing map calling convention.
188+
189+
.. code-block::
190+
191+
(defn f [& {:keys [a b] :as kwargs}]
192+
[a b kwargs])
193+
194+
(f :a 1 {:b 2 :a 3})
195+
;;=> [3 2 {:a 3 :b 2}]
196+
197+
.. _nested_destructuring:
198+
199+
Nested Destructuring
200+
^^^^^^^^^^^^^^^^^^^^
201+
202+
Both associative and sequential destructuring binding forms may be nested within one another.
203+
204+
.. code-block::
205+
206+
(let [[{:keys [a] [e f] :d} [b c]] [{:a 1 :d [4 5]} [:b :c]]]
207+
[a b c e f])
208+
;;=> [1 :b :c 4 5]
61209
62210
.. _references_and_refs:
63211

@@ -71,6 +219,8 @@ TBD
71219
Transducers
72220
-----------
73221

222+
TBD
223+
74224
.. _hierarchies:
75225

76226
Hierarchies

docs/pyinterop.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ For properties which are explicitly *not* read only, you can mutate their value
147147
In most cases, Basilisp's method and property access features should be sufficient.
148148
However, in case it is not, Python's :ref:`builtins <python_builtins>` such as `getattr` and `setattr` are still available and can supplement Basilisp's interoperability features.
149149

150-
.. _keyword_arguments:
150+
.. _py_interop_keyword_arguments:
151151

152152
Keyword Arguments
153153
-----------------
@@ -185,7 +185,7 @@ For functions which do support keyword arguments, two strategies are supported f
185185

186186
.. note::
187187

188-
Basilisp functions support a variant of keyword arguments via destructuring support provided by ``fn`` and ``defn``.
188+
Basilisp functions support a variant of :ref:`keyword_arguments` via destructuring support provided by ``fn`` and ``defn``.
189189
The ``:apply`` strategy relies on that style of keyword argument support to idiomatically integrate with Basilisp functions.
190190

191191
.. code-block:: clojure
@@ -196,7 +196,7 @@ For functions which do support keyword arguments, two strategies are supported f
196196
197197
The ``:apply`` strategy is appropriate in situations where there are few or no positional arguments defined on your function.
198198
With this strategy, the compiler converts the Python dict of string keys and values into a sequential stream of de-munged keyword and value pairs which are applied to the function.
199-
As you can see in the example above, this strategy fits neatly with the existing support for destructuring key and value pairs from rest arguments in a function definition.
199+
As you can see in the example above, this strategy fits neatly with the existing support for :ref:`destructuring` key and value pairs from rest arguments in a function definition.
200200

201201
.. warning::
202202

@@ -210,7 +210,7 @@ As you can see in the example above, this strategy fits neatly with the existing
210210
211211
The ``:collect`` strategy is a better accompaniment to functions with positional arguments.
212212
With this strategy, Python keyword arguments are converted into a Basilisp map with de-munged keyword arguments and passed as the final positional argument of the function.
213-
You can use map destructuring on this final positional argument, just as you would with the map in the ``:apply`` case above.
213+
You can use :ref:`associative_destructuring` on this final positional argument, just as you would with the map in the ``:apply`` case above.
214214

215215
Type Hinting
216216
------------

docs/reader.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ String literals are always read with the UTF-8 encoding.
150150
String literals may contain the following escape sequences: ``\\``, ``\a``, ``\b``, ``\f``, ``\n``, ``\r``, ``\t``, ``\v``.
151151
Their meanings match the equivalent escape sequences supported in `Python string literals <https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals>`_\.
152152

153-
.. _byte_strings
153+
.. _byte_strings:
154154

155155
Byte Strings
156156
------------

0 commit comments

Comments
 (0)