diff --git a/docs/contrib/index.rst b/docs/contrib/index.rst index ed75add..5ddac2a 100644 --- a/docs/contrib/index.rst +++ b/docs/contrib/index.rst @@ -12,4 +12,5 @@ Contents: flow loop multi + sequences walk diff --git a/docs/contrib/sequences.rst b/docs/contrib/sequences.rst new file mode 100644 index 0000000..1c675e0 --- /dev/null +++ b/docs/contrib/sequences.rst @@ -0,0 +1,81 @@ +============== +Lazy sequences +============== + +.. versionadded:: 0.12.0 + +The sequences module contains a few macros for declaring sequences that are +evaluated only as much as the client code requires. Unlike generators, they +allow accessing the same element multiple times. They cache calculated values, +and the implementation allows for recursive definition of sequences without +resulting in recursive computation. + +To use these macros, you need to require them and import some other names like +so: + +.. code-block:: hy + + (require [hy.contrib.sequences [defseq seq]]) + (import [hy.contrib.sequences [Sequence end-sequence]]) + +The simplest sequence can be defined as ``(seq [n] n)``. This defines a sequence +that starts as ``[0 1 2 3 ...]`` and continues forever. In order to define a +finite sequence, you need to call ``end-sequence`` to signal the end of the +sequence: + +.. code-block:: hy + + (seq [n] + "sequence of 5 integers" + (cond [(< n 5) n] + [True (end-sequence)])) + +This creates the following sequence: ``[0 1 2 3 4]``. For such a sequence, +``len`` returns the amount of items in the sequence and negative indexing is +supported. Because both of these require evaluating the whole sequence, calling +one on an infinite sequence would take forever (or at least until available +memory has been exhausted). + +Sequences can be defined recursively. For example, the Fibonacci sequence could +be defined as: + +.. code-block:: hy + + (defseq fibonacci [n] + "infinite sequence of fibonacci numbers" + (cond [(= n 0) 0] + [(= n 1) 1] + [True (+ (get fibonacci (- n 1)) + (get fibonacci (- n 2)))])) + +This results in the sequence ``[0 1 1 2 3 5 8 13 21 34 ...]``. + +.. _seq: + +seq +=== + +Usage: ``(seq [n] (* n n)`` + +Creates a sequence defined in terms of ``n``. + +.. _defseq: + +defseq +====== + +Usage: ``(defseq numbers [n] n)`` + +Creates a sequence defined in terms of ``n`` and assigns it to a given name. + +.. _end-sequence: + +end-sequence +============ + +Usage: ``(seq [n] (if (< n 5) n (end-sequence)))`` + +Signals the end of a sequence when an iterator reaches the given +point of the sequence. Internally, this is done by raising +``IndexError``, catching that in the iterator, and raising +``StopIteration``. diff --git a/hy/contrib/sequences.hy b/hy/contrib/sequences.hy new file mode 100644 index 0000000..f2f4ff3 --- /dev/null +++ b/hy/contrib/sequences.hy @@ -0,0 +1,79 @@ +;; Copyright (c) 2016 Tuukka Turto +;; +;; Permission is hereby granted, free of charge, to any person obtaining a +;; copy of this software and associated documentation files (the "Software"), +;; to deal in the Software without restriction, including without limitation +;; the rights to use, copy, modify, merge, publish, distribute, sublicense, +;; and/or sell copies of the Software, and to permit persons to whom the +;; Software is furnished to do so, subject to the following conditions: +;; +;; The above copyright notice and this permission notice shall be included in +;; all copies or substantial portions of the Software. +;; +;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +;; THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +;; FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +;; DEALINGS IN THE SOFTWARE. +;; + +(defclass Sequence [] + [--init-- (fn [self func] + "initialize a new sequence with a function to compute values" + (setv (. self func) func) + (setv (. self cache) []) + (setv (. self high-water) -1)) + --getitem-- (fn [self n] + "get nth item of sequence" + (if (hasattr n "start") + (genexpr (get self x) [x (range n.start n.stop + (or n.step 1))]) + (do (when (neg? n) + ; Call (len) to force the whole + ; sequence to be evaluated. + (len self)) + (if (<= n (. self high-water)) + (get (. self cache) n) + (do (while (< (. self high-water) n) + (setv (. self high-water) (inc (. self high-water))) + (.append (. self cache) (.func self (. self high-water)))) + (get self n)))))) + --iter-- (fn [self] + "create iterator for this sequence" + (setv index 0) + (try (while True + (yield (get self index)) + (setv index (inc index))) + (except [_ IndexError] + (raise StopIteration)))) + --len-- (fn [self] + "length of the sequence, dangerous for infinite sequences" + (setv index (. self high-water)) + (try (while True + (get self index) + (setv index (inc index))) + (except [_ IndexError] + (len (. self cache))))) + max-items-in-repr 10 + --str-- (fn [self] + "string representation of this sequence" + (setv items (list (take (inc self.max-items-in-repr) self))) + (.format (if (> (len items) self.max-items-in-repr) + "[{0}, ...]" + "[{0}]") + (.join ", " (map str items)))) + --repr-- (fn [self] + "string representation of this sequence" + (.--str-- self))]) + +(defmacro seq [param &rest seq-code] + `(Sequence (fn ~param (do ~@seq-code)))) + +(defmacro defseq [seq-name param &rest seq-code] + `(def ~seq-name (Sequence (fn ~param (do ~@seq-code))))) + +(defn end-sequence [] + "raise IndexError exception to signal end of sequence" + (raise (IndexError "list index out of range"))) diff --git a/tests/__init__.py b/tests/__init__.py index 9580680..9c11bd9 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -21,6 +21,7 @@ from .native_tests.contrib.walk import * # noqa from .native_tests.contrib.multi import * # noqa from .native_tests.contrib.curry import * # noqa from .native_tests.contrib.botsbuildbots import * # noqa +from .native_tests.contrib.sequences import * # noqa if PY3: from .native_tests.py3_only_tests import * # noqa diff --git a/tests/native_tests/contrib/sequences.hy b/tests/native_tests/contrib/sequences.hy new file mode 100644 index 0000000..50db5a8 --- /dev/null +++ b/tests/native_tests/contrib/sequences.hy @@ -0,0 +1,111 @@ +;; Copyright (c) 2016 Tuukka Turto +;; +;; Permission is hereby granted, free of charge, to any person obtaining a +;; copy of this software and associated documentation files (the "Software"), +;; to deal in the Software without restriction, including without limitation +;; the rights to use, copy, modify, merge, publish, distribute, sublicense, +;; and/or sell copies of the Software, and to permit persons to whom the +;; Software is furnished to do so, subject to the following conditions: +;; +;; The above copyright notice and this permission notice shall be included in +;; all copies or substantial portions of the Software. +;; +;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +;; THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +;; FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +;; DEALINGS IN THE SOFTWARE. +;; + +(require [hy.contrib.sequences [seq defseq]]) + +(import [hy.contrib.sequences [Sequence end-sequence]]) + +(defn test-infinite-sequence [] + "NATIVE: test creating infinite sequence" + (assert (= (list (take 5 (seq [n] n))) + [0 1 2 3 4]))) + +(defn test-indexing-sequence [] + "NATIVE: test indexing sequence" + (defseq shorty [n] + (cond [(< n 10) n] + [True (end-sequence)])) + (setv 0-to-9 (list (range 10))) + (assert (= (get shorty 0) + (get 0-to-9 0)) + "getting first element failed") + (assert (= (get shorty 5) + (get 0-to-9 5)) + "getting 5th element failed") + (assert (= (get shorty -1) + (get 0-to-9 -1)) + "getting element -1 failed")) + +(defn test-slicing-sequence [] + "NATIVE: test slicing sequence" + (defseq shorty [n] + (cond [(< n 10) n] + [True (end-sequence)])) + (setv 0-to-9 (list (range 10))) + (assert (= (first shorty) + (first 0-to-9)) + "getting first failed") + (assert (= (list (rest shorty)) + (list (rest 0-to-9))) + "getting rest failed") + (assert (= (list (cut shorty 2 6)) + (list (cut 0-to-9 2 6))) + "cutting 2-6 failed") + (assert (= (list (cut shorty 2 8 2)) + (list (cut 0-to-9 2 8 2))) + "cutting 2-8-2 failed") + (assert (= (list (cut shorty 8 2 -2)) + (list (cut 0-to-9 8 2 -2))) + "negative cut failed")) + +(defn test-recursive-sequence [] + "NATIVE: test defining a recursive sequence" + (defseq fibonacci [n] + (cond [(= n 0) 0] + [(= n 1) 1] + [True (+ (get fibonacci (- n 1)) + (get fibonacci (- n 2)))])) + (assert (= (first fibonacci) + 0) + "first element of fibonacci didn't match") + (assert (= (second fibonacci) + 1) + "second element of fibonacci didn't match") + (assert (= (get fibonacci 40) + 102334155) + "40th element of fibonacci didn't match") + (assert (= (list (take 9 fibonacci)) + [0 1 1 2 3 5 8 13 21]) + "taking 8 elements of fibonacci didn't match")) + +(defn test-nested-functions [] + "NATIVE: test that defining nested functions is possible" + (defseq primes [n] + "infinite sequence of prime numbers" + (defn divisible? [n prevs] + "is n divisible by any item in prevs?" + (any (map (fn [x] + (not (% n x))) + prevs))) + (defn previous-primes [n] + "previous prime numbers" + (take (dec n) primes)) + (defn next-possible-prime [n] + "next possible prime after nth prime" + (inc (get primes (dec n)))) + (cond [(= n 0) 2] + [True (do (setv guess (next-possible-prime n)) + (while (divisible? guess (previous-primes n)) + (setv guess (inc guess))) + guess)])) + (assert (= (list (take 10 primes)) + [2 3 5 7 11 13 17 19 23 29]) + "prime sequence didn't match"))