函数式编程中的合并函数

6

最近,我在阅读Python的“函数式编程指南”时,发现了一个名为test_generators.py的标准模块,在其中找到了以下生成器:

# conjoin is a simple backtracking generator, named in honor of Icon's
# "conjunction" control structure.  Pass a list of no-argument functions
# that return iterable objects.  Easiest to explain by example:  assume the
# function list [x, y, z] is passed.  Then conjoin acts like:
#
# def g():
#     values = [None] * 3
#     for values[0] in x():
#         for values[1] in y():
#             for values[2] in z():
#                 yield values
#
# So some 3-lists of values *may* be generated, each time we successfully
# get into the innermost loop.  If an iterator fails (is exhausted) before
# then, it "backtracks" to get the next value from the nearest enclosing
# iterator (the one "to the left"), and starts all over again at the next
# slot (pumps a fresh iterator).  Of course this is most useful when the
# iterators have side-effects, so that which values *can* be generated at
# each slot depend on the values iterated at previous slots.

def simple_conjoin(gs):

    values = [None] * len(gs)

    def gen(i):
        if i >= len(gs):
            yield values
        else:
            for values[i] in gs[i]():
                for x in gen(i+1):
                    yield x

    for x in gen(0):
        yield x

我花了一些时间才理解它的工作原理。它使用可变列表values来存储迭代器产生的结果,并且第N+1个迭代器返回values,这个值通过整个迭代器链传递。
当我在阅读有关函数式编程的内容时,我偶然发现了这段代码,开始思考是否可以使用函数式编程(使用itertools模块中的函数)重写这个conjoin生成器。有很多以函数式风格编写的例程(只需看一下“Recipes”部分的结尾即可)。
但不幸的是,我没有找到任何解决方案。
那么,是否可能仅使用itertools模块就用函数式编程编写这个conjoin生成器呢?
谢谢

似乎将函数式风格与期望的副作用混合在一起是违反直觉的。如果这些函数是纯的,每个函数只需要被调用一次。@Ionuț:它不像mapzip。它更接近笛卡尔积,但混入了函数应用的变化。 - recursive
1
如果你想要没有副作用的懒惰函数式编程,那么你需要使用 itertools.product。如果你想要副作用,那么你就必须自己动手实现。两者不能兼得。 - Katriel
所以,如果我理解正确的话,他们使用可变的“values”列表编写了相当复杂的代码,只是为了能够访问所有左侧封闭迭代器生成的值。如果我们不需要任何副作用,我们可以使用“itertools.product”来获得相同的功能。 - ovgolovin
@revursive,是的,你说得对。我在提交评论后注意到了这一点。 - Ionuț G. Stan
2个回答

3

这种方法可行,而且仍然是懒惰的:

def conjoin(gs):
    return [()] if not gs else (
        (val,) + suffix for val in gs[0]() for suffix in conjoin(gs[1:])
    )

def range3():
    return range(3)

print list(conjoin([range3, range3]))

输出:

[(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2), (2, 0), (2, 1), (2, 2)]

演示可变状态的示例用法:

x = ""
def mutablerange():
    global x
    x += "x"
    return [x + str(i) for i in range(3)]

print list(conjoin([range3, mutablerange]))

输出:(注意“x”数量逐渐增加)

[(0, 'x0'), (0, 'x1'), (0, 'x2'), (1, 'xx0'), (1, 'xx1'), (1, 'xx2'), (2, 'xxx0'), (2, 'xxx1'), (2, 'xxx2')]

如果我们使用 itertools.product

x = ""
print list(itertools.product(range3(), mutablerange()))

结果如下:
[(0, 'x0'), (0, 'x1'), (0, 'x2'), (1, 'x0'), (1, 'x1'), (1, 'x2'), (2, 'x0'), (2, 'x1'), (2, 'x2')]

因此,可以清楚地看到,itertools.product会缓存迭代器返回的值。

我必须承认,起初我并不理解我在问题帖子中提到的联合函数之所以引人注目,是因为它可以使用封闭生成器的数据。然后@katrielalex写了关于itertools.product的内容,我记得它做的事情与simple_conjoin相同。一开始我有点震惊于simple_conjoin的复杂性(所有这些可变值的用法以及对所有嵌套生成器传递数据的理解不足),所以我无法想出任何东西。 - ovgolovin
2
values 的可变性并不是核心问题。问题在于函数会改变其他东西(任何东西),这些东西在函数运行期间保持不变,因此它们的下一次运行可能会受到上一次运行的影响。这就是为什么无法创建一个真正有用的函数实现,同时也是函数式递归的其中之一(要么是一个,要么是另一个,不能同时兼备)-- 函数必须具有副作用(不是纯函数),才能与 simple_conjoin 一起使用。没有副作用,它只是一个笛卡尔积,正如其他人所提到的那样。 - agf
1
使用 itertools.product,每个函数仅迭代一次。而使用 simple_conjoin,内部循环发生的次数取决于何时外部循环用尽,因此函数可能被迭代多次。当然,您可以在 itertools.product 中使用具有副作用的函数,但它们仍然只会被迭代一次,所以您无法获得例如 (0, 0), (0, 1), (1, 10), (5, 11),因为值被重复使用,所以如果您从第二个迭代器中获取 0 然后是 1,则第二列中始终会有 01 - agf
所以,正如我在帖子中所说的那样,答案是你不能拥有一个既有用又功能性的“simple_conjoin”,因为它只是它的副作用使它特殊。 - agf
1
@ovgolovin,奇怪的for values[0] in x():只是一个巧妙的累加器,不要被它迷惑。这与itertools.product的区别在于,itertools.product接受_生成器_并_缓存_将被多次使用的值,而conjoin接受_生成器函数_,它重新调用而不是使用缓存的值。 - senderle
显示剩余12条评论

2
simple_conjoin使用相同的基本构建块--循环,条件和yield--作为itertools配方的构建块。它还将函数视为数据,这是函数式编程的标志。

当然,在迭代器具有副作用时,这是最有用的,因此可以在每个位置迭代的值取决于先前位置上迭代的值。

然而,这与函数式编程的工作方式相反。在函数式编程中,每个函数都接受输入并产生输出,并以其他方式与程序的其余部分交互。

simple_conjoin中,函数不需要输入,但具有副作用。这是它使用的核心。

因此,虽然你可以用函数式风格来编写它,但它在简单翻译中不会有用。

你需要找出一种方法来编写它,使它在没有副作用的情况下运行,然后才能生成真正的“函数式”实现。

注意:@recursive的答案很好,但如果range3具有副作用,则它不会真正成为函数式的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接