Skip to content

Commit fe3f027

Browse files
authored
More concepts documentation (#926)
Continuing to whittle away at #666
1 parent fce0654 commit fe3f027

File tree

3 files changed

+189
-40
lines changed

3 files changed

+189
-40
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2222
* Fix a bug where reader column offset numbering began at 1, rather than 0 (#905)
2323

2424
### Other
25-
* Add REPL documentation module (#205, #925)
25+
* Add several sections to Concepts documentation module (#666)
26+
* Add REPL documentation module (#250)
2627
* Add documentation module for Basilisp interfaces (#920)
2728
* Add GitHub source links to generated API documentation (#921)
2829
* Update Concepts documentation module with See Also links for most sections (#925)

docs/concepts.rst

Lines changed: 154 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -222,11 +222,39 @@ Macros created with ``defmacro`` automatically have access to two additional par
222222
Metadata
223223
--------
224224

225-
TBD
225+
Basilisp symbols and collection types support optional metadata.
226+
As the name implies, metadata describes the data contained in a collection or the symbol.
227+
Users will most frequently encounter metadata used either as a hint for the compiler or as an artifact added to a symbol after compilation.
228+
However, metadata is reified at runtime and available for use for purposes other than compiler hints.
229+
230+
.. note::
231+
232+
Metadata is not considered when comparing two objects for equality or when generating their hash codes.
233+
234+
.. note::
235+
236+
Despite the fact that metadata is not considered for object equality, object metadata is nevertheless immutably linked to the object.
237+
Changing the metadata of an object as by :lpy:fn:`with-meta` or :lpy:fn:`vary-meta` will result in a different object.
238+
239+
.. code-block::
240+
241+
(def m ^:kw ^python/str ^{:map :yes} {:data []})
242+
243+
;; will emit compiler metadata since we're inspecting the metadata of the Var
244+
(meta #'m) ;; => {:end-col 48 :ns basilisp.user :end-line 1 :col 0 :file "<REPL Input>" :line 1 :name m}
245+
246+
;; will emit the metadata we created when we def'ed m
247+
(meta m) ;; => {:kw true :tag <class 'str'> :map :yes}
248+
249+
;; with-meta replaces the metadata on a copy
250+
(meta (with-meta m {:kw false})) ;; => {:kw false}
251+
252+
;; source object metadata remains unchanged
253+
(meta m) ;; => {:kw true :tag <class 'str'> :map :yes}
226254
227255
.. seealso::
228256

229-
:ref:`Reading metadata on literals <reader_metadata>`, :lpy:fn:`meta`, :lpy:fn:`with-meta`, :lpy:fn:`vary-meta`, :lpy:fn:`alter-meta!`, :lpy:fn:`reset-meta!`
257+
:ref:`Reading metadata on literals <reader_metadata>`, :lpy:fn:`meta`, :lpy:fn:`with-meta`, :lpy:fn:`vary-meta`
230258

231259
.. _delays:
232260

@@ -240,18 +268,11 @@ Once a delay has been evaluated, it caches its results and returns the cached re
240268

241269
.. code-block::
242270
243-
basilisp.user=> (def d (delay (println "evaluating") (+ 1 2 3)))
244-
#'basilisp.user/d
245-
246-
basilisp.user=> d
247-
<basilisp.lang.delay.Delay object at 0x1077803a0>
248-
249-
basilisp.user=> (force d)
250-
evaluating
251-
6
252-
253-
basilisp.user=> (force d)
254-
6
271+
(def d (delay (println "evaluating") (+ 1 2 3)))
272+
(force d) ;; prints "evaluating"
273+
;; => 6
274+
(force d) ;; does not print
275+
;; => 6
255276
256277
.. seealso::
257278

@@ -266,6 +287,16 @@ Promises are containers for receiving a deferred result, typically from another
266287
The value of a promise can be written exactly once using :lpy:fn:`deliver`.
267288
Threads may await the results of the promise using a blocking :lpy:fn:`deref` call.
268289

290+
.. code-block::
291+
292+
(def p (promise))
293+
(realized? p) ;; => false
294+
@(future (deliver p (+ 1 2 3)))
295+
(realized? p) ;; => true
296+
@p ;; => 6
297+
(deliver p 7) ;; => nil
298+
@p ;; => 6
299+
269300
.. seealso::
270301

271302
:lpy:fn:`promise`, :lpy:fn:`deliver`, :lpy:fn:`realized?`, :lpy:fn:`deref`
@@ -275,29 +306,119 @@ Threads may await the results of the promise using a blocking :lpy:fn:`deref` ca
275306
Atoms
276307
-----
277308

278-
TBD
309+
Atoms are mutable, thread-safe reference containers which are useful for storing state that may need to be accessed (and changed) by multiple threads.
310+
New atoms can be created with a default value using :lpy:fn:`atom`.
311+
The state can be mutated in a thread-safe way using :lpy:fn:`swap!` and :lpy:fn:`reset!` (among others) without needing to coordinate with other threads.
312+
Read the value of the atom using :lpy:fn:`deref`.
313+
314+
.. code-block::
315+
316+
(def a (atom 0))
317+
(swap! a inc) ;; => 1
318+
@a ;; => 1
319+
(swap! a #(+ 3 %)) ;; => 4
320+
@a ;; => 4
321+
(reset! a 0) ;; => 0
322+
@a ;; => 0
323+
324+
Atoms are designed to contain one of Basilisp's immutable :ref:`data_structures`.
325+
The ``swap!`` function in particular uses the :lpy:fn:`compare-and-set!` function to atomically swap in the results of applying the provided function to the existing value.
326+
``swap!`` attempts to compare and set the value in a loop until it succeeds.
327+
Since atoms may be accessed by multiple threads simultaneously, it is possible that the value of an atom has changed between when the state was polled and when the function finished computing its final result.
328+
Update functions should therefore be free of side-effects since they may be called multiple times.
329+
330+
.. note::
331+
332+
Atoms implement :py:class:`basilisp.lang.interfaces.IRef` and :py:class:`basilisp.lang.interfaces.IReference` and therefore support validators, watchers, and mutable metadata.
279333

280334
.. seealso::
281335

282-
:lpy:fn:`atom`, :lpy:fn:`compare-and-set!`, :lpy:fn:`reset!`, :lpy:fn:`reset-vals!`, :lpy:fn:`swap!`, :lpy:fn:`swap-vals!`, :lpy:fn:`deref`, :ref:`references_and_refs`
336+
:lpy:fn:`atom`, :lpy:fn:`compare-and-set!`, :lpy:fn:`reset!`, :lpy:fn:`reset-vals!`, :lpy:fn:`swap!`, :lpy:fn:`swap-vals!`, :lpy:fn:`deref`, :ref:`reference_types`
337+
338+
.. _reference_types:
283339

284-
.. _references_and_refs:
340+
Reference Types
341+
---------------
285342

286-
References and Refs
287-
-------------------
343+
Basilisp's built-in reference types :ref:`vars` and :ref:`atoms` include support for metadata, validation, and watchers.
288344

289-
TBD
345+
Unlike :ref:`metadata` on data structures, reference type metadata is mutable.
346+
The identity of a reference type is the container, rather than the contained value, so it makes sense that if the value of a container can change so can the metadata.
347+
:ref:`Var metadata <var_metadata>` is typically set at compile-time by a combination of compiler provided metadata and user metadata (typically via :lpy:form:`def`).
348+
On the other hand, :ref:`atom <atoms>` have no metadata by default.
349+
Metadata can be mutated using :lpy:fn:`alter-meta!` and :lpy:fn:`reset-meta!`.
350+
351+
Both Vars and atoms support validation of their contained value at the time it is set using a validator function.
352+
Validator functions are functions of one argument returning either a single boolean value (where ``false`` indicates the value is invalid) or throwing an exception upon failure.
353+
The validator will be called with the new proposed value of a ref before that value is applied.
354+
355+
.. code-block::
356+
357+
(def a (atom 0))
358+
(set-validator! a (fn [v] (= 0 (mod v 2))))
359+
(swap! a inc) ;; => throws basilisp.lang.exception.ExceptionInfo: Invalid reference state {:data 1 :validator <...>}
360+
(swap! a #(+ 2 %)) ;; => 2
361+
362+
Vars and atoms also feature support for watch functions which will be called on changes to the contained value.
363+
Watch functions are functions of 4 arguments (watch key, reference value, old value, and new value).
364+
Unlike validators, watches may not veto proposed changes to the contained value and any return value will be ignored.
365+
A watch can be added to a reference using :lpy:fn:`add-watch` using a key and watches may be removed using :lpy:fn:`remove-watch` using the same key.
366+
367+
.. code-block::
368+
369+
(def a (atom 0))
370+
(add-watch a :print (fn [_ r old new] (println r "changed from" old "to" new)))
371+
(swap! a inc) ;; => prints "<basilisp.lang.atom.Atom object at 0x113b01070> changed from 0 to 1"
372+
;; => 1
373+
374+
.. note::
375+
376+
Watch functions are called synchronously after a value change in an nondeterministic order.
377+
378+
.. warning::
379+
380+
By the time a watch function is called, it is possible that the contained value has changed again, so users should use the provided arguments for the new and old value rather than attempting to :lpy:fn:`deref` the ref.
290381

291382
.. seealso::
292383

293-
:lpy:fn:`alter-meta!`, :lpy:fn:`reset-meta!`, :lpy:fn:`add-watch`, :lpy:fn:`remove-watch`, :lpy:fn:`get-validator`, :lpy:fn:`set-validator!`
384+
:ref:`atoms`, :ref:`vars`, :lpy:fn:`alter-meta!`, :lpy:fn:`reset-meta!`, :lpy:fn:`add-watch`, :lpy:fn:`remove-watch`, :lpy:fn:`get-validator`, :lpy:fn:`set-validator!`
294385

295386
.. _transients:
296387

297388
Transients
298389
----------
299390

300-
TBD
391+
Basilisp supports creating transient versions of most of its :ref:`persistent collections <data_structures>` using the :lpy:fn:`transient` function.
392+
Transient versions of persistent data structures use local mutability to improve throughput for common data manipulation operations.
393+
Because transients are mutable, they are intended to be used in local, single-threaded contexts where you may be constructing or modifying a collection.
394+
395+
Despite their mutability, the APIs for mutating transient collections are intentionally quite similar to that of standard persistent data structures.
396+
Unlike classical data structure mutation APIs, you may not simply hang on to a single reference and issue repeated function calls or methods to that same data structure.
397+
Instead, you use the transient-compatible variants of the existing persistent data structure functions (those ending with a ``!``) such as :lpy:fn:`assoc!`, :lpy:fn:`conj!`, etc.
398+
As with the persistent data structures, you must use the return value from each of these functions as the input to subsequent operations.
399+
400+
Once you have completed modifying a transient, you should call :lpy:fn:`persistent!` to freeze the data structure back into its persistent variant.
401+
After freezing a transient back into a persistent data structure, references to the transient are no longer guaranteed to be valid and may throw exceptions.
402+
403+
Many :lpy:ns:`basilisp.core` functions already use transients under the hood by default.
404+
Take for example this definition of a function to merge an arbitrary number of maps (much like :lpy:fn:`merge`).
405+
406+
.. code-block::
407+
408+
(defn merge [& maps]
409+
(when (some identity maps)
410+
(persistent!
411+
(reduce #(conj! %1 %2) (transient {}) maps))))
412+
413+
.. note::
414+
415+
You can create transient versions of maps, sets, and vectors.
416+
Lists may not be made transient, since there would be no benefit.
417+
418+
.. warning::
419+
420+
Transient data structures are not thread-safe and must therefore not be modified by multiple threads at once.
421+
It is the user's responsibility to ensure synchronization mutations to transients across threads.
301422

302423
.. seealso::
303424

@@ -308,7 +429,13 @@ TBD
308429
Volatiles
309430
---------
310431

311-
TBD
432+
Volatiles are mutable, *non-thread-safe* reference containers which are useful for storing state that is mutable and is only changed in a single thread.
433+
Create a new volatile using :lpy:fn:`volatile!`.
434+
The stored value can be modified using :lpy:fn:`vswap!` and :lpy:fn:`vreset!`.
435+
436+
.. note::
437+
438+
Volatiles are most frequently used for creating performant stateful :ref:`transducers`.
312439

313440
.. seealso::
314441

@@ -358,24 +485,13 @@ TBD
358485

359486
:lpy:fn:`defprotocol`, :lpy:fn:`protocol?`, :lpy:fn:`extend`, :lpy:fn:`extend-protocol`, :lpy:fn:`extend-type`, :lpy:fn:`extenders`, :lpy:fn:`extends?`, :lpy:fn:`satisfies?`
360487

361-
.. _data_types:
362-
363-
Data Types
364-
----------
365-
366-
TBD
367-
368-
.. seealso::
369-
370-
:lpy:fn:`deftype`
371-
372-
.. _records:
488+
.. _data_types_and_records:
373489

374-
Records
375-
-------
490+
Data Types and Records
491+
----------------------
376492

377493
TBD
378494

379495
.. seealso::
380496

381-
:ref:`records` , :lpy:fn:`defrecord` , :lpy:fn:`record?`
497+
:lpy:fn:`deftype`, :lpy:fn:`defrecord` , :lpy:fn:`record?`

docs/runtime.rst

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ See the documentation for :lpy:fn:`require` for more details.
106106
Vars
107107
----
108108

109-
Vars are mutable :ref:`reference types <references_and_refs>` which hold a reference to something.
109+
Vars are mutable :ref:`reference types <reference_types>` which hold a reference to something.
110110
Users typically interact with Vars with the :lpy:form:`def` form and the :lpy:fn:`basilisp.core/defn` macro which create Vars to hold he result of the expression or function.
111111
All values created with these forms are stored in Vars and interned in a Namespace so they can be looked up later.
112112
The Basilisp compiler uses Vars interned in Namespaces during name resolution to determine if a name is referring to a local name (perhaps in a :lpy:form:`let` binding or as a function argument) or if it refers to a Var.
@@ -118,6 +118,38 @@ Specific metadata keys given during the creation of a Var can enable specific fe
118118

119119
:lpy:fn:`alter-var-root`, :lpy:fn:`find-var`, :lpy:fn:`thread-bound?`, :lpy:fn:`var-get`, :lpy:fn:`var-set`, :lpy:fn:`with-redefs`, :lpy:fn:`with-redefs-fn`
120120

121+
.. _var_metadata:
122+
123+
Metadata
124+
^^^^^^^^
125+
126+
Whenever a Var is defined as by :lpy:form:`def`, the compiler typically adds some metadata about where the Var was defined.
127+
The following is a non-exhaustive list of potential metadata keys that may be set by the compiler.
128+
129+
All Vars might get the following keys:
130+
131+
- ``:ns`` the :ref:`namespace <namespaces>` the Var is interned in
132+
- ``:name`` the name of the Var as a symbol
133+
- ``:file`` the name of the source file where the Var was defined, or if it was defined in a REPL or via a string (such as by :lpy:fn:`eval`) then a descriptive string surrounded by ``<...>``
134+
- ``:line``, ``:col``, ``:end-line``, ``:end-col`` location metadata about where in ``:file`` the Var was defined
135+
- ``:doc`` the docstring provided at the time the Var was interned, if one
136+
- ``:tag`` typically a return value for functions or type hint for values
137+
138+
Users may provide the following metadata which the compiler will pass through:
139+
140+
- ``:redef`` (see :ref:`compiler` for more details)
141+
- ``:private`` (see :ref:`private_vars` below)
142+
- ``:dynamic`` (see :ref:`dynamic_vars` below)
143+
144+
Vars containing functions (typically defined via some variant of :lpy:fn:`defn` or :lpy:fn:`defmacro`) might get the following keys:
145+
146+
- ``:macro`` if the function is a macro and eligible to be called during macroexpansion
147+
- ``:arglists`` a sequence of the argument vectors for each defined arity of the function
148+
149+
.. seealso::
150+
151+
:ref:`reference_types`
152+
121153
.. _dynamic_vars:
122154

123155
Dynamic Vars

0 commit comments

Comments
 (0)