diff --git a/AUTHORS b/AUTHORS index 5c9cdfe6..b8eed6e3 100644 --- a/AUTHORS +++ b/AUTHORS @@ -109,3 +109,4 @@ * Gabriel F. C. Pereira <empresagabriel@gmail.com> * Daniel Nagy <danielnagy@posteo.de> * John Blundell <jlobblet@jlobblet.co.uk> +* Artur Wroblewski <wrobell@riseup.net> diff --git a/NEWS.rst b/NEWS.rst index 9535a707..d39b3490 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -7,6 +7,7 @@ New Features ------------------------------ * New macro `defmacro-kwargs`. * New macro `parse-fn-params`. +* New macro `some->`. * New function `sign`. * New function `thru`. diff --git a/docs/index.rst b/docs/index.rst index aef0a12c..8a74adab 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -38,6 +38,7 @@ Reference .. hy:automacro:: -> .. hy:automacro:: ->> .. hy:automacro:: as-> +.. hy:automacro:: some-> .. hy:automacro:: doto ``collections`` — Tools for data structures diff --git a/hyrule/argmove.hy b/hyrule/argmove.hy index 6f9ad056..d52c6469 100644 --- a/hyrule/argmove.hy +++ b/hyrule/argmove.hy @@ -1,5 +1,6 @@ (import - hyrule.iterables [rest]) + hyrule.iterables [rest] + itertools [chain]) (eval-and-compile @@ -138,6 +139,34 @@ ~name)) +(defmacro some-> [head #* args] + "Thread `head` first through the `rest` of the forms, but short-circuit + if a form returns `None`. + + ``some->`` (or the *threading macro* with short-circuit) is used to avoid + nesting of expressions. The threading macro inserts each expression into + the next expression's first argument place. However if an expression + returns `None`, then following expressions are ignored. The following + code demonstrates this: + + Examples: + :: + + => (defn output [a b] (print a b)) + => (some-> (+ 4 6) (output 5)) + 10 5 + + => (defn ret_none [a b] None) + => (some-> 1 (+ 2) (ret_none 3) (* 3)) + None + " + (setv val (hy.gensym)) + `(cond (is (setx ~val ~head) None) None + ~@(chain.from_iterable (gfor node args + [`(is (setx ~val (hy.R.hyrule.-> ~val ~node)) None) None])) + True ~val)) + + (defmacro doto [form #* expressions] "Perform possibly mutating `expressions` on `form`, returning resulting obj. diff --git a/tests/test_argmove.hy b/tests/test_argmove.hy index b95d85b0..66fd4032 100644 --- a/tests/test_argmove.hy +++ b/tests/test_argmove.hy @@ -1,4 +1,5 @@ -(require hyrule [-> ->> as-> doto]) +(import unittest [mock]) +(require hyrule [-> ->> as-> some-> doto]) (defn test-threading [] @@ -63,6 +64,36 @@ "Sir Joseph Cooke Verco"))) +(defn test-threading-some [] + ; the macro uses `cond`, so test for odd and even number of expressions + (assert (= (some-> 1 (+ 2) (* 3)) 9)) + (assert (= (some-> 1 (+ 2) (* 3) (+ 1)) 10)) + + ; test for non-expression form + (defn inc [x] (+ x 1)) + (assert (= (some-> 1 inc) 2))) + + +(defn test-threading-some-circuit [] + (setv m (mock.MagicMock)) + + ; test for null head + (assert (is (some-> None m) None)) + (assert (= m.call_count 0)) ;; m is never called + + ; test short-circuit with a function for odd and even number of + ; expressions + (defn ret_none [a b] None) + + (setv m (mock.MagicMock)) + (assert (is (some-> 1 m (ret_none 3) m) None)) + (assert (= m.call_count 1)) ;; m is called once only, before `ret_none` + + (setv m (mock.MagicMock)) + (assert (is (some-> 1 m m (ret_none 3) m) None)) + (assert (= m.call_count 2))) ;; m is called twice only, before `ret_none` + + (defn test-doto [] (setv collection []) (doto collection (.append 1) (.append 2) (.append 3))