From dd0a243e1b68041ec8636be6ed77319b9857cf1b Mon Sep 17 00:00:00 2001 From: James Skillen Date: Thu, 19 Jan 2017 17:31:06 +0000 Subject: [PATCH 01/10] Add recur.stackless(). --- fn/recur.py | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/fn/recur.py b/fn/recur.py index cb9d813..f3daa83 100644 --- a/fn/recur.py +++ b/fn/recur.py @@ -1,5 +1,6 @@ -"""Provides decorator to deal with tail calls in recursive function.""" +"""Provides decorators to deal with tail calls in recursive functions.""" +from collections import namedtuple class tco(object): """Provides a trampoline for functions that need one. @@ -44,3 +45,88 @@ def __call__(self, *args, **kwargs): if callable(act): action = act kwargs = result[2] if len(result) > 2 else {} + +class stackless(object): + """Provides a "stackless" (constant Python stack space) recursion + decorator for generators. + + Invoking as f() creates the control structures. Within a + function, only use `yield f.call()` and `yield f.tailcall()`. + + Usage examples: + + Tail call optimised recursion with tailcall(): + + @recur.stackless + def fact(n, acc=1): + if n == 0: + yield acc + return + yield fact.tailcall(n-1, n*acc) + + Non-tail recursion with call() uses heap space so won't overflow: + + @recur.stackless + def fib(n): + if n == 0: + yield 1 + return + if n == 1: + yield 1 + return + yield (yield fib.call(n-1)) + (yield fib.call(n-2)) + + Mutual recursion also works: + + @recur.stackless + def is_odd(n): + if n == 0: + yield False + return + yield is_even.tailcall(n-1) + + @recur.stackless + def is_even(n): + if n == 0: + yield True + return + yield is_odd.tailcall(n-1) + + """ + + __slots__ = "func", + + Thunk = namedtuple('Thunk', ("func", "args", "kwargs", "is_tailcall")) + + def __init__(self, func): + self.func = func + + def call(self, *args, **kwargs): + return self.Thunk(self.func, args, kwargs, False) + + def tailcall(self, *args, **kwargs): + return self.Thunk(self.func, args, kwargs, True) + + def __call__(self, *args, **kwargs): + s = [self.func(*args, **kwargs)] + r = [] + while s: + try: + if r: + v = s[-1].send(r[-1]) + r.pop() + else: + v = next(s[-1]) + except StopIteration: + s.pop() + continue + + if isinstance(v, self.Thunk): + g = v.func(*v.args, **v.kwargs) + if v.is_tailcall: + s[-1] = g + else: + s.append(g) + else: + r.append(v) + return r[0] From 899003fab40499025269c03f86d9b5976b042a13 Mon Sep 17 00:00:00 2001 From: Jacob Bridges Date: Fri, 17 Feb 2017 14:58:24 -0600 Subject: [PATCH 02/10] changed `pip install fn` to `pip install fn.py` --- README.rst | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index 189cc3c..7991e09 100644 --- a/README.rst +++ b/README.rst @@ -436,19 +436,13 @@ To install ``fn.py``, simply: .. code-block:: console - $ pip install fn - -Or, if you absolutely must: - -.. code-block:: console - - $ easy_install fn + $ pip install fn.py You can also build library from source .. code-block:: console - $ git clone https://github.com/kachayev/fn.py.git + $ git clone https://github.com/fnpy/fn.py.git $ cd fn.py $ python setup.py install From 438b4c6383a49148a45953fee494849277312c45 Mon Sep 17 00:00:00 2001 From: Pedro Rodrigues Date: Sun, 16 Apr 2017 17:34:47 +0100 Subject: [PATCH 03/10] add codestyle and python 3.6 environments to tox config --- tox.ini | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 9228186..d114513 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,11 @@ [tox] -envlist = py{26,27,33,34,35,py,py3} +envlist = py{26,27,33,34,35,36,py,py3},codestyle skip_missing_interpreters = True [testenv] deps = pytest commands = py.test + +[testenv:codestyle] +deps = pycodestyle +commands = pycodestyle From 15b90c34768840e15ba27677388c283b61ec26c3 Mon Sep 17 00:00:00 2001 From: Pedro Rodrigues Date: Sun, 16 Apr 2017 17:35:34 +0100 Subject: [PATCH 04/10] fix simple codestyle warnings --- fn/iters.py | 2 ++ fn/op.py | 1 + fn/underscore.py | 2 ++ tests/test_finger_tree.py | 1 + 4 files changed, 6 insertions(+) diff --git a/fn/iters.py b/fn/iters.py index 67558cf..e6336b2 100644 --- a/fn/iters.py +++ b/fn/iters.py @@ -58,6 +58,7 @@ def first_true(iterable, default=False, pred=None): """ return next(filter(pred, iterable), default) + # widely-spreaded shortcuts to get first item, all but first item, # second item, and first item of first item from iterator respectively head = first = partial(flip(nth), 0) @@ -251,6 +252,7 @@ def flatten(items): else: yield item + if version_info[0] == 3 and version_info[1] >= 3: from itertools import accumulate else: diff --git a/fn/op.py b/fn/op.py index 12f4b6d..93fdacd 100644 --- a/fn/op.py +++ b/fn/op.py @@ -8,6 +8,7 @@ def _apply(f, args=None, kwargs=None): return f(*(args or []), **(kwargs or {})) + apply = apply if version_info[0] == 2 else _apply diff --git a/fn/underscore.py b/fn/underscore.py index 2f34154..e602525 100644 --- a/fn/underscore.py +++ b/fn/underscore.py @@ -9,6 +9,7 @@ from .op import apply, flip, identity from .uniform import map, zip + div = operator.div if version_info[0] == 2 else operator.truediv letters = string.letters if version_info[0] == 2 else string.ascii_letters @@ -189,4 +190,5 @@ def __call__(self, *args): __ror__ = fmap(flip(operator.or_), "other | self") __rxor__ = fmap(flip(operator.xor), "other ^ self") + shortcut = _Callable() diff --git a/tests/test_finger_tree.py b/tests/test_finger_tree.py index 20e29e5..f9040f4 100644 --- a/tests/test_finger_tree.py +++ b/tests/test_finger_tree.py @@ -41,5 +41,6 @@ def test_iterator(self): sum(Deque.from_iterable(range(1, 20))) ) + if __name__ == '__main__': unittest.main() From 2f4db5d3e2f4b80d4ca675fd265c634c4802fb69 Mon Sep 17 00:00:00 2001 From: Pedro Rodrigues Date: Sun, 16 Apr 2017 17:36:22 +0100 Subject: [PATCH 05/10] fix short variable name warnings --- fn/immutable/list.py | 7 +++---- tests/test_iterators.py | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/fn/immutable/list.py b/fn/immutable/list.py index 122f3a1..1aa5eca 100644 --- a/fn/immutable/list.py +++ b/fn/immutable/list.py @@ -47,10 +47,9 @@ def __radd__(self, el): return self.cons(el) def __iter__(self): - l = self - while l: - yield l.head - l = l.tail + while self: + yield self.head + self = self.tail def __len__(self): return self._count diff --git a/tests/test_iterators.py b/tests/test_iterators.py index 42518db..9e71d1d 100644 --- a/tests/test_iterators.py +++ b/tests/test_iterators.py @@ -280,5 +280,5 @@ def test_accumulate(self): ) def test_filterfalse(self): - l = iters.filterfalse(lambda x: x > 10, [1, 2, 3, 11, 12]) - self.assertEqual([1, 2, 3], list(l)) + filtered = iters.filterfalse(lambda x: x > 10, [1, 2, 3, 11, 12]) + self.assertEqual([1, 2, 3], list(filtered)) From b8df370265d9a312254300a9b8f9f2926597e8c0 Mon Sep 17 00:00:00 2001 From: Pedro Rodrigues Date: Sun, 16 Apr 2017 17:50:07 +0100 Subject: [PATCH 06/10] =?UTF-8?q?In=20Python=20there=20is=20no=20magic=20m?= =?UTF-8?q?ethod=20to=20override=20the=20=E2=80=9Cis=E2=80=9D=20operators?= =?UTF-8?q?=20behaviour.=20https://docs.python.org/3/reference/expressions?= =?UTF-8?q?.html#is?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_underscore.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_underscore.py b/tests/test_underscore.py index 40bff48..f263393 100644 --- a/tests/test_underscore.py +++ b/tests/test_underscore.py @@ -146,8 +146,7 @@ def test_comparator(self): self.assertFalse((_ == 10)(9)) def test_none(self): - # FIXME: turning the '==' into 'is' throws an error - self.assertTrue((_ == None)(None)) + self.assertTrue((_ == None)(None)) # noqa: E711 class pushlist(list): def __lshift__(self, item): From eb9c9f582aeb0e68c287bc8dcb0696eb3dc9b817 Mon Sep 17 00:00:00 2001 From: Pedro Rodrigues Date: Sun, 16 Apr 2017 17:56:39 +0100 Subject: [PATCH 07/10] adds codestyle and python 3.6 to travis --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index f867760..016c318 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,11 +2,13 @@ sudo: false language: python python: 3.5 env: + - TOXENV=codestyle - TOXENV=py26 - TOXENV=py27 - TOXENV=py33 - TOXENV=py34 - TOXENV=py35 + - TOXENV=py36 - TOXENV=pypy - TOXENV=pypy3 install: pip install tox From 65c774c274181d3a986f2d50836250cb1a0f7224 Mon Sep 17 00:00:00 2001 From: Pedro Rodrigues Date: Sun, 16 Apr 2017 18:24:16 +0100 Subject: [PATCH 08/10] Move python 3.6 and pypy3 environments to their own travis image. Python 3.6 failed silently: https://travis-ci.org/medecau/fn.py/jobs/222589293#L150 Python 3.6 is not available in python 3.5 base image and 3.5 is not available in the 3.6 image. Pypy3 failed catastrophically The version of pypy3 currently provided by travis is old and is no longer supported by the most recent version of pip. The solution, for now, is to use the pypy3 image. Related issues: https://github.com/travis-ci/travis-ci/issues/4794 https://github.com/travis-ci/travis-ci/issues/4990 https://github.com/travis-ci/travis-ci/issues/7146 --- .travis.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 016c318..97a1594 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,11 +8,15 @@ env: - TOXENV=py33 - TOXENV=py34 - TOXENV=py35 - - TOXENV=py36 - TOXENV=pypy - - TOXENV=pypy3 install: pip install tox script: tox +matrix: + include: + - python: 3.6 + env: TOXENV=py36 + - python: pypy3 + env: TOXENV=pypy3 notifications: email: false From c90d16272e131c31b3687fbb6b873705a613519b Mon Sep 17 00:00:00 2001 From: James Skillen Date: Tue, 16 May 2017 14:12:43 +0100 Subject: [PATCH 09/10] stackless() bug fix for void functions. --- fn/recur.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/fn/recur.py b/fn/recur.py index f3daa83..97ce15d 100644 --- a/fn/recur.py +++ b/fn/recur.py @@ -96,7 +96,7 @@ def is_even(n): __slots__ = "func", - Thunk = namedtuple('Thunk', ("func", "args", "kwargs", "is_tailcall")) + Thunk = namedtuple("Thunk", ("func", "args", "kwargs", "is_tailcall")) def __init__(self, func): self.func = func @@ -110,6 +110,7 @@ def tailcall(self, *args, **kwargs): def __call__(self, *args, **kwargs): s = [self.func(*args, **kwargs)] r = [] + v = None while s: try: if r: @@ -129,4 +130,4 @@ def __call__(self, *args, **kwargs): s.append(g) else: r.append(v) - return r[0] + return r[0] if r else None From ae9f3eabd52d153b94d3ae0027b2aa9f5baece47 Mon Sep 17 00:00:00 2001 From: James Skillen Date: Tue, 16 May 2017 14:25:28 +0100 Subject: [PATCH 10/10] Add blank lines to fix tox error. --- fn/recur.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fn/recur.py b/fn/recur.py index 97ce15d..23bf3dd 100644 --- a/fn/recur.py +++ b/fn/recur.py @@ -2,6 +2,7 @@ from collections import namedtuple + class tco(object): """Provides a trampoline for functions that need one. @@ -46,6 +47,7 @@ def __call__(self, *args, **kwargs): action = act kwargs = result[2] if len(result) > 2 else {} + class stackless(object): """Provides a "stackless" (constant Python stack space) recursion decorator for generators.