Merge pull request #1855 from Kodiologist/anaphix

Overhaul hy.extra.anaphoric
This commit is contained in:
Kodi Arfer 2020-01-05 07:56:11 -05:00 committed by GitHub
commit 42100b1658
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 150 additions and 126 deletions

View File

@ -17,15 +17,27 @@ To use these macros you need to require the ``hy.extra.anaphoric`` module like s
``(require [hy.extra.anaphoric [*]])`` ``(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:
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 As :ref:`if <if>`, but the result of the test form is named ``it`` in
true and false branches. 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: .. _ap-each:
@ -33,9 +45,17 @@ true and false branches.
ap-each 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: .. _ap-each-while:
@ -43,10 +63,10 @@ Evaluate the form for each element in the list for side-effects.
ap-each-while 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 As ``ap-each``, but the form ``pred`` is run before the body forms on
``True``. each iteration, and the loop ends if ``pred`` is false.
.. code-block:: hy .. code-block:: hy
@ -60,11 +80,10 @@ Evaluate the form for each element where the predicate form returns
ap-map ap-map
====== ======
Usage: ``(ap-map form list)`` Usage: ``(ap-map form xs)``
The anaphoric form of map works just like regular map except that Create a generator like :py:func:`map` that yields each result of ``form``
instead of a function object it takes a Hy form. The special name evaluated with ``it`` bound to successive elements of ``xs``.
``it`` is bound to the current object from the list in the iteration.
.. code-block:: hy .. code-block:: hy
@ -77,10 +96,12 @@ instead of a function object it takes a Hy form. The special name
ap-map-when 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 As ``ap-map``, but the predicate function ``predfn`` (yes, that's a
determin when to apply the form. 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 .. code-block:: hy
@ -96,11 +117,9 @@ determin when to apply the form.
ap-filter 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 The :py:func:`filter` equivalent of ``ap-map``.
filter the elements of the list. The special name ``it`` is bound to
the current element in the iteration.
.. code-block:: hy .. code-block:: hy
@ -113,11 +132,9 @@ the current element in the iteration.
ap-reject ap-reject
========= =========
Usage: ``(ap-reject form list)`` Usage: ``(ap-reject form xs)``
This function does the opposite of ``ap-filter``, it rejects the Equivalent to ``(ap-filter (not form) xs)``.
elements passing the predicate . The special name ``it`` is bound to
the current element in the iteration.
.. code-block:: hy .. code-block:: hy
@ -130,10 +147,9 @@ the current element in the iteration.
ap-dotimes ap-dotimes
========== ==========
Usage ``(ap-dotimes n body)`` Usage: ``(ap-dotimes n body)``
This function evaluates the body *n* times, with the special Equivalent to ``(ap-each (range n) body…)``.
variable ``it`` bound from *0* to *1-n*. It is useful for side-effects.
.. code-block:: hy .. code-block:: hy
@ -148,15 +164,15 @@ variable ``it`` bound from *0* to *1-n*. It is useful for side-effects.
ap-first ap-first
======== ========
Usage ``(ap-first predfn list)`` Usage: ``(ap-first form xs)``
This function returns the first element that passes the predicate or Evaluate the predicate ``form`` for each element ``it`` of ``xs``. When
``None``, with the special variable ``it`` bound to the current element in the predicate is true, stop and return ``it``. If the predicate is never
iteration. true, return ``None``.
.. code-block:: hy .. code-block:: hy
=>(ap-first (> it 5) (range 10)) => (ap-first (> it 5) (range 10))
6 6
@ -165,15 +181,15 @@ iteration.
ap-last ap-last
======== ========
Usage ``(ap-last predfn list)`` Usage: ``(ap-last form list)``
This function returns the last element that passes the predicate or Evaluate the predicate ``form`` for every element ``it`` of ``xs``.
``None``, with the special variable ``it`` bound to the current element in Return the last element for which the predicate is true, or ``None`` if
iteration. there is no such element.
.. code-block:: hy .. code-block:: hy
=>(ap-last (> it 5) (range 10)) => (ap-last (> it 5) (range 10))
9 9
@ -182,18 +198,24 @@ iteration.
ap-reduce 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 This macro is an anaphoric version of :py:func:`reduce`. It works as
elements in the body and applying the result and the 3rd element follows:
etc. until the list is exhausted. Optionally an initial value can be
supplied so the function will be applied to initial value and the - Bind ``acc`` to the first element of ``xs``, bind ``it`` to the
first element instead. This exposes the element being iterated as second, and evaluate ``form``.
``it`` and the current accumulated value as ``acc``. - 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 .. code-block:: hy
=>(ap-reduce (+ it acc) (range 10)) => (ap-reduce (+ it acc) (range 10))
45 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. Makes an expression into a function with an implicit ``%`` parameter list.

View File

@ -6,112 +6,71 @@
;;; These macros make writing functional programs more concise ;;; These macros make writing functional programs more concise
(defmacro ap-if [test-form then-form &optional else-form] (defmacro ap-if [test-form then-form &optional else-form]
`(do (rit `(do
(setv it ~test-form) (setv it ~test-form)
(if it ~then-form ~else-form))) (if it ~then-form ~else-form))))
(defmacro ap-each [lst &rest body] (defmacro ap-each [xs &rest body]
"Evaluate the body form for each element in the list." (rit `(for [it ~xs] ~@body)))
`(for [it ~lst] ~@body))
(defmacro ap-each-while [lst form &rest body] (defmacro ap-each-while [xs form &rest body]
"Evaluate the body form for each element in the list while the (rit `(for [it ~xs]
predicate form evaluates to True." (unless ~form
(setv p (gensym)) (break))
`(do ~@body)))
(defn ~p [it] ~form)
(for [it ~lst]
(if (~p it)
~@body
(break)))))
(defmacro ap-map [form lst] (defmacro ap-map [form xs]
"Yield elements evaluated in the form for each element in the list." (rit `(gfor it ~xs ~form)))
(setv v (gensym 'v) f (gensym 'f))
`((fn []
(defn ~f [it] ~form)
(for [~v ~lst]
(yield (~f ~v))))))
(defmacro ap-map-when [predfn rep lst] (defmacro ap-map-when [predfn rep xs]
"Yield elements evaluated for each element in the list when the (rit `(gfor it ~xs (if (~predfn it) ~rep it))))
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-filter [form lst] (defmacro ap-filter [form xs]
"Yield elements returned when the predicate form evaluates to True." (rit `(gfor it ~xs :if ~form it)))
(setv pred (gensym))
`((fn []
(defn ~pred [it] ~form)
(for [val ~lst]
(if (~pred val)
(yield val))))))
(defmacro ap-reject [form lst] (defmacro ap-reject [form xs]
"Yield elements returned when the predicate form evaluates to False" (rit `(gfor it ~xs :if (not ~form) it)))
`(ap-filter (not ~form) ~lst))
(defmacro ap-dotimes [n &rest body] (defmacro ap-dotimes [n &rest body]
"Execute body for side effects `n' times, with it bound from 0 to n-1" (rit `(for [it (range ~n)]
(unless (numeric? n) ~@body)))
(raise (TypeError (.format "{!r} is not a number" n))))
`(ap-each (range ~n) ~@body))
(defmacro ap-first [predfn lst] (defmacro ap-first [form xs]
"Yield the first element that passes `predfn`" (rit `(next
(with-gensyms [n] (gfor it ~xs :if ~form it)
`(do None)))
(setv ~n None)
(ap-each ~lst (when ~predfn (setv ~n it) (break)))
~n)))
(defmacro ap-last [predfn lst] (defmacro ap-last [form xs]
"Yield the last element that passes `predfn`" (setv x (gensym))
(with-gensyms [n] (rit `(do
`(do (setv ~x None)
(setv ~n None) (for [it ~xs :if ~form]
(ap-each ~lst (none? ~n) (setv ~x it))
(when ~predfn ~x)))
(setv ~n it)))
~n)))
(defmacro! ap-reduce [form o!lst &optional [initial-value None]] (defmacro! ap-reduce [form o!xs &optional [initial-value None]]
"Anaphoric form of reduce, `acc' and `it' can be used for a form" (recur-sym-replace {'it (gensym) 'acc (gensym)} `(do
`(do
(setv acc ~(if (none? initial-value) (setv acc ~(if (none? initial-value)
`(do `(do
(setv ~g!lst (iter ~g!lst)) (setv ~g!xs (iter ~g!xs))
(next ~g!lst)) (next ~g!xs))
initial-value)) initial-value))
(ap-each ~g!lst (setv acc ~form)) (for [it ~g!xs]
acc)) (setv acc ~form))
acc)))
(deftag % [expr] (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]) (setv %symbols (sfor a (flatten [expr])
:if (and (symbol? a) :if (and (symbol? a)
(.startswith a '%)) (.startswith a '%))
@ -132,3 +91,23 @@
'(&kwargs %**))] '(&kwargs %**))]
~expr)) ~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))

View File

@ -7,11 +7,17 @@
(defn test-ap-if [] (defn test-ap-if []
(ap-if True (assert (is it True))) (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 [] (defn test-ap-each []
(setv res []) (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]))) (assert (= res [1 2 3 4])))
(defn test-ap-each-while [] (defn test-ap-each-while []
@ -47,7 +53,13 @@
(assert (= (do (setv n []) (ap-dotimes 3 (.append n 3)) n) (assert (= (do (setv n []) (ap-dotimes 3 (.append n 3)) n)
[3 3 3])) [3 3 3]))
(assert (= (do (setv n []) (ap-dotimes 3 (.append n it)) n) (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 [] (defn test-ap-first []
(assert (= (ap-first (> it 5) (range 10)) 6)) (assert (= (ap-first (> it 5) (range 10)) 6))

View File

@ -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)))