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 [*]])``
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 <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.

View File

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

View File

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

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