在Haskell中,我们有一个函数readFile :: FilePath -> IO String
。理解monad时我的问题是为什么将它包装在IO
中?我们不能写这样的函数:
(lines.readFile) path
而不是
(readFile >>= lines) path
IO包装器有什么好处?
答案 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
函数,生成计算机应执行的操作,然后计算机执行操作。
记谱法将过程中的陌生感带走,让您感受到更标准的编程语言。您有三种选择:
这些分别按以下方式完成:
action args
result <- action args
let result = f . g . h . whateverCalculation $ value
这类似于你所做的像C这样的命令式语言:
action(args);
result = action(args);
result = f(g(h(whateverCalculation(value))));
答案 2 :(得分:2)
要使(lines.readFile) path
生效,readFile
的类型必须为FilePath -> String
。然而,这在Haskell中没有意义。当给出相同的参数时,Haskell函数应该总是产生相同的结果。但是,如果结果类型readFile
为String
,则不会发生这种情况,因为readFile "foo.txt"
必须对此类readFile
的任何有用实现产生不同的字符串,具体取决于关于 foo.txt 文件的内容。
此问题的Haskell解决方案是readFile
类型为FilePath -> IO String
。 IO 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