Skip to content

Commit 8fdb81b

Browse files
authored
Final concepts documentation module (#951)
Should close out #666
1 parent 79e5030 commit 8fdb81b

File tree

2 files changed

+177
-4
lines changed

2 files changed

+177
-4
lines changed

docs/concepts.rst

Lines changed: 174 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -346,9 +346,76 @@ Control Structures
346346
Basilisp features many variations on traditional programming control structures such as ``if`` and ``while`` loops thanks to the magic of :ref:`macros`.
347347
Using these control structure variants in preference to raw :lpy:form:`if` s can often help clarify the meaning of your code while also using reducing the amount of code you have to write.
348348

349-
In addition to the stalwart :lpy:fn:`condp`, :lpy:fn:`and`, and :lpy:fn:`or`, Basilisp also features threading macros which help writing clear and concise code.
349+
Of particular note are the ``when`` variants, which may be useful when you are only checking for a single condition:
350+
351+
.. code-block:: clojure
352+
353+
(when (some neg? coll)
354+
(throw (ex-info "Negative values are not permitted" {:values coll})))
355+
356+
Users may also find the ``let`` variants of ``if`` and ``when`` particularly useful for binding a name for use conditionally:
357+
358+
.. code-block::
359+
360+
;; note that the return value from `re-matches` will not be bound if the return
361+
;; value is `nil` or `false`, so we can safely destructure the return here
362+
(defn parse-num
363+
[s]
364+
(when-let [[_ num] (re-matches #"(\d+)" s)]
365+
(int num)))
366+
367+
Basilisp also features threading macros which help writing clear and concise code.
350368
Threading macros can help transform deeply nested expressions into a much more readable pipeline of expressions whose source order matches the execution order at runtime.
351369

370+
Threading macros come in three basic variants, each of which can be useful in different circumstances:
371+
372+
- ``->`` is called "thread-first"; successive values will be slotted in as the *first* argument in the next expression
373+
- ``->>`` is called "thread-last"; successive values will be slotted in as the *last* argument in the next expression
374+
- ``as->`` is called "thread-as"; allows users to select where in the subsequent expression the previous expression will be slotted
375+
376+
.. code-block::
377+
378+
;; without threading, successive updates or modifications to maps and other
379+
;; persistent data structures would be quite challenging to read
380+
(update (assoc user :most-recent-login (datetime.datetime/now)) :num-logins inc)
381+
382+
;; thread-first macro can help unnest the above logic and make clearer the
383+
;; order of execution
384+
(-> user
385+
(assoc :most-recent-login (datetime.datetime/now))
386+
(update :num-logins inc))
387+
388+
;; likewise, thread-last is frequently useful for seq library functions
389+
(take 3 (sort (map inc (filter non-neg? coll))))
390+
391+
;; note that in threading macros functions with no arguments may elide
392+
;; parentheses -- the macro will ensure they are added
393+
(->> coll
394+
(filter non-neg?)
395+
(map inc)
396+
sort
397+
(take 3))
398+
399+
;; thread-as is particularly useful for heterogeneous operations when the
400+
;; argument of successive invocations is not in a consistent position
401+
(assoc user :historical-names (conj (:historical-names user) name)))
402+
403+
;; this is a bit of a contrived example since it could more easily be
404+
;; accomplished by using `update`, but this pattern frequently pops up
405+
;; dealing with real world data
406+
(as-> (:historical-names user) $
407+
(conj $ name)
408+
(assoc user :historical-names $))
409+
410+
Two variants of thread-first and thread-last are also included:
411+
412+
- ``some`` variants only thread successive values when the previous value is not ``nil``
413+
- ``cond`` variants only thread successive values when some other condition evaluates to logical true
414+
415+
.. note::
416+
417+
"Threading macros" are unrelated to the concept of "threads" used for concurrent execution within a program.
418+
352419
.. seealso::
353420

354421
Control structures: :lpy:fn:`if-not`, :lpy:fn:`if-let`, :lpy:fn:`if-some`, :lpy:fn:`when`, :lpy:fn:`when-let`, :lpy:fn:`when-first`, :lpy:fn:`when-some`, :lpy:fn:`when-not`, :lpy:fn:`cond`, :lpy:fn:`and`, :lpy:fn:`or`, :lpy:fn:`not`, :lpy:fn:`dotimes`, :lpy:fn:`while`, :lpy:fn:`case`, :lpy:fn:`condp`, :lpy:fn:`with`, :lpy:fn:`doto`
@@ -418,6 +485,7 @@ Like the built-in :lpy:fn:`map`, ``pmap`` executes the provided function across
418485
Various Functions
419486
^^^^^^^^^^^^^^^^^
420487

488+
- Functions used for printing: :lpy:fn:`pr`, :lpy:fn:`pr-str`, :lpy:fn:`prn`, :lpy:fn:`prn-str`, :lpy:fn:`print`, :lpy:fn:`print-str`, :lpy:fn:`println`, :lpy:fn:`println-str`, :lpy:fn:`printf`, :lpy:fn:`with-in-str`, :lpy:fn:`with-out-str`, :lpy:fn:`flush`, :lpy:fn:`newline`
421489
- Functions for throwing and introspecting exceptions: :lpy:fn:`ex-info`, :lpy:fn:`ex-cause`, :lpy:fn:`ex-data`, :lpy:fn:`ex-message`, :lpy:ns:`basilisp.stacktrace`
422490
- Functions for generating random data: :lpy:fn:`rand`, :lpy:fn:`rand-int`, :lpy:fn:`rand-nth`, :lpy:fn:`random-uuid`, :lpy:fn:`random-sample`, :lpy:fn:`shuffle`
423491
- Functions which can be used to introspect the Python type hierarchy: :lpy:fn:`class`, :lpy:fn:`cast`, :lpy:fn:`bases`, :lpy:fn:`supers`, :lpy:fn:`subclasses`
@@ -846,11 +914,114 @@ The stored value can be modified using :lpy:fn:`vswap!` and :lpy:fn:`vreset!`.
846914
Transducers
847915
-----------
848916

849-
TBD
917+
Transducers are a tool for structuring pipelines of transformations on sequences of data which have some key advantages over simply composing :ref:`Seq <working_with_seqs>` operations:
918+
919+
1. Transducers are often more efficient than their equivalent composed Seq operations since they do not create intermediate Seqs for each step in the pipeline.
920+
2. Transducers are composable.
921+
3. Transducers are reusable.
922+
923+
Many of the Seq library functions provide an arity for creating a transducer directly which mirrors the functionality of its classical Seq usage.
924+
For example:
925+
926+
.. code-block::
927+
928+
(map :price) ;; returns a transducer which fetches the :price key from a map
929+
(keep identity) ;; returns a transducer which returns only non-nil values
930+
(filter pos?) ;; returns a transducer which filters only positive values
931+
932+
Each step above can be used as a transducer on its own, but one of the key benefits of transducers is composition.
933+
Transducing functions can be combined using the standard :lpy:fn:`comp` function:
934+
935+
.. code-block::
936+
937+
(def xform
938+
(comp
939+
(map :price)
940+
(keep identity)
941+
(filter pos?)))
942+
943+
When combined using ``comp``, these transducers are run not in the classical order of function composition (from outside in) but rather in the order they appear in the source.
944+
The transducer above is equivalent to writing the following in classical Seq library functions:
945+
946+
.. code-block::
947+
948+
(filter pos? (keep identity (map :price coll)))
949+
950+
;; or simplified using the ->> macro
951+
(->> coll
952+
(map :price)
953+
(keep identity)
954+
(filter pos?))
955+
956+
.. _applying_transducers:
957+
958+
Applying Transducers
959+
^^^^^^^^^^^^^^^^^^^^
960+
961+
Once you've created a transducer function, you'll want to use it!
962+
The Basilisp core library provides a number of different tools for applying transducers to sequence or collection.
963+
964+
Imagine we have an input dataset that looks like this with the given transducer:
965+
966+
.. code-block::
967+
968+
(def xform
969+
(comp
970+
(filter #(= (:category %) :hardware))
971+
(filter :quantity)
972+
(map #(assoc % :total (* (:price %) (:quantity %))))))
973+
974+
(def data [{:price 0.17 :name "M6-0.5" :quantity nil :category :hardware}
975+
{:price 8.99 :name "Hammer" :quantity nil :category :tools}
976+
{:price 0.20 :name "M6-0.75" :quantity 10 :category :hardware}
977+
{:price 0.22 :name "M6-1.0" :quantity 5 :category :hardware}
978+
{:price 0.24 :name "M6-1.25" :quantity nil :category :hardware}
979+
{:price 0.27 :name "M6-1.5" :quantity 7 :category :hardware}
980+
{:price 0.29 :name "M6-2.0" :quantity 12 :category :hardware}])
981+
982+
For a straightforward replacement of the :lpy:fn:`reduce` function, you can use :lpy:fn:`transduce`.
983+
``transduce`` will consume the input collection eagerly just as ``reduce`` would.
984+
Using the dataset above, we may be interested in calculating the total value of all of the in-stock items:
985+
986+
.. code-block::
987+
988+
;; note how we combine the existing xform with a new transducing function
989+
;; to extract just the total value of each item out
990+
(transduce (comp xform (map :total)) + data) ;; => 8.469999999999999
991+
992+
Use :lpy:fn:`into` to transform one collection type into another using transducers.
993+
``into`` always utilizes :ref:`transients` whenever possible to efficiently build the output collection type.
994+
Using the previous transducer and functions again, we could collect all of the in-stock item names into a vector:
995+
996+
.. code-block::
997+
998+
(into [] (comp xform (map :name)) data) ;; => ["M6-0.75" "M6-1.0" "M6-1.5" "M6-2.0"]
999+
1000+
For a non-caching lazy sequence, reach for :lpy:fn:`eduction`.
1001+
For cases which you may only ever intend to iterate over a sequence once and do not need its results cached, this may be more efficient.
1002+
1003+
Finally, :lpy:fn:`sequence` creates a lazy sequence of applying the transducer functions to an input sequence.
1004+
Note that although the input sequence is consumed lazily, each step in the transducer is run for every consumed element from the sequence.
1005+
1006+
.. _early_transducer_termination:
1007+
1008+
Early Termination
1009+
^^^^^^^^^^^^^^^^^
1010+
1011+
Transducers (and reducers in general) can be terminated early by wrapping the return value in a call to :lpy:fn:`reduced` (or use the utility function :lpy:fn:`ensure-reduced` if to avoid double wrapping the final value).
1012+
Transducers and :lpy:fn:`reduce` check for reduced values (as by :lpy:fn:`reduced?`) and return the wrapped value if one is encountered.
1013+
1014+
The :lpy:fn:`halt-when` transducer makes use of this pattern.
8501015

8511016
.. seealso::
8521017

853-
:lpy:fn:`eduction`, :lpy:fn:`completing`, :lpy:fn:`halt-when`, :lpy:fn:`sequence`, :lpy:fn:`transduce`, :lpy:fn:`into`, :lpy:fn:`cat`, :lpy:fn:`reduced`, :lpy:fn:`reduced?`, :lpy:fn:`ensure-reduced`, :lpy:fn:`unreduced`
1018+
`Clojure's documentation on Transducers <https://clojure.org/reference/transducers>`_
1019+
1020+
Functions for applying transducers: :lpy:fn:`eduction`, :lpy:fn:`completing`, :lpy:fn:`sequence`, :lpy:fn:`transduce`, :lpy:fn:`into`
1021+
1022+
Functions for terminating transducers early: :lpy:fn:`reduced`, :lpy:fn:`reduced?`, :lpy:fn:`ensure-reduced`, :lpy:fn:`unreduced`
1023+
1024+
Functions which can return transducers: :lpy:fn:`halt-when`, :lpy:fn:`cat`, :lpy:fn:`map`, :lpy:fn:`map-indexed`, :lpy:fn:`mapcat`, :lpy:fn:`filter`, :lpy:fn:`remove`, :lpy:fn:`keep`, :lpy:fn:`keep-indexed`, :lpy:fn:`take`, :lpy:fn:`take-while`, :lpy:fn:`drop`, :lpy:fn:`drop-while`, :lpy:fn:`drop-last`, :lpy:fn:`interpose`, :lpy:fn:`take-nth`, :lpy:fn:`partition-all`, :lpy:fn:`partition-by`, :lpy:fn:`distinct`, :lpy:fn:`dedupe`
8541025

8551026
.. _multimethods:
8561027

src/basilisp/core.lpy

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3130,7 +3130,9 @@
31303130

31313131
(defn partition-by
31323132
"Return a lazy sequence of partitions, splitting ``coll`` each time ``f`` returns a
3133-
different value."
3133+
different value.
3134+
3135+
Returns a stateful transducer if no collection is provided."
31343136
([f]
31353137
(fn [rf]
31363138
(let [prev (volatile! :basilisp.core.partition-by/default)

0 commit comments

Comments
 (0)