具有副作用的封闭器是否被视为“功能样式”?


9

许多现代编程语言都支持某种闭包概念,即一段代码(一个块或一个函数)

  1. 可以被视为一个值,因此可以存储在变量中,传递给代码的不同部分,可以在一个程序的一部分中定义,并可以在同一程序的完全不同的部分中调用。
  2. 可以从定义它的上下文中捕获变量,并在以后调用它时访问它们(可能在完全不同的上下文中)。

这是用Scala编写的闭包示例:

def filterList(xs: List[Int], lowerBound: Int): List[Int] =
  xs.filter(x => x >= lowerBound)

函数文字x => x >= lowerBound包含free变量lowerBound,该变量由filterList具有相同名称的函数的参数关闭(绑定)。该闭包将传递给library方法filter,该方法可以作为常规函数重复调用它。

我已经在该站点上阅读了很多问题和答案,据我所知,术语闭包通常会自动与函数式编程和函数式编程风格相关联。

维基百科上的函数编程的定义为:

在计算机科学中,函数式编程是一种编程范例,将计算视为对数学函数的评估,并避免了状态数据和可变数据。与强调状态变化的命令式编程风格相反,它强调函数的应用。

并进一步

在函数代码中,函数的输出值仅取决于输入到函数的参数。消除副作用可以使理解和预测程序的行为变得更加容易,这是开发函数式编程的主要动机之一。

另一方面,编程语言提供的许多闭包构造允许闭包捕获非局部变量并在调用闭包时对其进行更改,从而对定义它们的环境产生副作用。

在这种情况下,闭包实现了函数编程的第一个想法(函数是可以像其他值一样移动的一流实体),却忽略了第二个想法(避免了副作用)。

带有副作用闭包的这种使用是否被视为功能样式,还是被视为可以用于功能性和非功能性编程样式的更通用的构造?是否有关于该主题的文献?

重要的提示

我不是在质疑副作用或关闭副作用的有用性。另外,我对讨论带或不带副作用的闭合的优缺点没有兴趣。

我只想知道函数编程的支持者是否仍将使用此类闭包视为函数样式,或者相反,在使用函数样式时不鼓励使用此类闭包。


3
在纯粹的功能样式/语言中,副作用是不可能的...所以我想这是一个问题:在什么纯度的水平上,人们认为代码可以起作用?
史蒂文·埃弗斯

@SnOrfus:100%?
乔治

1
在纯功能性语言中,副作用是不可能的,因此应该回答您的问题。
史蒂文·埃弗斯

@SnOrfus:在许多语言中,闭包被认为是一种函数式编程构造,因此我有些困惑。在我看来(1)它们的用法比仅使用FP更通用,并且(2)使用闭包并不能自动保证使用的是一种功能样式。
乔治

下降投票者能否就如何改善这个问题留下笔记?
Giorgio

Answers:


7

没有; 功能范式的定义是关于状态的缺乏和隐含的副作用。它与高阶函数,闭包,语言支持的列表操作或其他语言功能无关...

函数式编程的名称来自函数的数学概念 - 对相同输入的重复调用始终会提供相同的输出 -无效函数。仅当数据不可变时才能实现。为了简化开发,这些函数变得可变(函数改变,数据仍然是不变的),因此,高阶函数(例如数学中的函数,例如派生函数)的概念成为一个函数,该函数将另一个函数作为输入。为了使函数可以随身携带和传递为参数,采用了一流的函数;之后,为进一步提高生产率,出现了瓶盖

当然,这是一个非常简化的视图。


3
高阶函数绝对与可变性无关,一等函数也无关。我可以轻松地在Haskell中传递函数并从中构造其他函数,而不会产生任何可变状态或副作用。
tdammers

@tdammers我可能不太清楚表达;当我说函数变得可变时,我并不是指数据可变性,而是指可以更改函数的行为(通过高阶函数)的事实。
m3th0dman 2012年

高阶函数不必更改现有函数;大多数教科书示例都现有功能合并为新功能,而无需对其进行任何修改- map例如take ,它采用一个功能,将其应用到列表中,然后返回结果列表。map不会修改其任何参数,也不会改变它作为参数使用的函数的行为,但是它绝对是一个高阶函数-如果仅使用function参数部分应用它,则您已经构造了一个在列表上运行的新功能,但仍未发生任何突变。
tdammers

