Functional programming 如何在纯函数式编程中执行副作用?

Functional programming 如何在纯函数式编程中执行副作用?,functional-programming,purely-functional,Functional Programming,Purely Functional,我现在正在讨论函数式编程的概念,并且发现它非常有趣、迷人和令人兴奋。特别是纯函数的概念,从各个方面来说,都是令人敬畏的 但有一件事我不明白:当你把自己局限于纯粹的功能时,如何处理副作用 例如,如果我想计算两个数字的和,我可以编写一个纯函数(JavaScript): 没问题。但是如果我想把结果打印到控制台上呢?“将某些内容打印到控制台”的任务并不纯粹是定义上的,但我应该如何在纯函数式编程语言中处理这个问题呢?Haskell是一种纯函数式语言,它使用“monad”处理“不纯净”的函数。monad基本

我现在正在讨论函数式编程的概念,并且发现它非常有趣、迷人和令人兴奋。特别是纯函数的概念,从各个方面来说,都是令人敬畏的

但有一件事我不明白:当你把自己局限于纯粹的功能时,如何处理副作用

例如,如果我想计算两个数字的和,我可以编写一个纯函数(JavaScript):


没问题。但是如果我想把结果打印到控制台上呢?“将某些内容打印到控制台”的任务并不纯粹是定义上的,但我应该如何在纯函数式编程语言中处理这个问题呢?

Haskell是一种纯函数式语言,它使用“monad”处理“不纯净”的函数。monad基本上是一种模式,可以很容易地将函数调用与继续传递联系起来。从概念上讲,Haskell中的print函数基本上接受三个参数:要打印的字符串、程序的状态和程序的其余部分。它调用程序的其余部分,同时传入程序的新状态,其中字符串出现在屏幕上。这样就不会修改任何状态


关于单子是如何工作的,有很多深入的解释,因为出于某种原因,人们认为这是一个很难理解的概念:它不是。你可以通过在互联网上搜索找到很多,我想这是我最喜欢的一个:

有几种方法可以做到这一点。你必须接受的一件事是,在某一点上,存在着一个神奇的不纯机器,它接受纯表达式,并通过与环境交互使它们变得不纯。你不应该问关于这台神奇机器的问题

有两种方法我可以在脑海里想出来。至少还有第三个我已经忘记了


I/O流 最容易理解的方法可能是流式I/O。您的
main
函数有一个参数:系统上发生的事件流–包括按键、文件系统上的文件等等。您的
main
函数还返回一件事:您希望在系统上发生的一系列事情

请注意,流就像列表,一次只能构建一个元素,而一旦构建了元素,接收者就会收到该元素。您的纯程序从这样的流中读取数据,并在希望系统执行某些操作时附加到自己的流中

使所有这些工作顺利进行的胶水是一台神奇的机器,它位于程序之外,从“请求”流中读取内容,并将内容放入“回答”流中。虽然你的程序是纯的,但这台神奇的机器不是

输出流可能如下所示:

[print('Hello, world! What is your name?'), input(), create_file('G:\testfile'), create_file('C:\testfile'), write_file(filehandle, 'John')]
相应的输入流是

['John', IOException('There is no drive G:, could not create file!'), filehandle]
查看输出流中的
输入如何导致输入流中出现
'John'
?这就是原则

一元I/O 一元I/O是Haskell所做的,并且做得非常好。您可以想象,这是一个巨大的I/O命令树,其中包含运算符将它们粘合在一起,然后您的
main
函数将这个庞大的表达式返回到位于程序外部的神奇机器,执行命令并执行指定的操作。这个神奇的机器是不纯的,而你的表情构建程序是纯的

您可能希望将此命令树想象成

main
  |
  +---- Cmd_Print('Hello, world! What is your name?')
  +---- Cmd_WriteFile
           |
           +---- Cmd_Input
           |
           +---+ return validHandle(IOResult_attempt, IOResult_safe)
               + Cmd_StoreResult Cmd_CreateFile('G:\testfile') IOResult_attempt
               + Cmd_StoreResult Cmd_CreateFile('C:\testfile') IOResult_safe
它做的第一件事是打印问候语。接下来,它要做的就是编写一个文件。为了能够写入文件,它首先需要从输入中读取它应该写入文件的任何内容。然后它应该有一个要写入的文件句柄。它从一个名为
validHandle
的函数中获取该值,该函数返回两个备选方案的有效句柄。这样,您可以将看起来像不纯净代码的代码与看起来像纯代码的代码混合在一起


这个“解释”几乎是在问关于你不应该问的魔法机器的问题,所以我将用几条智慧来结束这个问题

  • 真正的一元I/O与这里的示例相差甚远。我的例子是一元I/O如何在不破坏纯度的情况下看起来像“引擎盖下”的可能解释之一

  • 不要试图用我的例子来理解如何使用纯I/O。某些东西在引擎盖下的工作方式与使用它的方式完全不同。如果你一生中从未见过汽车,你也不会通过阅读汽车的蓝图而成为一名好司机

    我一直说,你不应该问关于神奇机器的问题,因为当程序员学习东西的时候,他们倾向于去戳机器,试图弄明白它。我不建议对纯I/O这样做。机器可能不会教你如何使用不同类型的I/O

    这类似于不通过查看已反汇编的JVM字节码来学习Java

  • 一定要学会使用一元I/O和基于流的I/O。这是一种很酷的体验,在你的工具带下有更多的工具总是很好的

    • kqr:

      至少还有第三个我已经忘记了

      延续可能是你试图回忆的东西

      乔尼:

      从概念上讲,Haskell中的print函数基本上需要三个 参数:要打印的字符串、程序状态、, 还有节目的其他部分

      你有它,还有更多:不需要额外的状态参数; 拥有程序的其余部分就足够了(只有实现才能处理问题) 与国家合作)。参见第3.2节(第18页) 由菲利普·瓦德勒提供详细信息

      就在Haskell的第一个版本发布之前,F.Warren Burton提出了一种基于伪数据(抽象值树)的技术
      main
        |
        +---- Cmd_Print('Hello, world! What is your name?')
        +---- Cmd_WriteFile
                 |
                 +---- Cmd_Input
                 |
                 +---+ return validHandle(IOResult_attempt, IOResult_safe)
                     + Cmd_StoreResult Cmd_CreateFile('G:\testfile') IOResult_attempt
                     + Cmd_StoreResult Cmd_CreateFile('C:\testfile') IOResult_safe