为什么将IO结果包装在IO Monad中

时间:2016-12-12 07:31:33

标签: haskell monads

在Haskell中,我们有一个函数readFile :: FilePath -> IO String。理解monad时我的问题是为什么将它包装在IO中?我们不能写这样的函数:

(lines.readFile) path

而不是

(readFile >>= lines) path 

IO包装器有什么好处?

3 个答案:

答案 0 :(得分:6)

Haskell表达式为referentially transparent。这意味着,如果readFile确实具有FilePath -> String类型,则表达式readFile "a.txt"将始终产生相同的结果。即使您读取文件,然后更改它,然后再次阅读,您将获得第一个状态的内容。

因此,我们需要在操作之间进行distingush,这就是IO的用途。在执行与之关联的操作之前,它不允许您在其他表达式中使用结果readFile "a.exe"。因此,在更改文件后,您必须再次执行读取操作,以获取文件内容,因此您将能够看到更改。

答案 1 :(得分:5)

我们编写创建计算机程序的函数

应该注意Haskell是一种函数式编程语言。在数学意义上,函数总是为相同的输入生成相同的值。

现在这个总是产生相同结果的要求会对事物产生很大的限制,因为读取文件的函数每次都必须产生相同的结果,即使文件稍后被更改。这显然不是我们真正想要的。

然而,有一种方法可以使函数式编程语言能够处理读取更改的文件。你所做的就是编写一个能产生计算机应该执行的动作的函数。因此,您可以执行由以下步骤组成的操作:

Read the file
Break it into lines
Change the even-numbered lines to uppercase
Output the lines to the screen

这四项行动尚未执行。它们只是我们可能执行的一系列操作。函数可以在每次调用时返回完全相同的潜在动作序列,这使其成为一个合适的数学函数。

Haskell中的main :: IO a函数返回程序应执行的操作。它总是返回相同的动作,使其成为一个合适的数学函数。运行程序时,计算机会评估main函数,生成计算机应执行的操作,然后计算机执行操作。

注释

记谱法将过程中的陌生感带走,让您感受到更标准的编程语言。您有三种选择:

  1. 执行操作并对其结果不执行任何操作
  2. 执行操作并存储结果
  3. 仅使用功能处理数据(无操作)
  4. 这些分别按以下方式完成:

    1. action args
    2. result <- action args
    3. let result = f . g . h . whateverCalculation $ value
    4. 这类似于你所做的像C这样的命令式语言:

      1. action(args);
      2. result = action(args);
      3. result = f(g(h(whateverCalculation(value))));

答案 2 :(得分:2)

要使(lines.readFile) path生效,readFile的类型必须为FilePath -> String。然而,这在Haskell中没有意义。当给出相同的参数时,Haskell函数应该总是产生相同的结果。但是,如果结果类型readFileString,则不会发生这种情况,因为readFile "foo.txt"必须对此类readFile的任何有用实现产生不同的字符串,具体取决于关于 foo.txt 文件的内容。

此问题的Haskell解决方案是readFile类型为FilePath -> IO StringIO String不是字符串,而是可以由计算机执行的程序,并且在执行时,以某种方式将String实现到内存中。虽然每次执行程序时生成的String可能不同,但程序本身保持不变,因此readFile在给定相同的参数时总是返回相同的结果(例如, readFile "foo.txt"始终是同一个程序。)

这种操作产生I / O依赖结果而不是结果本身的程序的技巧只有在依赖于I / O的结果保持不透明时才有效。也就是说,如果没有办法直接提取它。换句话说,例如,不能有IO String -> String函数 - 对于一个函数,它将允许我们使用上面讨论过的不合适类型readFile来实现FilePath -> String。但是,使用I / O相关结果的间接方式不会导致麻烦。其中一个是用它来创建第二个程序,它的I / O依赖结果和第一个程序一样不透明。 Monad界面允许我们表达这种使用模式:

(>>=) :: Monad m => m a -> (a -> m b) -> m b

(>>=)专门设为IO,我们得到:

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

第一个程序具有类型IO a,并且使用第一个程序的I / O相关结果生成第二个程序的函数具有类型a -> IO b(>>=)的结果是一个程序,它按顺序执行第一个程序和第二个新生成的程序。例如......

readFile "foo.txt" >>= putStrLn

...是一个程序,它读取 foo.txt 的内容,然后显示这些内容。

P.S。:关于涉及lines的示例,值得注意的是,(readFile >>= lines) path(正如您所写)和(\p -> readFile p >>= lines) path都被类型检查器拒绝。有效的方法是:

(fmap lines . readFile) path

其中,我们以不同的方式间接使用文件内容。如果我们有一个产生I / O依赖结果的程序,我们可以把它变成一个程序,它产生这个结果的修改版本。这是通过fmap类中的Functor完成的:

fmap :: Functor f => (a -> b) -> f a -> f b

或者,专注于IO

fmap @IO :: (a -> b) -> IO a -> IO b