使局部使用可变性变得纯粹的函数吗?

问题描述 投票:0回答:1

我是 Scala、FP 和一般编程的初学者。我试图理解什么时候某些东西可以被称为正确的 FP。

如果我们说函数式编程是将函数链接在一起,以便 f(x) = x(对于一个输入和一个输出),那么如果您构建依赖于局部突变的函数,您是否可以说您仍在使用 fp,但其中相同的输入总是有相同的输出?

比较下面的 Scala 代码,它总是为 x <= 10, and x for all x > 10 生成 10。也就是说,该函数总是有一个可预测的结果,除非我构建它们的逻辑有缺陷:

def imp(x: Int): Int = {
  var y = x // in case x is an immutable value
  while (y < 10) {
    y += 1
  }
  return y
}

def tailRec(x: Int): Int = {
  if (x >= 10) return x
  else tailRec(x + 1)
}

val x = 2;
imp(x); //10
tailRec(x); //10

我想说我的命令函数没有副作用(它不会改变函数外部的状态)。我想说它在本地破坏了引用透明度,因为 y 不仅仅是值的名称,而且会随着时间的推移而变化。然而,如果我们从全局设置考虑命令式函数,这似乎并不重要,因为该函数总是给出与正确的纯尾递归函数相同的结果。如果你把函数看成一个黑盒子并且只关注 f(x)=x,那么这还是 FP 吗?

scala functional-programming mutation imperative
1个回答
0
投票

这些函数从定义上来说绝对是纯粹的。我将使用 Wikipedia 提供的一个,它说纯函数具有以下属性:

  1. 对于相同的参数,函数返回值是相同的(局部静态变量、非局部变量、可变引用参数或输入流没有变化,即引用透明度),并且
  2. 该函数没有副作用(局部静态变量、非局部变量、可变引用参数或输入/输出流不会发生变化)。

函数的内部状态相关的唯一情况是,如果所述状态是static,则比每个单独的函数调用寿命更长,并可能影响后续调用中的返回值(在 Scala 中,只有当方法发生变化时才应该发生这种情况)其类别的状态)。

关于将纯代码和非纯代码混合在一起,肯定很难做得这么好,但通过一些理解我认为这是可行的。我想说最好的方法是 Scala Collection API 本身,例如您可以在

List.map
方法的定义中看到的那样:公开一个纯 API 并在一个小的、受限范围,不会破坏引用透明度。这允许在没有任何干扰的情况下对纯代码和不纯代码进行分层。我想说,选择不纯粹方法的最佳理由是性能(但这应该始终由测量驱动——在大多数应用程序中,可以公平地说,专注于这方面是不经济的)并允许表达算法以更直接和可读的方式进行(这是选择不纯粹方法的一个很好的理由,只要你的抽象正确——这是相当困难的,但无论如何可能值得做)。

为了更深入,我认为看看编译器为这两种方法输出的内容很有趣:Scala 编译器能够检测尾递归并将其优化为

while
循环的结果,确保它也是堆栈-safe(您还可以确保编译器使用
@tailrec
注释强制函数是尾递归的)。我稍微调整了您的代码以使条件相同,这是编译器输出上
javap -v
的(已清理的)输出:

  public int imp(int);
    ...
    Code:
      stack=2, locals=3, args_size=2
         0: iload_1
         1: istore_2
         2: iload_2
         3: bipush        10
         5: if_icmpge     14
         8: iinc          2, 1
        11: goto          2
        14: iload_2
        15: ireturn
      ...

  public int tailRec(int);
    ...
    Code:
      stack=2, locals=2, args_size=2
         0: iload_1
         1: bipush        10
         3: if_icmpge     12
         6: iinc          1, 1
         9: goto          0
        12: iload_1
        13: ireturn
      ...

基本上,您可以观察到的唯一区别是,在递归版本中,您必须分配一个可变变量(因为它是 Scala,所以不可能改变参数)。如果在一个温暖的 JVM 上这个被 JIT 消除了,我不会感到惊讶。 ;-)

© www.soinside.com 2019 - 2024. All rights reserved.