在纯函数式编程中是否可能存在副作用?

35 浏览
0 Comments

在纯函数式编程中是否可能存在副作用?

我一直在努力理解函数式编程。我查阅了lambda演算、LISP、OCaml、F#甚至组合逻辑,但我面临的主要问题是:如何在不违背纯函数式编程的基本前提(即对于给定的输入,输出是确定的)的情况下完成需要副作用的操作,比如:\n

    \n

  • 与用户交互,
  • \n

  • 与远程服务通信,或者
  • \n

  • 使用随机抽样进行模拟处理
  • \n

\n希望我的问题表达清楚;如果不清楚,我欢迎任何帮助理解的尝试。提前感谢您的帮助。

0
0 Comments

在纯函数式编程中是否可能存在副作用的问题是因为与用户交互和与远程服务通信确实需要一些非函数式的部分来完成软件的功能。许多"函数式语言"(比如大多数Lisp)并不是完全函数式的。尽管在大多数情况下,它们还是允许你进行具有副作用的操作,但这样的操作在大多数情况下是"不鼓励"的。

Haskell是"纯函数式"的,但仍然允许你通过IO monad来执行非函数式的操作。基本思想是,你的纯函数式程序生成一个惰性数据结构,然后由一个非函数式的程序(你不需要编写,它是环境的一部分)对其进行求值。可以说,这个数据结构本身就是一个命令式程序。因此,在函数式语言中,你可以说是在进行命令式的元编程。

无论采用哪种方法,目标都是在程序中创建函数式部分与非函数式部分之间的分离,并尽可能地减小非函数式部分的规模。函数式部分往往更具可重用性、可测试性和易于推理。

0
0 Comments

纯函数式编程语言Haskell中,所有的函数都是纯函数,即对于相同的输入,它们总是产生相同的输出。但是在Haskell中如何处理副作用呢?这个问题通过使用monads得到了很好的解决。

以I/O为例,Haskell中的每个进行I/O的函数都返回一个IO计算,即在IO monad中的计算。因此,一个从键盘读取整数的函数,不会返回一个整数,而是返回一个在运行时会产生一个整数的IO计算:

askForInt :: String -> IO Int

因为它返回的是一个I/O计算而不是一个整数,所以不能直接在求和等操作中使用这个结果。为了访问这个整数值,需要"解开"这个计算。唯一的方法是使用bind函数(>>=):

(>>=) :: IO a -> (a -> IO b) -> IO b

由于这也返回一个I/O计算,所以最终总是得到一个I/O计算。这就是Haskell如何隔离副作用的方式。IO monad充当了真实世界状态的抽象(实际上,在内部它通常是用一个名为RealWorld的类型来实现状态部分)。

但是由于monads不是函数,Haskell不是一个"纯函数式编程语言"。

: 你说的它们不是"函数"是什么意思?为什么说Haskell不是纯函数式的?Haskell是纯函数式的。

作为纯函数式语言,并不意味着所有东西都是函数。它是一种函数式的语言,而且所有的函数都是纯的(不管unsafePerformIO是什么)。

Haskell是纯的。Haskell程序没有副作用 - 它们返回一个"计算"(而不是一个值),这个计算在给定状态的"世界"(封装在一个IO monad中)的情况下,产生另一个"世界"的状态(返回的IO monad)。

这篇博文(不是我写的)提出了一个有趣/幽默的观点,认为C语言是纯函数式的:conal.net/blog/posts/the-c-language-is-purely-functional

这就像说它不是纯函数式的,因为它有整数。

0
0 Comments

纯函数式编程是否可能出现副作用的问题的原因是大多数真实世界的函数式编程在大多数情况下并不是“纯”的,所以回答这个问题的一半答案是“你必须放弃纯度”。尽管如此,还是有一些替代方案的。

在最“纯”的意义上,整个程序代表了一个或多个参数的单个函数,返回一个值。如果你眯起眼睛,稍微挥动一下手,你可以声明所有用户输入都是函数的“参数”部分,所有输出都是“返回值”部分,然后稍微做一些小手脚,让它只在“需要时”执行实际的I/O。

