【问题标题】:Are side-effects possible in pure functional programming在纯函数式编程中是否可能出现副作用
【发布时间】:2010-12-27 08:45:37
【问题描述】:

一段时间以来,我一直在尝试专注于函数式编程。我查看了 lambda 演算、LISP、OCaml、F# 甚至组合逻辑,但我遇到的主要问题是 - 你如何做需要副作用的事情,例如:

  • 与用户互动,
  • 与远程服务通信,或
  • 使用随机抽样处理模拟

不违反纯函数式编程的基本前提,即对于给定的输入,输出是确定性的?

我希望我说得通;如果不是,我欢迎任何帮助我理解的尝试。提前致谢。

【问题讨论】:

    标签: functional-programming side-effects


    【解决方案1】:

    大多数现实世界的函数式编程在大多数意义上都不是“纯粹的”,所以你问题的一半答案是“你通过放弃纯粹性来做到这一点”。也就是说,有 个替代方案。

    在纯粹的“最纯粹”意义上,整个程序代表一个或多个参数的单个函数,返回一个值。如果您眯起眼睛并稍微挥动一下手,您可以声明所有用户输入都是函数“参数”的一部分,并且所有输出都是“返回值”的一部分,然后稍微捏造一些东西,让它只做“按需”的实际 I/O。

    类似的观点是声明函数的输入是“外部世界的整个状态”,并且评估函数返回一个新的、修改后的“世界状态”。在这种情况下,程序中使用世界状态的任何函数显然都免于“确定性”,因为程序的两次评估都不会具有完全相同的外部世界。

    如果您想用纯 lambda 演算(或类似的东西,例如深奥的语言 Lazy K)编写交互式程序,从概念上讲,您可以这样做。

    在更实际的情况下,问题归结为在将输入用作函数的参数时确保 I/O 以正确的顺序发生。这个问题的“纯”解决方案的一般结构是函数组合。例如,假设您有三个执行 I/O 的函数,并且您想按特定顺序调用它们。如果您执行RunThreeFunctions(f1, f2, f3) 之类的操作,则无法确定评估它们的顺序。另一方面,如果您让每个函数将另一个函数作为参数,则可以像这样链接它们:f1( f2( f3())), in在这种情况下,您知道f3 将首先被评估,因为f2 的评估取决于它的值。 [编辑:另请参阅下面关于惰性与急切评估的评论。这很重要,因为惰性求值实际上在非常纯粹的上下文中很常见;例如,纯 lambda 演算中递归的标准实现在热切评估下是非终止的。]

    同样,要在 lambda 演算中编写交互式程序,您可能会这样做。如果您想要一些实际可用于编程的东西,您可能希望将函数组合部分与函数的概念结构结合起来,该函数获取和返回表示世界状态的值,并创建一些高阶抽象来处理流水线“ I/O 函数之间的世界状态”值,理想情况下还保持“世界状态”被包含以强制执行严格的线性 - 在这一点上,您几乎重新发明了 Haskell 的 IO Monad。

    希望这不仅让您更加感到困惑。

    【讨论】:

    • "另一方面,如果你让每个函数接受另一个函数作为参数,你可以像这样链接它们:f1( f2( f3())),在这种情况下你知道 f3将首先评估,因为 f2 的评估取决于它的值。” - 仅当 f2f1 实际使用其参数来计算其结果,并且仅当 f1 的调用者也将使用其结果时。否则惰性评估可以合法地开始。
    • 你当然是对的,为了不加剧答案已经非常冗长,我掩盖了很多其他细节。
    • 那么随着函数的评估,世界状态是否会发生变化?是什么重写了世界状态结构是在应用程序之外吗?您是否必须不断循环调用您的应用程序来处理不断变化的世界状态?
    • “世界状态”主要是概念抽象,而不是程序中的实际数据结构。重要的是任何具有副作用的函数都接受一个世界状态作为参数,并在完成后返回一个新的世界状态。独特的世界状态令牌在函数之间传递时的进展代表了程序对外部世界的不可撤销的影响。只要世界状态不被重用,就会保持一致的结构。
    • 为了迂腐,国际象棋游戏的世界状态还包括轮到谁和某些棋子的部分移动历史(由于 en passant 捕获和铸造)。 Haskell 风格、基于状态的 monad 封装了该状态的细节,并抽象了在函数之间传递状态数据的过程。从理论上讲,您可以传递一个显式的状态结构以获得相同的效果——monad 只是为了强制正确性并避免样板代码。
    【解决方案2】:

    Haskell 是一种纯函数式编程语言。在 Haskell 中,所有函数都是纯函数(即它们总是为相同的输入提供相同的输出)。但是你如何处理 Haskell 中的副作用?嗯,这个问题通过monads的使用很好的解决了。

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

    askForInt :: String -> IO Int
    

    例如,因为它返回 I/O 计算而不是 Int,所以不能直接将此结果用于求和。为了访问Int 值,您需要“解包”计算。唯一的方法是使用绑定函数(>>=):

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

    因为这也返回一个 IO 计算,所以你总是以一个 I/O 计算结束。这就是 Haskell 隔离副作用的方式。 IO monad 充当对现实世界状态的抽象(实际上,在幕后,它通常使用名为 RealWorld 的类型来实现状态部分)。

    【讨论】:

    • 但由于 monad 是非函数式的,Haskell 不是“纯函数式编程语言”。
    • @RBarryYoung:你是什么意思,它们是“无功能的”?是什么让你说 Haskell 不是纯粹的函数式? Haskell 纯函数式的。
    • 作为纯函数式语言并不意味着一切都是函数。它是一种函数式语言,所有函数都是(没关系unsafePerformIO
    • Haskell 是纯粹的。 Haskell 程序没有副作用——它们返回一个计算(不是一个值),给定“世界”的一个状态(包装在IO monad 中),产生另一个状态“世界”(返回的 IO monad)。
    • 这篇博文(不是我写的)提出了一个有趣/幽默的论点,即 C 是纯函数式的:conal.net/blog/posts/the-c-language-is-purely-functional
    【解决方案3】:

    与用户交互并与远程服务进行通信确实需要您的软件有某种非功能性部分。

    许多“函数式语言”(像大多数 Lisps 一样)不是纯粹函数式的。他们仍然允许你做有副作用的事情,尽管在大多数情况下“不鼓励”有副作用的事情。

    Haskell 是“纯功能性的”,但仍然允许您通过 IO monad 执行非功能性的事情。基本思想是,您的纯函数式程序会发出一个惰性数据结构,该数据结构由非函数式程序(您不编写,它是环境的一部分)评估。有人可能会争辩说,这种数据结构本身就是一个命令式程序。所以你是在用函数式语言进行命令式元编程。

    忽略哪种方法“更好”,这两种情况的目标都是在程序的功能部分和非功能部分之间建立分离,并尽可能限制非功能部分的大小。功能部分往往更易于重用、可测试且更易于推理。

    【讨论】:

      【解决方案4】:

      函数式编程是关于限制和隔离副作用,而不是试图完全摆脱它们......因为你做不到。

      ... 是的,我发现 FP 很有用(当然对于 Erlang 来说):我发现从“想法”到“程序”(或从问题到解决方案)更容易......但当然这可能只是做我吧。

      【讨论】:

      • 我也喜欢,我一直很喜欢流水线和过滤作为基本数据处理功能的概念,这通常是函数式编程的基本原则。
      • @Jeremy E:同意:我来自硬件工程背景,我经常从“数据流”的角度思考,就像 Erlang 允许我那样。
      【解决方案5】:

      我所知道的唯一完全纯函数式语言是 C++ 中的模板系统。 Haskell 通过使程序的命令部分显式化而位居第二。

      在 Haskell 中,程序具有可变状态,但函数(几乎总是)没有。你保持 99% 的程序纯,只有与外界交互的部分是不纯的。因此,当您测试功能时,您知道没有副作用。纯核,带有不纯的外壳。

      【讨论】:

      • 我开始自己得出同样的结论,大家帮我解决这个问题。
      • Haskell 没有可变状态:它使用定义“newtype StateT sma = StateT {runStateT :: s -> m (a, s)}”“伪造”它,并且 IO 有一个几乎相同的定义。
      【解决方案6】:

      您至少需要知道另一个基本概念:Monads。您将需要它来执行 I/O 和其他“有用”的东西!

      【讨论】:

        【解决方案7】:

        Haskell 的做法是使用 monad,请参阅 wikipedia 和 Haskell 在其 page 上的解释。

        基本上这个想法是你不会摆脱 IO monad。我的理解是,您可以链接解开 IO monad 的函数并执行该函数。但是您无法完全删除 IO monad。

        另一个使用不直接绑定到 IO 的 monad 的例子是 Maybe Monad。与 IO monad 相反,这个 monad 是“不可包装的”。但是使用 Maybe monad 更容易解释 monad 的使用。假设您具有以下功能。

        wrap :: Maybe x -> (x -> y) -> Maybe y
        wrap Nothing  f = Nothing
        wrap (Just x) f = Just (f x)
        

        现在您可以调用wrap (Just 4) (5+),它会返回Just 9

        IO-monad 的想法是你可以在内部类型上使用像 (+5) 这样的函数。 monad 将确保函数将被串行调用,因为每个函数都与包装 IO-monad 链接在一起。

        【讨论】:

        • 就是这样。有一个绑定函数/运算符 (>>=) 可以“解包”一个 IO 计算并将其传递给一个返回 另一个 IO 计算的函数。因为没有其他方法可以“解包” IO 计算,所以您无法摆脱它。
        • @Ruben: 'unwrappable' => 为了将来参考,这些单子被称为开放单子,而不是 IO 是一个封闭单子。
        【解决方案8】:

        鉴于大多数程序对外部世界都有一些影响(写入文件、修改数据库中的数据......),整个程序很少没有副作用。除了学术练习之外,即使尝试也没有任何意义。

        但程序是由构建块(子例程、函数、方法,随心所欲地称呼它)组装而成的,而纯函数构成了表现良好的构建块。

        大多数函数式编程语言不要求函数是纯函数,尽管优秀的函数式程序员会尝试在可行和实用的情况下尽可能多地使其函数成为纯函数,以便获得引用透明性的好处。

        Haskell 更进一步。 Haskell 程序的每一部分都是纯粹的(至少在没有诸如“unsafePerformIO”之类的罪恶的情况下)。您在 Haskell 中编写的所有函数都是纯函数。

        副作用是通过单子引入的。它们可以用来引入一种“购物清单——购物者”的分离。本质上,您的程序编写了一个购物清单(它只是数据,可以以纯粹的方式进行操作),而语言运行时解释购物清单并进行有效的购物。你所有的代码都是纯的,对等式推理等很友好,而不纯的代码是由编译器编写的。

        【讨论】:

          【解决方案9】:

          即使您没有在工作中使用它,学习一种或多种函数式编程语言也是学习以不同方式思考的好方法,并为您提供了解决问题的替代方法的工具包(如果可以的话,它也会让您感到沮丧) '不要做像其他语言中的函数式方法那样整洁干净的事情)。

          这让我更擅长编写 XSL 样式表。

          【讨论】:

          • XSLT:因为图灵完备的 XML 正是世界所需要的。
          • @camccann 我希望你是在讽刺。
          • 我发现图灵完备的 XML 远优于图灵完备的类型泛化机制(参见 C++ 模板等)。也就是说,对于讨厌尖括号的人,有 XQuery。
          • 讽刺世界需要图灵完备的 XML:是的。对 XSLT 存在图灵完备的讽刺:很遗憾,没有。 (更别提图灵完备的排版语言或构建系统了。)
          • @Pavel Minaev:另一方面,回到主题,C++ 模板实际上是一种纯粹的函数式语言!
          【解决方案10】:

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

          这取决于...

          是什么意思

          [...] 我遇到的主要问题是如何在不违反 函数式编程的基本前提的情况下做需要副作用的事情 [...] >对于给定的输入,输出是确定性的?

          ...这是一个有趣的描述:这是一个类似的描述:

          [...] 函数式编程的数学基础,它要求函数的值由其参数的值唯一确定。

          Nondeterminism with Referential Transparency in Functional Programming Languages,F. Warren Burton。

          ...Burton 然后更准确地说:

          引用透明性,即表达式在相同环境中始终具有相同值的属性,是函数式程序的数学基础的核心。

          我们将为another question 保留“纯”、“参照透明”和“副作用”等术语的选择,而是选择修改此处提出的问题以避免使用它们:

          用函数式语言编写的程序(函数式程序)如何执行practical 任务,例如:

          • 与用户互动,
          • 与远程服务通信,
          • 使用随机抽样处理模拟,
          • 打印出an SVG file(例如作为海报),
          • 进行计划备份,

          ...等等,同时确保对于给定的输入,输出是确定性的?

          Burton 的解决方案使用他所谓的伪数据:抽象的一次性值。然后以合适的结构化值的形式无限供应这些 - Burton 使用树:

          • 原始树 - 作为参数传递给正在运行的程序 - 被分成子树,在整个程序中分布(也作为程序函数的参数);

          • 然后从这些子树中检索新的抽象值以供原始函数使用,其中会发生预期的效果。

          • 每个抽象值只能使用一次,因此每个原始调用都需要另一个新的抽象值作为输入 - 如果原始调用以某种方式重复,则输出将是相同的。

          除了提供非确定性之外,Burton 还简要描述了如何扩展他的方法以访问其他系统资源(特别是 timestampsspacestamps)。如需更多信息,请阅读his paper - 它只有 5 页长...

          【讨论】:

            猜你喜欢
            • 2013-08-12
            • 2023-03-04
            • 2011-07-19
            • 2019-12-04
            • 2016-08-15
            • 1970-01-01
            • 1970-01-01
            • 2013-10-21
            • 1970-01-01
            相关资源
            最近更新 更多