IO monad模式处理副作用的好处是否纯粹是学术上的?


17

对不起,还有另一个FP +副作用问题,但是我找不到一个可以完全回答我的问题。

我对函数式编程的(有限的)理解是,应将状态/副作用最小化并与无状态逻辑分开。

我还收集了Haskell的方法,即IO monad,它通过将有状态的动作包装在一个容器中以供以后执行(被认为超出程序本身的范围)来实现。

我试图理解这种模式,但实际上是确定是否在Python项目中使用它,因此要避免使用Haskell规范。

粗暴的例子来了。

如果我的程序将XML文件转换为JSON文件:

def main():
    xml_data = read_file('input.xml')  # impure
    json_data = convert(xml_data)  # pure
    write_file('output.json', json_data) # impure

IO monad的方法不是有效地做到这一点:

steps = list(
    read_file,
    convert,
    write_file,
)

然后通过不实际调用这些步骤来放任自己的责任,而是让口译员这样做?

换一种说法,就像写:

def main():  # pure
    def inner():  # impure
        xml_data = read_file('input.xml')
        json_data = convert(xml_data)
        write_file('output.json', json_data)
    return inner

然后期待别人打来电话inner(),说您的工作已经完成,因为它main()很纯正。

基本上,整个程序最终将包含在IO monad中。

实际执行代码时,读取文件后的所有内容都取决于该文件的状态,因此仍然会遭受与命令式实现相同的与状态相关的错误,因此,作为一名程序员,您将真正维护任何东西吗?

我完全赞赏减少隔离有状态行为的好处,这实际上就是为什么我这样构造命令式版本的原因:收集输入,做纯净的东西,吐出输出。希望它convert()可以是完全纯净的,并可以从可缓存性,线程安全性等方面受益。

我也很欣赏monadic类型很有用,特别是在以可比较类型运行的管道中,但看不到为什么IO应该使用monad,除非已经在这样的管道中使用了。

处理IO monad模式带来的副作用(我所缺少的)还有其他好处吗?


1
您应该观看此视频 最终无需借助类别理论或Haskell就能揭示单子的奇迹。事实证明,monad是用JavaScript轻松表达的,并且是Ajax的关键促成因素之一。单子弹很棒。它们是简单的事物,几乎是微不足道的实现,具有管理复杂性的强大功能。但是,理解它们非常困难,而且大多数人一旦拥有了这一点,似乎就会失去向他人解释它们的能力。
罗伯特·哈维

好的视频,谢谢。我实际上是从JS函数编程入门中学到的(后来又读了一百万篇…)。尽管已经看了,但是我相当确定我的问题是针对IO monad的,Crock在该视频中没有涉及。
Stu Cox

嗯... AJAX不算是一种I / O吗?
罗伯特·哈维

1
请注意,mainHaskell程序中的类型为IO ()IO操作。这实际上根本不是一个函数;这是一个价值。您的整个程序是一个纯值,其中包含告诉语言运行时应该执行的操作的指令。所有不纯净的东西(实际上是执行IO操作)不在程序的范围之内。
Wyzard

在您的示例中,单子部分是何时获取一个计算的结果(read_file)并将其用作下一个计算的参数(write_file)。如果您只有一系列独立的动作,则不需要Monad。
lortabac 2015年

Answers:


14

基本上,整个程序最终将包含在IO monad中。

从Haskellers的角度来看,这就是我认为您看不到的地方。所以我们有一个像这样的程序:

module Main

main :: IO ()
main = do
  xmlData <- readFile "input.xml"
  let jsonData = convert xmlData
  writeFile "output.json" jsonData

convert :: String -> String
convert xml = ...

我认为Haskeller对此的典型看法是convert,纯粹的一部分:

  1. 可能是该程序的大部分内容,并且比各IO部分要复杂得多;
  2. 无需进行任何处理即可进行推理和测试IO

因此,他们不认为这convert是“载”中IO,而是因为它被孤立IO。从类型上讲,无论convert做什么都绝不能依赖于IO动作中发生的任何事情。

实际执行代码时,读取文件后的所有内容都取决于该文件的状态,因此仍然会遭受与命令式实现相同的与状态相关的错误,因此,作为一名程序员,您将获得真正的收获吗?

我会说这分为两件事:

  1. 程序运行时,to 参数的值convert取决于文件的状态。
  2. 但是该convert函数的作用依赖于文件的状态。 即使在不同点使用不同的参数调用,它convert始终是相同的函数

