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..318a684 --- /dev/null +++ b/docs/contrib/sequences.rst @@ -0,0 +1,77 @@ +============== +Lazy sequences +============== + +.. versionadded:: 0.12.0 + +Sequences module contains few macros for declaring sequences that are evaluated +only as much as the client code requests elements. Compared to generators they +allow accessing same element multiple times. Since they cache calculated +values, they aren't suited for infinite sequences. However, the implementation +allows recursive definition of sequences, without resulting recursive +computation. + +To use these macros you need to require them and import other types like: + +.. code-block:: hy + + (require [hy.contrib.sequences [defseq seq]]) + (import [hy.contrib.sequences [Sequence end-sequence]]) + +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, ``end-sequence`` needs to be called to signal end of +the sequence: + +.. code-block:: hy + + (seq [n] + (cond [(< n 5) n] + [true (end-sequence)])) + +This creates following sequence: ``[0 1 2 3 4]``. For such a sequence ``len`` +returns amount of items in sequence and negative indexing is suported. Because +both of thse require evaluating whole sequence, calling such a function would +take forever (or at least until available memory has been exhausted). + +Sequence can be defined recursively. Canonical example of fibonacci numbers +is defined as: + +.. code-block:: hy + + (defseq fibonacci [n] + (cond [(= n 0) 0] + [(= n 1) 1] + [true (+ (get fibonacci (- n 1)) + (get fibonacci (- n 2)))])) + +This results sequence of ``[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 end of a sequence when iterator reaches certain point of sequence. +Internally this is done by raising ``IndexError``, catching that in iterator +and raising ``StopIteration``. diff --git a/hy/contrib/sequences.hy b/hy/contrib/sequences.hy new file mode 100644 index 0000000..20f4c46 --- /dev/null +++ b/hy/contrib/sequences.hy @@ -0,0 +1,78 @@ +;; 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) + nil) + --getitem-- (fn [self n] + "get nth item of sequence" + (if (hasattr n "start") + (if n.step + (genexpr (get self x) [x (range n.start n.stop n.step)]) + (genexpr (get self x) [x (range n.start n.stop 1)])) + (do (when (neg? n) + (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))))) + --str-- (fn [self] + "string representation of this sequence" + (setv items (list (take 11 self))) + (.format (if (= (len items) 11) + "[{0}, ...]" + "[{0}]") + (.join ", " (map str items)))) + --repr-- (fn [self] + "string representation of this sequence" + (.--str-- self))]) + +(defmacro seq [param seq-code] + `(Sequence (fn ~param ~seq-code))) + +(defmacro defseq [seq-name param seq-code] + `(def ~seq-name (seq ~param ~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..4d40473 --- /dev/null +++ b/tests/native_tests/contrib/sequences.hy @@ -0,0 +1,85 @@ +;; 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)])) + (assert (= (get shorty 0) + (get (range 10) 0)) + "getting first element failed") + (assert (= (get shorty 5) + (get (range 10) 5)) + "getting 5th element failed") + (assert (= (get shorty -1) + (get (range 10) -1)) + "getting element -1 failed")) + +(defn test-slicing-sequence [] + "NATIVE: test slicing sequence" + (defseq shorty [n] + (cond [(< n 10) n] + [true (end-sequence)])) + (assert (= (first shorty) + (first (range 10))) + "getting first failed") + (assert (= (list (rest shorty)) + (list (rest (range 10)))) + "getting rest failed") + (assert (= (list (cut shorty 2 6)) + (list (cut (range 10) 2 6))) + "cutting 2-6 failed") + (assert (= (list (cut shorty 2 8 2)) + (list (cut (range 10) 2 8 2))) + "cutting 2-8-2 failed") + (assert (= (list (cut shorty 8 2 -2)) + (list (cut (range 10) 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"))