How to write foldr (right fold) generator in Python?

戏子无情 提交于 2021-01-03 01:09:08

问题


Python's reduce is a left-fold, which means it is tail-recursive and its uses can be neatly rewritten as a loop. However, Python does not have a built-in function for doing right folds. Since right-folds are most naturally written with recursion (and Python doesn't like recursion as much as functional languages), I'm interested in writing a right fold (foldr) in terms of a generator.

How can this be done? And very specifically, how can it be done in Python 2.7?

EDIT: I should have mentioned that one of the benefits to foldr is that you can sometimes fold on infinite lists without risk of eating your stack alive. I would like to see answers that preserve this property.

For example, Haskell's foldr is lazy on both input and output and can allow for short-circuiting "step" functions to work on long/infinite inputs:

foldr (&&) True (repeat False)  -- gives False

Any Python variant that uses list/reversed/etc. on the input will hang if given itertools.repeat(some_value).

Note that Python's reduce chokes in the same example because of strictness:

reduce(lambda x, y: x and y, itertools.repeat(False), True) # hangs

回答1:


So a simple generator in python (without appropriate error checking):

def foldr(op, lst):
    l, x = reversed(list(lst)), None
    for i in l:
        if not x:
            x = i
            continue
        x = op(x, i)
        yield x

e.g.:

>>> from operator import mul
>>> for i in foldr(mul, [1,2,3,4]):
...     print i
24
24
12

Almost identical to the 'roughly equivalent' implementation of reduce in the documentation:

def foldr(function, iterable, initializer=None):
    it = reversed(list(iterable))
    if initializer is None:
        try:
            initializer = next(it)
        except StopIteration:
            raise TypeError('foldr() of empty sequence with no initial value')
    accum_value = initializer
    for x in it:
        accum_value = function(accum_value, x)
        yield accum_value

[Edit] So purely as an exercise of the mind and with very little practical value, it is possible to defer as long as there is some cooperation between the function that you a folding over... e.g.:

class Defer(object):
    def __init__(self, func, *args):
        self.func = func
        self.args = args
    def __bool__(self):
        return self.func(*self.args)
    def __int__(self):
        return self.func(*self.args)

def foldr(function, iterable, initializer):
    it = iter(iterable)
    try:
        return function(next(it), Defer(foldr, function, it, initializer))
    except StopIteration:
        return initializer

Then as long as the function converts to the right type you can defer the calculation, however this will not work with native operators, so not sure how useful this really is:

>>> print(foldr(lambda a, b: int(a)*int(b), [1,2,3,4], 1))
24

Defining a forever generator:

from itertools import repeat
def forever():
    yield False
    yield True
    for i in repeat(False):
        yield i

Folding or across an infinite list, returns when it finds a True

>>> print(foldr(lambda a, b: bool(a) or bool(b), forever(), False))
True



回答2:


You will have to catch appropriate exceptions but should be an idea of how to do it iteratively:

def foldr(a, b, l):
    if isinstance(l, Iterator):
        it = reversed(list(l))
    else:
        it = reversed(l)
    try:
       nxt = next(it)
    except StopIteration:
        return 
    c = a(nxt, b)  
    stop = object() 
    while nxt is not stop:
        yield c
        nxt = next(it, stop)
        c = a(nxt, c) if nxt is not stop else c



from operator import truediv
for c in (foldr(truediv, 1, [1, 2, 3, 4, 5, 6, 7, 8])):
    print(c)



回答3:


If you are going to define a function using generators, why not use the following?

def foldr(op, lst):
    return reduce(op, reversed(lst))



回答4:


I think something like this is what you want:

def foldr(fn, seq, init):
    it = iter(seq)
    try:
        x = next(it)
    except StopIteration:
        try:
            for elem in init:
                yield elem
        except TypeError:
            yield init
    else:
        try:
            for elem in fn(x, foldr(fn, it, init)):
                yield elem
        except TypeError:
            yield fn(x, foldr(fn, it, init))

It's not exactly production-ready since it will hit the Python stack limit pretty quickly and it will be surprising in the presence of side-effecting functions due to the double call to fn, but it should be enough to give you an idea.



来源:https://stackoverflow.com/questions/29372983/how-to-write-foldr-right-fold-generator-in-python

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!