@tdammers:确实:可变性仅在命令式或多范例语言中使用。即使它们可以具有高阶函数(或方法)和闭包的概念,但它们会放弃不变性。这可能很有用(我并不是说不应该这样做),但是我的问题是,您是否仍然可以调用此功能。通常,这意味着关闭并不是严格意义上的功能概念。
乔治

@乔治:大多数经典的FP语言确实具有可变性;Haskell是我唯一想到的完全不允许变异的工具。尽管如此,避免可变状态仍然是FP中的重要价值。
tdammers

9

不可以。“函数式”意味着无副作用的编程。

要了解原因,请查看Eric Lippert的博客中有关ForEach<T>扩展方法的条目,以及为什么Microsoft在Linq中未包括类似的序列方法:

我出于两个原因在哲学上反对提供这种方法。

第一个原因是这样做违反了所有其他序列运算符所基于的功能编程原理。显然,调用此方法的唯一目的是引起副作用。表达式的目的是计算一个值,而不引起副作用。声明的目的是引起副作用。这个东西的调用位置看起来很像一个表达式(尽管,诚然,由于该方法是空返回的,所以该表达式只能在“语句表达式”上下文中使用。)使其成为唯一且仅对副作用有用的序列运算符。

第二个原因是这样做会为语言增加零新的表示能力。这样做使您可以重写以下完全清晰的代码:

foreach(Foo foo in foos){ statement involving foo; }

变成这段代码:

foos.ForEach((Foo foo)=>{ statement involving foo; });

它使用几乎完全相同的字符,但顺序略有不同。但是第二个版本更难理解,更难调试,并且引入了闭包语义,从而有可能以微妙的方式改变对象的生存期。


1
尽管,我同意他的看法ParallelQuery<T>.ForAll(...)。这样的实现IEnumerable<T>.ForEach(...)对于调试ForAll语句非常有用(替换为ForAllwith ForEach并删除with ,AsParallel()您可以更轻松地逐步执行/调试它)
Steven Evers 2012年

显然,乔·达菲有不同的想法。:D
罗伯特·哈维

Scala也不强制执行纯度:您可以将非纯闭包传递给高阶函数。我的印象是,闭包的想法并非特定于函数式编程,而是更通用的想法。
乔治

5
@Giorgio:闭包不必纯粹,仍然可以视为闭包。但是,它们必须是纯净的,才能被视为“功能样式”。
罗伯特·哈维

3
@Giorgio:能够围绕易变的状态和副作用进行闭合并将其传递给另一个功能非常有用。这与功能编程的目标背道而驰。我认为,很多混乱来自于功能语言中常见的lambda的优雅语言支持。
GlenPeterson,2012年

0

函数式编程可以肯定将一流的函数带入下一个概念层次,但是声明匿名函数或将函数传递给其他函数不一定是函数式编程。在C语言中,一切都是整数。一个数字,一个指向数据的指针,一个指向函数的指针...全部都是整数。您可以将函数指针传递给其他函数,列出函数指针...哎呀,如果您使用汇编语言,函数实际上只是存储机器指令块的内存中的地址。给函数起个名字对于需要编译器编写代码的人来说是额外的开销。因此,在完全没有功能的语言中,功能在这种意义上是“一流的”。

如果您唯一要做的就是在REPL中计算数学公式,那么您在语言上就可以纯净。但是大多数业务编程都有副作用。在等待长时间运行的程序完成时亏钱是一个副作用。采取任何外部操作:写入文件,更新数据库,按顺序记录事件等,都需要更改状态。如果您将这些动作封装在不可变的包装程序中,从而避免产生副作用,那么我们就可以争论状态是否真的改变了,从而使您的代码不必担心它们。但这就像在讨论一棵树如果掉入森林而没有人听到的声音是否会发出声音。事实是这棵树是直立的,然后倒在地上。完成工作后状态会发生变化,即使只是报告完成工作也是如此。

因此,我们剩下的是功能纯度的标度,不是黑白的,而是灰色的。在这种规模上,副作用越少,可变性越少,效果越好(功能更强)。

如果您在其他功能代码中绝对需要副作用或可变状态,则请尽力将其封装到程序的其余部分中。使用闭包(或其他方法)将副作用或可变状态注入原本应该是纯函数的函数编程的对立面。唯一的例外可能是,如果关闭是封装传递给它的代码的副作用的最有效方法,则可能是这样。它仍然不是“函数式编程”,但它可能是您在某些情况下可以获得的壁橱。

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.