在函数式编程中,没有副作用的局部可变变量是否仍被视为“不良习惯”?


23

在一个仅在内部使用的函数中具有可变的局部变量(例如,该函数没有副作用,至少不是故意的)是否仍被视为“非函数”?

例如,在“使用Scala进行功能编程”课程样式检查中,将任何var用法视为错误

我的问题是,如果函数没有副作用,是否仍然不鼓励编写命令式代码?

例如,如果不更改输入,而不是对累加器模式使用尾递归,那么执行局部for循环并创建局部可变ListBuffer并将其添加到本地怎么了?

如果答案是“是的,即使没有副作用,也总是劝阻他们”,那是什么原因呢?


3
我听说过的有关该主题的所有建议,劝告等都将共享可变状态称为复杂性的来源。该课程是否仅供初学者使用?然后,这可能是出于故意的过分简化。
Kilian Foth

3
@KilianFoth:共享的可变状态在多线程上下文中是一个问题,但是非共享的可变状态也可能导致程序也难以推理。
迈克尔·肖

1
我认为使用局部可变变量并不一定是坏习惯,但它不是“函数式风格”:我认为Scala课程(我在去年秋天参加)的目的是教您如何使用函数式风格进行编程。一旦可以清楚地区分功能性样式和命令式样式,就可以决定何时使用哪种样式(如果您的编程语言允许两者都使用)。var总是无功能的。Scala具有惰性val和尾部递归优化,可以完全避免var。
Giorgio

Answers:


17

毫无疑问,这里的一件事是宣称某事不是它的纯函数。

如果以真正完全独立的方式使用可变变量,则该函数在外部是纯净的,每个人都很高兴。实际上,Haskell 通过类型系统明确支持这一点,甚至可以确保可变引用不能在创建它们的函数之外使用。

就是说,我认为谈论“副作用”并不是看待它的最佳方法(这就是为什么我在上面说“纯”的原因)。在函数和外部状态之间建立依赖关系的任何事情都使事情难以推理,其中包括诸如了解当前时间或以非线程安全方式使用隐藏的可变状态之类的事情。


16

问题不在于可变性本身,而是缺乏参照透明性。

参照透明的事物和对其的引用必须始终相等,因此参照透明的函数将始终为给定的一组输入返回相同的结果,并且参照透明的“变量”实际上是一个值而不是变量,因为它不能改变。您可以创建一个内部透明变量的参照透明函数。那不是问题。但是,根据您正在执行的操作,可能很难保证该功能是参照透明的。

我可以想到一个例子,必须在哪里使用可变性来完成功能非常强大的事情:记忆化。记忆是从函数中缓存值,因此不必重新计算它们。它是参照透明的,但确实使用了变异。

但是总的来说,引用透明性和不变性是并存的,除了引用透明函数和备注中的局部可变变量外,我不确定还有其他例子不是这种情况。


4
关于记忆的观点非常好。请注意,Haskell强烈强调了编程的引用透明性,但是类似于备忘录的类似于备忘录的行为,包括语言运行时在幕后进行的大量突变。
CA McCann 2013年

@CA McCann:我想您说的很重要:在功能语言中,运行时可以使用变体来优化计算,但该语言中没有允许程序员使用变体的构造。另一个示例是一个带有循环变量的while循环:在Haskell中,您可以编写一个尾部递归函数,该函数可以使用可变变量来实现(避免使用堆栈),但是程序员看到的是从一个传递的不可变函数参数呼叫下一个。
Giorgio

@Michael Shaw:对于“ +1问题本身不是可变性,而是缺乏参照透明性”而+1。也许您可以引用具有唯一性类型的Clean语言:这些语言具有可变性,但仍可保证引用透明性。
Giorgio

@Giorgio:尽管我不时听到有关Clean的知识,但我对它一无所知。也许我应该调查一下。
Michael Shaw

@Michael Shaw:我对Clean不太了解,但是我知道它使用唯一性类型来确保引用透明。基本上,您可以修改数据对象,前提是修改后您没有对旧值的引用。IMO可以说明您的观点:引用透明性是最重要的一点,而不变性只是确保这一点的一种可能方法。
Giorgio

8

将其归结为“良好做法”与“不良做法”并不是很好。Scala支持可变值,因为它们比不变值(本质上是迭代的)要好得多,可以解决某些问题。

从角度来看,我非常确定,通过CanBuildFromscala提供的几乎所有不可变结构,都可以在内部进行某种形式的突变。关键是他们公开的内容是不可变的。保持尽可能多的不可变值有助于使程序更易于推理,减少出错的可能性。

这并不意味着当您遇到更适合变异的问题时,就不必在内部避免使用可变的结构和值。

考虑到这一点,可以使用Scala之类的语言提供的许多高阶函数(映射/过滤器/折叠)来更好地解决通常需要可变变量(例如循环)的许多问题。注意那些。


2
是的,使用Scala的集合时,我几乎永远不需要for循环。mapfilterfoldLeftforEach 做的伎俩大部分时间,但如果他们不这样做,能感觉到我“OK”,以恢复到蛮力势在必行代码是好的。(只要没有副作用,当然)
Eran Medan 2013年

3

除了线程安全性的潜在问题外,您通常还会丢失很多类型安全性。命令性循环的返回类型为Unit,几乎可以将任何表达式用作输入。高阶函数甚至递归都具有更精确的语义和类型。

与命令性循环相比,功能性容器处理还有更多选择。有了必要的,你基本上forwhile和在这两个像小的变化do...whileforeach

在函数中,您具有聚合,计数,过滤,查找,flatMap,fold,groupBy,lastIndexWhere,map,maxBy,minBy,分区,扫描,sortBy,sortWith,span和takeWhile,仅列举了一些Scala的常见问题标准库。当您习惯了可用的for循环时,相比之下命令式循环似乎太基本了。

使用局部可变性的唯一真正原因是偶尔会提高性能。


2

我会说大部分都还可以。而且,在某些情况下,以这种方式生成结构可能是提高性能的好方法。Clojure通过提供Transient数据结构解决了这个问题。

基本思想是允许在有限范围内进行局部突变,然后在返回结构之前冻结该结构。这样,用户仍然可以像对待纯代码一样对代码进行推理,但是您可以在需要时执行就地转换。

如链接所示:

如果一棵树落在树林中,它会发出声音吗?如果纯函数对某些本地数据进行了更改以产生不可变的返回值,那可以吗?


2

没有局部可变变量确实有一个优势-它使函数对线程更友好。

我被这样的局部变量(不在我的代码中,也没有源代码)烧死了,导致了低概率的数据损坏。没有一种或另一种方式提到线程安全,没有状态在调用之间持续存在,也没有副作用。在我看来,它并非不是线程安全的,在100,000个随机数据损坏中追逐1是一个皇家难题。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.