7

Is there a built in function or standard library function roughly equivalent to

def recur_until(start, step_fu, stop_predicate=lambda _: False):
    current = start
    while not stop_predicate(current):
        yield current
        current = step_fu(current)

or

def recur_while(start, step_fu, predicate=lambda _: True):
    current = start
    while predicate(current):
        yield current
        current = step_fu(current)

or even just

def recur(start, step_fu):
    current = start
    while True:
        yield current
        current = step_fu(current)

in any version of Python? (The latter is as good as the other two when combined with itertools.takewhile.)

A generator function like these would allow to compute certain recursively defined sequences iteratively, namely first-order recurrence relations.

While these aren't too hard to implement when needed, I feel like something like them should be part of itertools or maybe functools, but if it is, I haven't been able to spot it in the documentation, yet.


Usage examples:

list(recur_until(2, lambda x: x**2 - 1, lambda x: x > 1e4))
# [2, 3, 8, 63, 3968]

Should also work with non-number elements:

list(recur_until('', lambda x: '[{}]({})'.format(x, len(x)), lambda x: len(x) > 30))
# ['',
#  '[](0)',
#  '[[](0)](5)',
#  '[[[](0)](5)](10)',
#  '[[[[](0)](5)](10)](16)',
#  '[[[[[](0)](5)](10)](16)](22)']
das-g
  • 9,718
  • 4
  • 38
  • 80
  • I.e., an equivalent to Haskell's `iterate` function. `iterate (\x -> x + 1) 0 = 0, 1, 2, 3, 4, 5, ...` – chepner Feb 26 '16 at 16:52
  • 1
    @chepner `iterate` in combination with `takeWhile`. – Sven Marnach Feb 26 '16 at 16:55
  • 4
    I never know what to do with questions which are just "does X exist?" and I'm confident the answer is "No". There's nothing I can do to justify that answer other than link to the documentation you've already linked. – DSM Feb 26 '16 at 16:55
  • @SvenMarnach True. I was focusing on the part missing from itertools, but you do need the combination of `iterate` and `{take,drop}while`. – chepner Feb 26 '16 at 17:07
  • @DSM, yeah, I know existence questions have the problem that negative answers are essentially unprovable. But as I'm particularly interested in what the standard library has to offer here, I didn't know how to put my question differently. – das-g Feb 26 '16 at 17:15
  • @SvenMarnach: Why did you remove your answer? I didn't have time to test it before it disappeared, but it did look promising. – das-g Feb 26 '16 at 17:24
  • I removed it because it was wrong. Unfixably wrong. – Sven Marnach Feb 26 '16 at 17:26

3 Answers3

5

In Python 3.3+, the new itertools.accumulate can be used to this purpose in combination with the other itertools

For example:

>>> from itertools import accumulate, repeat, takewhile
>>> fun = accumulate(range(2, 10), lambda x, _: x**2 - 1)
>>> list(fun)
[2, 3, 8, 63, 3968, 15745023, 247905749270528, 61457260521381894004129398783]
>>> fun = takewhile(lambda y: y < 1e4, accumulate(repeat(2), lambda x, _: x**2 - 1))
>>> list(fun)
[2, 3, 8, 63, 3968]

accumulate takes a sequence and a function with 2 arguments: The first is the accumulate value and the second is the next value in the sequence. In this case, we only need the first argument, which will be the first element of the sequence passed to accumulate for the first call of the passed function and the return value of that function for subsequent calls.

Thus, we only need the start of the passed sequence to be our initial value — 2 in this case. The content of the rest of the sequence is irrelevant, but we can use it's length to control how many elements we want (as in the first example) or to create an infinite generator (like the second example).

das-g
  • 9,718
  • 4
  • 38
  • 80
Copperfield
  • 8,131
  • 3
  • 23
  • 29
  • Heh, I had just started my other computer which has Python 3.4 to test something similar to this shortly before I saw your answer. – das-g Feb 26 '16 at 19:57