这是一个有点抽象的观点,但这实际上是Haskellers谈论此事时的含义的关键。您希望以convert这样一种方式进行编写:给定任何有效的参数,它将为该参数产生正确的结果。当您这样看时,读取文件是有状态操作这一事实并没有纳入等式中。重要的是,无论传递给它什么参数,无论它来自哪里,都convert必须正确处理它。纯度限制了convert对其输入的处理,这一事实简化了这种推理。

因此,如果convert从某些参数产生不正确的结果,并将其readFile提供给这样的参数,我们就不会将其视为state引入的错误。这是纯函数中的错误!


我认为这是最好的描述(尽管其他人也为我澄清了一些事情),谢谢。
Stu Cox

值得注意的是,在python中使用monad的好处可能较少,因为python仅具有一种(静态)类型,因此不会对任何事情做任何保证?
jk。

7

很难确定“纯粹学术”到底是什么意思,但我认为答案大多是“否”。

正如西蒙·佩顿·琼斯(Simon Peyton Jones)的《处理尴尬小队》所述强烈建议阅读!),单子I / O旨在解决Haskell用于处理I / O的实际问题。阅读带有“请求和响应”的服务器示例,我不会在此处复制它。这很有启发性。

与Python不同的是,Haskell鼓励一种“纯”计算风格,这种风格可以由其类型系统来实施。当然,您可以在Python编程时使用自律来遵守这种风格,但是您未编写的模块又如何呢?如果没有类型系统(和公共库)的太多帮助,则Monadic I / O在Python中的用处可能较小。语言的哲学并不意味着要进行严格的纯/不纯分离。

请注意,这更多是关于Haskell和Python的不同哲学,而不是学术上的单声道I / O。我不会在Python中使用它。

另一件事。你说:

基本上,整个程序最终将包含在IO monad中。

确实,Haskell main函数“存在”于中IO,但鼓励真正的Haskell程序在不需要IO时不要使用。您编写的几乎不需要执行I / O的每个函数几乎都不应具有type IO

因此,我想在您的上一个示例中将其倒退:main是不纯正的(因为它可以读写文件),但是诸如核心功能之类convert是纯净的。


3

为什么 IO不纯?因为它可能在不同的时间返回不同的值。必须以一种或另一种方式来考虑对时间的依赖。对于懒惰的评估,这一点甚至更为关键。考虑以下程序:

main = do  
    putStrLn "Please enter your name"  
    name <- getLine
    putStrLn $ "Hello, " ++ name

如果没有IO monad,为什么第一个提示会输出?没有什么依赖于它,因此懒惰的评估意味着它永远不会得到要求。在读取输入之前,也没有什么能迫使提示符输出。就计算机而言,如果没有IO monad,则前两个表达式是完全相互独立的。幸运的是,name对后两个施加了命令。

还有其他方法可以解决顺序依赖性问题,但是使用IO monad可能是最简单的方法(至少从语言的角度而言),它可以使所有内容停留在惰性函数领域中,而无需很少的命令性代码。它也是最灵活的。例如,您可以相对轻松地在运行时根据用户输入动态地构建IO管道。


2

我对函数式编程的(有限的)理解是,应将状态/副作用最小化并与无状态逻辑分开。

这不仅仅是函数式编程;在任何语言中,这通常都是一个好主意。如果你做单元测试,你采取折中的办法分开read_file()convert()write_file()配备完美自然,因为,尽管convert()目前的代码中最复杂,规模最大的部分是,编写测试,因为这是比较容易:所有你需要设置为输入参数。为read_file()和编写测试write_file()非常困难(即使函数本身几乎是微不足道的),因为在调用函数之前和之后,您需要在文件系统上创建和/或读取内容。理想情况下,您应该使此类功能如此简单,以至于不进行测试就感到很自在,从而为自己省去了很多麻烦。

Python和Haskell之间的区别在于,Haskell具有类型检查器,可以证明函数没有副作用。在Python中,您需要希望没有人意外地将文件读取或写入功能放入convert()(例如read_config_file())中。在Haskell中,当您声明convert :: String -> String或类似但没有IOmonad时,类型检查器将保证这是一个纯函数,仅依赖于其输入参数,而没有其他依赖。如果有人尝试修改convert以读取配置文件,他们将迅速看到编译器错误,表明他们将破坏函数的纯度。(并且希望他们足够明智,可以将结果read_config_file移出convert并传递到其中convert,以保持纯度。)

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.