【问题标题】:Does functional programming allow mutability of locally scoped objects?函数式编程是否允许局部范围对象的可变性?
【发布时间】:2015-02-05 15:45:16
【问题描述】:

我了解到对象的可变性是根据其状态而不是其身份来定义的。

下面的程序更改函数 hailstone() 内名称 count 引用的本地范围对象的状态。

def hailstone(n):
    count = 1
    """Print the terms of the 'hailstone sequence' from n to 1."""
    assert n > 0
    print(n)
    if n > 1:
        if n % 2 == 0:
            count += hailstone(n / 2)
        else:
            count += hailstone((n * 3) + 1)
    return count

if (__name__ == '__main__'):
    result = hailstone(10)
    print(result)

在阅读了this article 的以下段落后,我发现上面的代码在更改名称count 所指的本地范围对象的状态方面看起来不错(即count += hailstone(n / 2)):

忽略这一切。函数式代码的特点是一件事:没有副作用。它不依赖于当前函数之外的数据,也不会改变存在于当前函数之外的数据。所有其他“功能性”事物都可以从这个属性派生出来。学习时将其用作导绳。

那么,我怎么理解this answer这句话的意思:

在函数式编程中,改变变量的值是不合适的。

函数式编程是否允许在我的上述程序中更改名称 count 引用的本地范围对象的状态?

【问题讨论】:

  • 您的代码没有副作用。无法从函数外部判断是否使用count 实现;它是一个纯粹的局部变量。此外,Python 中的整数是不可变的。 count 引用的对象永远不会改变,扩充的赋值引用了一个同名的新对象。
  • @jonrsharpe 是的,每当您修改该对象的值 (>>> a = 2 >>> a += 2) 时,python 都会创建新的 inttype 对象。我将如何定义可变性?是对象的状态改变还是对象的身份改变?
  • @overexchange 参见例如stackoverflow.com/q/8056130/3001761
  • @overexchange 这不是一个合适的讨论;我不会在 cmets 中向你解释 Python 的整个对象模型。做一些研究!
  • 相关(关于程序员):programmers.stackexchange.com/q/196112/110531

标签: python language-agnostic functional-programming


【解决方案1】:

函数式编程没有明确的定义。然而,它通常意味着没有,或者至少是最小化,副作用——有时人们说纯函数式编程是为了强调它们完全没有。但是副作用是什么?

一个常见的定义是副作用是任何破坏引用透明度的东西。反过来,参照透明度可以用多种方式定义,但也许最有启发性的定义是 求值顺序 应该是无关紧要的。也就是说,如果您可以以任何顺序简化程序的任何子表达式,只需通过替换定义而不改变程序的结果,程序就是引用透明的(或)。特别是,您始终可以将变量替换为其(唯一!)定义。

显然,对可变变量的赋值打破了这种情况,因此必须将其视为副作用——即使它只是局部的。顺便说一句,同样是一个打印语句。

有一些方法可以在不破坏引用透明度的情况下拥有可变状态或 IO。这就是 monad 的神秘概念。不过这里就不多说了。

【讨论】:

    【解决方案2】:

    不了解 Python,我假设 count 是一个“本地”变量,从外部看不到,并且每次调用 hailstone 都会获得自己的 count 实例。如果是这样,那么正如@jonrsharpe 解释的那样,您对count 所做的事情无关紧要。

    但是请注意,您的代码是不必要的冗长。为什么不简单:

    if n > 1:
        if n % 2 == 0:
            return 1 + hailstone(n/2)
        else:
            return 1 + hailstone(n*3+1)
    else:
        return 1
    

    原来这个变量根本不需要,更不用说需要更新了。

    【讨论】:

      【解决方案3】:

      您的函数在函数级别没有副作用(除了print)。这是被称为功能性的必要条件,但不是充分条件。函数式程序由一个大表达式组成。您的示例是函数级别的表达式,在函数体中穿插着命令式语句。没有elseif 是一个语句,变量重新分配也是。

      这并不是说没有副作用的命令式函数并不比有副作用的命令式函数好。您的示例当然有其优势,即使它不被视为纯粹的功能。

      【讨论】:

        【解决方案4】:

        这个程序变异了x

        x = 2
        x = x * 3
        x = x + 4
        return x
        

        在函数式编程中,您不使用突变。您的代码应该只定义每个变量一次:

        x1 = 2
        x2 = x1 * 3
        x3 = x2 + 4
        return x3
        

        编译器可能会为 x1、x2 和 x3 分配相同的内存位置,并就地变异,就像原始程序一样。不过,这是一个实现细节,应该不重要。

        与之前的顺序程序不同,您可以将新程序视为一个方程组并以任意顺序读取它。在这种情况下,由于数据依赖性,计算仍然必须从上到下进行,但通常不必这样做。

        您可以通过使用函数完全摆脱“=”符号。为了摆脱“x1 = 2”,将剩余的行参数化为 x1 并将 2 传递给函数:

        (function(x1) {
            x2 = x1 * 3
            x3 = x2 + 4
            return x3
        })(2)
        

        并继续此过程以删除“x2 = x1 * 3”和“x3 = x2 * 4”。

        有时您在循环中分配给一个变量以累积一些东西(例如,在数组中添加项目for x in A: sum += x)。在这种情况下,您可以重写程序以使用诸如“sum”、“reduce”、“map”、“filter”之类的函数来替代重复赋值,这些函数是循环的高级抽象。


        我在撒谎。只要可变性不会“泄漏”到范围之外,就可以在纯函数程序中具有局部可变状态。在 Haskell 中,为此目的有一个名为“ST”的库。它嵌入了一种带有命令“创建变量”、“读取变量”、“写入变量”的小型命令式语言,如果您的程序没有泄漏可变变量,您可以在纯函数中检索其结果。但是,它使用起来更尴尬(参见this answer),并且不像在命令式语言中使用赋值那样频繁。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2018-12-30
          • 1970-01-01
          • 1970-01-01
          • 2018-07-16
          • 2013-07-13
          • 1970-01-01
          • 2011-09-14
          相关资源
          最近更新 更多