diff --git a/docs/extra/anaphoric.rst b/docs/extra/anaphoric.rst index 2e335da..b763a8b 100644 --- a/docs/extra/anaphoric.rst +++ b/docs/extra/anaphoric.rst @@ -17,15 +17,27 @@ To use these macros you need to require the ``hy.extra.anaphoric`` module like s ``(require [hy.extra.anaphoric [*]])`` +These macros are implemented by replacing any use of the designated +anaphoric symbols (``it``, in most cases) with a gensym. Consequently, +it's unwise to nest these macros, or to use an affected symbol as +something other than a variable name, as in ``(print "My favorite +Stephen King book is" 'it)``. + .. _ap-if: ap-if ===== -Usage: ``(ap-if (foo) (print it))`` +Usage: ``(ap-if test-form then-form else-form)`` -Evaluates the first form for truthiness, and bind it to ``it`` in both the -true and false branches. +As :ref:`if `, but the result of the test form is named ``it`` in +the subsequent forms. As with ``if``, the else-clause is optional. + +.. code-block:: hy + + => (import os) + => (ap-if (.get os.environ "PYTHONPATH") + ... (print "Your PYTHONPATH is" it)) .. _ap-each: @@ -33,9 +45,17 @@ true and false branches. ap-each ======= -Usage: ``(ap-each [1 2 3 4 5] (print it))`` +Usage: ``(ap-each xs body…)`` -Evaluate the form for each element in the list for side-effects. +Evaluate the body forms for each element ``it`` of ``xs`` and return +``None``. + +.. code-block:: hy + + => (ap-each [1 2 3] (print it)) + 1 + 2 + 3 .. _ap-each-while: @@ -43,10 +63,10 @@ Evaluate the form for each element in the list for side-effects. ap-each-while ============= -Usage: ``(ap-each-while list pred body)`` +Usage: ``(ap-each-while xs pred body…)`` -Evaluate the form for each element where the predicate form returns -``True``. +As ``ap-each``, but the form ``pred`` is run before the body forms on +each iteration, and the loop ends if ``pred`` is false. .. code-block:: hy @@ -60,11 +80,10 @@ Evaluate the form for each element where the predicate form returns ap-map ====== -Usage: ``(ap-map form list)`` +Usage: ``(ap-map form xs)`` -The anaphoric form of map works just like regular map except that -instead of a function object it takes a Hy form. The special name -``it`` is bound to the current object from the list in the iteration. +Create a generator like :py:func:`map` that yields each result of ``form`` +evaluated with ``it`` bound to successive elements of ``xs``. .. code-block:: hy @@ -77,10 +96,12 @@ instead of a function object it takes a Hy form. The special name ap-map-when =========== -Usage: ``(ap-map-when predfn rep list)`` +Usage: ``(ap-map-when predfn rep xs)`` -Evaluate a mapping over the list using a predicate function to -determin when to apply the form. +As ``ap-map``, but the predicate function ``predfn`` (yes, that's a +function, not an anaphoric form) is applied to each ``it``, and the +anaphoric mapping form ``rep`` is only applied if the predicate is true. +Otherwise, ``it`` is yielded unchanged. .. code-block:: hy @@ -96,11 +117,9 @@ determin when to apply the form. ap-filter ========= -Usage: ``(ap-filter form list)`` +Usage: ``(ap-filter form xs)`` -As with ``ap-map`` we take a special form instead of a function to -filter the elements of the list. The special name ``it`` is bound to -the current element in the iteration. +The :py:func:`filter` equivalent of ``ap-map``. .. code-block:: hy @@ -113,11 +132,9 @@ the current element in the iteration. ap-reject ========= -Usage: ``(ap-reject form list)`` +Usage: ``(ap-reject form xs)`` -This function does the opposite of ``ap-filter``, it rejects the -elements passing the predicate . The special name ``it`` is bound to -the current element in the iteration. +Equivalent to ``(ap-filter (not form) xs)``. .. code-block:: hy @@ -130,10 +147,9 @@ the current element in the iteration. ap-dotimes ========== -Usage ``(ap-dotimes n body)`` +Usage: ``(ap-dotimes n body…)`` -This function evaluates the body *n* times, with the special -variable ``it`` bound from *0* to *1-n*. It is useful for side-effects. +Equivalent to ``(ap-each (range n) body…)``. .. code-block:: hy @@ -148,15 +164,15 @@ variable ``it`` bound from *0* to *1-n*. It is useful for side-effects. ap-first ======== -Usage ``(ap-first predfn list)`` +Usage: ``(ap-first form xs)`` -This function returns the first element that passes the predicate or -``None``, with the special variable ``it`` bound to the current element in -iteration. +Evaluate the predicate ``form`` for each element ``it`` of ``xs``. When +the predicate is true, stop and return ``it``. If the predicate is never +true, return ``None``. .. code-block:: hy - =>(ap-first (> it 5) (range 10)) + => (ap-first (> it 5) (range 10)) 6 @@ -165,15 +181,15 @@ iteration. ap-last ======== -Usage ``(ap-last predfn list)`` +Usage: ``(ap-last form list)`` -This function returns the last element that passes the predicate or -``None``, with the special variable ``it`` bound to the current element in -iteration. +Evaluate the predicate ``form`` for every element ``it`` of ``xs``. +Return the last element for which the predicate is true, or ``None`` if +there is no such element. .. code-block:: hy - =>(ap-last (> it 5) (range 10)) + => (ap-last (> it 5) (range 10)) 9 @@ -182,18 +198,24 @@ iteration. ap-reduce ========= -Usage ``(ap-reduce form list &optional initial-value)`` +Usage: ``(ap-reduce form xs &optional initial-value)`` -This function returns the result of applying form to the first 2 -elements in the body and applying the result and the 3rd element -etc. until the list is exhausted. Optionally an initial value can be -supplied so the function will be applied to initial value and the -first element instead. This exposes the element being iterated as -``it`` and the current accumulated value as ``acc``. +This macro is an anaphoric version of :py:func:`reduce`. It works as +follows: + +- Bind ``acc`` to the first element of ``xs``, bind ``it`` to the + second, and evaluate ``form``. +- Bind ``acc`` to the result, bind ``it`` to the third value of ``xs``, + and evaluate ``form`` again. +- Bind ``acc`` to the result, and continue until ``xs`` is exhausted. + +If ``initial-value`` is supplied, the process instead begins with +``acc`` set to ``initial-value`` and ``it`` set to the first element of +``xs``. .. code-block:: hy - =>(ap-reduce (+ it acc) (range 10)) + => (ap-reduce (+ it acc) (range 10)) 45 @@ -202,7 +224,7 @@ first element instead. This exposes the element being iterated as #% == -Usage ``#% expr`` +Usage: ``#% expr`` Makes an expression into a function with an implicit ``%`` parameter list. diff --git a/hy/extra/anaphoric.hy b/hy/extra/anaphoric.hy index d53e9fc..7b48146 100644 --- a/hy/extra/anaphoric.hy +++ b/hy/extra/anaphoric.hy @@ -6,112 +6,71 @@ ;;; These macros make writing functional programs more concise (defmacro ap-if [test-form then-form &optional else-form] - `(do + (rit `(do (setv it ~test-form) - (if it ~then-form ~else-form))) + (if it ~then-form ~else-form)))) -(defmacro ap-each [lst &rest body] - "Evaluate the body form for each element in the list." - `(for [it ~lst] ~@body)) +(defmacro ap-each [xs &rest body] + (rit `(for [it ~xs] ~@body))) -(defmacro ap-each-while [lst form &rest body] - "Evaluate the body form for each element in the list while the - predicate form evaluates to True." - (setv p (gensym)) - `(do - (defn ~p [it] ~form) - (for [it ~lst] - (if (~p it) - ~@body - (break))))) +(defmacro ap-each-while [xs form &rest body] + (rit `(for [it ~xs] + (unless ~form + (break)) + ~@body))) -(defmacro ap-map [form lst] - "Yield elements evaluated in the form for each element in the list." - (setv v (gensym 'v) f (gensym 'f)) - `((fn [] - (defn ~f [it] ~form) - (for [~v ~lst] - (yield (~f ~v)))))) +(defmacro ap-map [form xs] + (rit `(gfor it ~xs ~form))) -(defmacro ap-map-when [predfn rep lst] - "Yield elements evaluated for each element in the list when the - predicate function returns True." - (setv f (gensym)) - `((fn [] - (defn ~f [it] ~rep) - (for [it ~lst] - (if (~predfn it) - (yield (~f it)) - (yield it)))))) +(defmacro ap-map-when [predfn rep xs] + (rit `(gfor it ~xs (if (~predfn it) ~rep it)))) -(defmacro ap-filter [form lst] - "Yield elements returned when the predicate form evaluates to True." - (setv pred (gensym)) - `((fn [] - (defn ~pred [it] ~form) - (for [val ~lst] - (if (~pred val) - (yield val)))))) +(defmacro ap-filter [form xs] + (rit `(gfor it ~xs :if ~form it))) -(defmacro ap-reject [form lst] - "Yield elements returned when the predicate form evaluates to False" - `(ap-filter (not ~form) ~lst)) +(defmacro ap-reject [form xs] + (rit `(gfor it ~xs :if (not ~form) it))) (defmacro ap-dotimes [n &rest body] - "Execute body for side effects `n' times, with it bound from 0 to n-1" - (unless (numeric? n) - (raise (TypeError (.format "{!r} is not a number" n)))) - `(ap-each (range ~n) ~@body)) + (rit `(for [it (range ~n)] + ~@body))) -(defmacro ap-first [predfn lst] - "Yield the first element that passes `predfn`" - (with-gensyms [n] - `(do - (setv ~n None) - (ap-each ~lst (when ~predfn (setv ~n it) (break))) - ~n))) +(defmacro ap-first [form xs] + (rit `(next + (gfor it ~xs :if ~form it) + None))) -(defmacro ap-last [predfn lst] - "Yield the last element that passes `predfn`" - (with-gensyms [n] - `(do - (setv ~n None) - (ap-each ~lst (none? ~n) - (when ~predfn - (setv ~n it))) - ~n))) +(defmacro ap-last [form xs] + (setv x (gensym)) + (rit `(do + (setv ~x None) + (for [it ~xs :if ~form] + (setv ~x it)) + ~x))) -(defmacro! ap-reduce [form o!lst &optional [initial-value None]] - "Anaphoric form of reduce, `acc' and `it' can be used for a form" - `(do +(defmacro! ap-reduce [form o!xs &optional [initial-value None]] + (recur-sym-replace {'it (gensym) 'acc (gensym)} `(do (setv acc ~(if (none? initial-value) `(do - (setv ~g!lst (iter ~g!lst)) - (next ~g!lst)) + (setv ~g!xs (iter ~g!xs)) + (next ~g!xs)) initial-value)) - (ap-each ~g!lst (setv acc ~form)) - acc)) + (for [it ~g!xs] + (setv acc ~form)) + acc))) (deftag % [expr] - "Makes an expression into a function with an implicit `%` parameter list. - - A `%i` symbol designates the (1-based) ith parameter (such as `%3`). - Only the maximum `%i` determines the number of `%i` parameters--the - others need not appear in the expression. - `%*` and `%**` name the `&rest` and `&kwargs` parameters, respectively. - - Nesting of `#%` forms is not recommended." (setv %symbols (sfor a (flatten [expr]) :if (and (symbol? a) (.startswith a '%)) @@ -132,3 +91,23 @@ '(&kwargs %**))] ~expr)) + +;;; -------------------------------------------------- +;;; Subroutines +;;; -------------------------------------------------- + + +(defn recur-sym-replace [d form] + "Recursive symbol replacement." + (cond + [(instance? HySymbol form) + (.get d form form)] + [(coll? form) + ((type form) (gfor x form (recur-sym-replace d x)))] + [True + form])) + + +(defn rit [form] + "Replace `it` with a gensym throughout `form`." + (recur-sym-replace {'it (gensym)} form)) diff --git a/tests/native_tests/extra/anaphoric.hy b/tests/native_tests/extra/anaphoric.hy index d99779f..1177ab0 100644 --- a/tests/native_tests/extra/anaphoric.hy +++ b/tests/native_tests/extra/anaphoric.hy @@ -7,11 +7,17 @@ (defn test-ap-if [] (ap-if True (assert (is it True))) - (ap-if False True (assert (is it False)))) + (ap-if False True (assert (is it False))) + + ; https://github.com/hylang/hy/issues/1847 + (setv it "orig") + (setv out (ap-if (+ 1 1) (+ it 1) (+ it 10))) + (assert (= out 3)) + (assert (= it "orig"))) (defn test-ap-each [] (setv res []) - (ap-each [1 2 3 4] (.append res it)) + (assert (is (ap-each [1 2 3 4] (.append res it)) None)) (assert (= res [1 2 3 4]))) (defn test-ap-each-while [] @@ -47,7 +53,13 @@ (assert (= (do (setv n []) (ap-dotimes 3 (.append n 3)) n) [3 3 3])) (assert (= (do (setv n []) (ap-dotimes 3 (.append n it)) n) - [0 1 2]))) + [0 1 2])) + + ; https://github.com/hylang/hy/issues/1853 + (setv n 5) + (setv x "") + (ap-dotimes n (+= x ".")) + (assert (= x "....."))) (defn test-ap-first [] (assert (= (ap-first (> it 5) (range 10)) 6)) diff --git a/tests/native_tests/extra/anaphoric_single.hy b/tests/native_tests/extra/anaphoric_single.hy new file mode 100644 index 0000000..1dd00aa --- /dev/null +++ b/tests/native_tests/extra/anaphoric_single.hy @@ -0,0 +1,11 @@ +;; Copyright 2019 the authors. +;; This file is part of Hy, which is free software licensed under the Expat +;; license. See the LICENSE. + +(require [hy.extra.anaphoric [ap-last]]) + +(defn test-anaphoric-single-require [] + ; https://github.com/hylang/hy/issues/1853#issuecomment-568192529 + ; `ap-last` should work even if `require`d without anything else + ; from the anaphoric module. + (assert (= (ap-last (> it 0) [-1 1 0 3 2 0 -1]) 2)))