diff --git a/NEWS.rst b/NEWS.rst index a21863ed..92387e6c 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -3,6 +3,10 @@ Unreleased ====================================================== +New Features +------------------------------ +* New macro `map-hyseq`. + Bug Fixes ------------------------------ * `map-model` now calls `as-model` only once (before its own recursion), diff --git a/docs/index.rst b/docs/index.rst index 9eae3800..058a7737 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -104,6 +104,7 @@ API .. hy:automacro:: defmacro-kwargs .. hy:automacro:: defmacro! .. hy:autofunction:: macroexpand-all +.. hy:autofunction:: map-hyseq .. hy:autofunction:: map-model .. hy:autofunction:: match-fn-params .. hy:automacro:: with-gensyms diff --git a/hyrule/macrotools.hy b/hyrule/macrotools.hy index a9e9b798..30de9b53 100644 --- a/hyrule/macrotools.hy +++ b/hyrule/macrotools.hy @@ -270,21 +270,34 @@ (_map-model (hy.as-model x) f)) (defn _map-model [x f] - (cond - (is-not (setx value (f x)) None) - (hy.as-model value) - (isinstance x hy.models.Sequence) - ((type x) - (gfor elem x (_map-model elem f)) - #** (cond - (isinstance x hy.models.FString) - {"brackets" x.brackets} - (isinstance x hy.models.FComponent) - {"conversion" x.conversion} - True - {})) - True - x)) + (if (is-not (setx value (f x)) None) + (hy.as-model value) + (map-hyseq x (fn [contents] + (gfor elem contents (_map-model elem f)))))) + +(defn map-hyseq [x f] + + "Apply the function ``f`` to the contents of the :ref:`sequential model ` ``x`` gathered into a tuple. ``f`` should return an iterable object. This result is then wrapped in the original model type, preserving attributes such as the brackets of an :class:`hy.models.FString`. :: + + (map-hyseq '[:a :b :c] (fn [x] + (gfor e x (hy.models.Keyword (.upper e.name))))) + ; => '[:A :B :C] + + Unlike :hy:func:`map-model`, ``map-hyseq`` isn't inherently recursive. + + If ``x`` isn't a sequential Hy model, it's returned as-is, without calling ``f``." + + (if (isinstance x hy.models.Sequence) + ((type x) + (f (tuple x)) + #** (cond + (isinstance x hy.models.FString) + {"brackets" x.brackets} + (isinstance x hy.models.FComponent) + {"conversion" x.conversion} + True + {})) + x)) (defmacro with-gensyms [args #* body] diff --git a/tests/test_macrotools.hy b/tests/test_macrotools.hy index abcb06f4..b92bd495 100644 --- a/tests/test_macrotools.hy +++ b/tests/test_macrotools.hy @@ -3,7 +3,7 @@ :readers [/]) (import pytest - hyrule [macroexpand-all map-model match-fn-params]) + hyrule [macroexpand-all map-hyseq map-model match-fn-params]) (defn test-defmacro-kwargs [] @@ -110,6 +110,42 @@ '(setv blah 1)))) +(defn test-map-hyseq [] + + ; If `x` isn't sequential (or not a model at all), `f` isn't called. + (assert (= + (map-hyseq + 3 + (fn [x] (raise ValueError))) + 3)) + (assert (= + (map-hyseq + [1 2] + (fn [x] (raise ValueError))) + [1 2])) + + ; `f` can be called when `x` is empty. + (assert (= + (map-hyseq + '[] + (fn [x] ['4])) + '[4])) + + ; `f` gets the sequence contents as a tuple. + (setv saw None) + (assert (= + (map-hyseq + '{1 2 3 4} + (fn [x] + (nonlocal saw) + (setv saw x) + (gfor e x (hy.models.Integer (+ e 1))))) + '{2 3 4 5})) + (assert (= saw #('1 '2 '3 '4)))) + ; Preservation of sequence attributes is tested as part of testing + ; `map-model`. + + (defn test-map-model [] ; When the callback returns `None`, the element is recursed into, or