4

The missing link is that you need something to convert your recursive stepping function into a generator. Once you have that, then you can use any of the itertools methods.

def recur_to_gen(step_fu, current, sentinel=None):
    while current != sentinel:
        yield current
        current = step_fu(current)


matches = itertools.takewhile(predicate, recur_to_gen(step_fu, start))

recur_to_gen is probably a reasonable thing to propose adding to itertools.

PaulMcG
  • 62,419
  • 16
  • 94
  • 130
  • It's usually called `iterate`, e.g. in Haskell or in this [functional Python library](https://github.com/kachayev/fn.py/blob/master/fn/iters.py#L81). – Sven Marnach Feb 26 '16 at 16:56
  • 1
    Unfortunately, I think "iterate" in Python is pretty closely tied to `iter`, which is more about stepping thru a sequence, rather than recursively applying a stepping function to an item to get the next item. But by whatever name, I think `itertools` would be a good home for something like this. – PaulMcG Feb 26 '16 at 17:00
  • Where is the generator expression? – Stefan Pochmann Feb 26 '16 at 17:03
  • 1
    @StefanPochmann The function `recur_to_gen()` is a generator function (since it `yield`s). There is no generator expression here. – Sven Marnach Feb 26 '16 at 17:04
  • 2
    @SvenMarnach: I think SP was drawing attention to PM's first sentence-- the OP's original function (though called `recur`) is non-recursive, and already a genfunc. – DSM Feb 26 '16 at 17:05
  • Sorry, I mentally equate a call to a generator with a generator expression, but that is sloppy terminology on my part. @SvenMarnach is correct. – PaulMcG Feb 26 '16 at 17:07
  • Edited, hopefully for the better in terminological precision. – PaulMcG Feb 26 '16 at 17:08
  • @PaulMcGuire Yes, that's what I meant and it's fixed now. Now I just still think the OP's original is better, so I disagree with the "need" to do this :-) – Stefan Pochmann Feb 26 '16 at 17:11
  • 1
    @DSM - not recursive in the sense that it calls itself, but in that it iterates through a not-necessarily-contiguous chain of items by calling a stepping function on the current item in order to get the next. – PaulMcG Feb 26 '16 at 17:13
  • @StefanPochmann - I like the separation of the "give me the sequence of items found by `next_item = step_function(current_item)`" from the predicate testing (as the OP has combined into one). With `recur_to_gen` or `iterate` or `recur`, you convert this chain of items into a sequence that could then be used for takewhile'ing, groupby'ing, or any other `itertool` that works with an iterator. – PaulMcG Feb 26 '16 at 17:15
  • I'll edit my question to also allow accepting no predicate, as that's indeed covered by `itertools.takewhile`. – das-g Feb 26 '16 at 17:19
  • Concerning the function name: I've chosen `recur` in the question, to reflect that the function implements a sequence produced by a **recur**sively defined **recur**rence, even if the implementation is iterative, not recursive. Though, as the English verb 'recur' exists and has a somewhat different meaning, I don't think that name can be used for proposing a function like this to be included in `itertools`. – das-g Feb 26 '16 at 19:53
3

The functional package provides the pieces to simulate this.

from functional import dropWhile, iterate    
recur = dropWhile(predicate, iterate(step_func, start))

For example,

>>> next(dropWhile(lambda x : x < 10, iterate(lambda x: x + 1, 2)))
10

(dropWhile isn't really any different from itertools.dropwhile.)

chepner
  • 497,756
  • 71
  • 530
  • 681
  • 1
    The latest available build is for Python 2.5. A newer functional package for Python: https://github.com/kachayev/fn.py – Sven Marnach Feb 26 '16 at 17:07
  • 1
    It still works (installed via `pip`, but I didn't test it extensively) but yes, I think `functional` was a one-off proof-of-concept that was never really supported. – chepner Feb 26 '16 at 17:09