类似的观点是声明函数的输入是“外部世界的整个状态”,计算函数返回一个新的、修改过的“世界状态”。在这种情况下,程序中使用世界状态的任何函数显然不再是“确定性”的,因为对程序的两次求值不会有完全相同的外部世界。

如果你想在纯的lambda演算(或等价的东西,比如奇特的编程语言Lazy K)中编写一个交互式程序,这就是你可能会这样做的方式。

从更实际的角度来看,问题归结为确保在将输入用作函数参数时,I/O按正确的顺序发生。解决这个问题的“纯”解决方案的一般结构是“函数组合”。例如,假设你有三个执行I/O的函数,并且你想按特定的顺序调用它们。如果你像这样做RunThreeFunctions(f1, f2, f3),没有任何东西可以确定它们将以什么顺序进行求值。另一方面,如果让每个函数接受另一个函数作为参数,你可以这样链接它们:f1(f2(f3())),在这种情况下,你知道f3将首先被求值,因为f2的求值取决于它的值。

再次说,如果你想在lambda演算中编写一个交互式程序,这可能是你会这样做的方式。如果你想要一个在编程中实际可用的东西,你可能想要将函数组合部分与接受和返回表示世界状态的值的函数的概念结构相结合,并创建一些高阶抽象来处理在I/O函数之间传递“世界状态”值的管道,理想情况下还要保持“世界状态”的封装以强制执行严格的线性性,这时你几乎重新发明了Haskell的IO Monad。

希望这并没有让你更加困惑。

“另一方面,如果让每个函数接受另一个函数作为参数,你可以这样链接它们:f1(f2(f3())),在这种情况下,你知道f3将首先被求值,因为f2的求值取决于它的值。” - 仅当f2f1实际上使用它的参数计算结果,并且只有调用者将使用它的结果时,惰性求值才能合法地启动。

你当然是正确的,为了不加剧答案已经很冗长的问题,我忽略了很多其他细节。

那么在函数求值的过程中,世界状态是如何改变的?重写世界状态结构的是应用程序之外的东西吗?你必须在循环中不断调用你的应用程序来处理不断变化的世界状态吗?

“世界状态”主要是一个概念上的抽象,而不是程序中的实际数据结构。重要的是,任何具有副作用的函数接受一个世界状态作为参数,并在完成时返回一个新的世界状态。唯一的世界状态标记的传递在函数之间表示程序对外部世界产生的不可撤销的影响。只要世界状态从未被重用,就能保持一致的结构。

至于在循环中调用应用程序,我不确定你的意思是什么。粗略地说,应用程序是一个关于世界状态的单一(复杂的)函数;应用程序内部是关于世界状态的其他函数。当程序运行时,会产生一系列内部世界状态,一个接一个地产生,最后一个状态是整个应用程序返回的状态。

所以,让我搞清楚一下,例如,国际象棋的世界状态将是棋盘和生成当前状态的移动序列,monad封装了每个玩家的选择,即他们决定做出什么移动,这是一个正确的例子吗?

将世界状态视为应用程序环境的抽象:内存、磁盘、屏幕、远程计算机等。你的应用程序将这些作为输入,并产生不同的集合作为输出。只要不重用不同的世界状态,就没问题。你的国际象棋例子是状态monad的一个例子,其状态是棋盘。monad封装了该状态的变化序列。然而,那些状态是可以重用的(考虑撤销),而世界状态抽象是不可撤销的副作用。

严格来说,国际象棋游戏的世界状态还包括轮到哪一方以及某些棋子的部分移动历史(由于“吃过路兵”和“王车易位”规则)。基于Haskell风格的基于状态的monad封装了该状态的细节,并抽象了在函数之间传递状态数据的过程。理论上,你可以通过传递一个显式的状态结构来实现相同的效果 - monad只是为了强制正确性和避免样板代码。

0