我正在看一个the Haskell Wikibook中的简单IO程序。该页面上显示的构造效果很好,但是我试图理解“如何”。
下面的writeChar
函数采用一个文件路径(作为字符串)和一个字符,并将该字符写入给定路径的文件中。该函数使用bracket来确保文件正确打开和关闭。在括号中进行的三个计算中,“在中间进行计算”(据我所知)是一个lambda函数,它返回hPutChar h c
的结果。
现在,hPutChar
本身的声明为hPutChar :: Handle -> Char -> IO ()
。这是我迷路的地方。我似乎将h
作为hPutChar
的句柄。我希望一个句柄能以某种方式引用以fp
打开的文件,但相反,它似乎是递归调用lambda函数\h
。我看不到这个lambda函数如何递归地调用自己,知道如何将c
写入文件fp
中。
我想了解为什么该函数的最后一行不应该读为(\h -> hPutChar fp c)
。尝试以这种方式运行它会导致“无法将类型'[Char]'与'Handle'相匹配”,鉴于hPutChar期望使用Handle数据类型而不是字符串,因此我认为这是明智的。
import Control.Exception
writeChar :: FilePath -> Char -> IO ()
writeChar fp c =
bracket
(openFile fp WriteMode)
hClose
(\h -> hPutChar h c)
答案 0 :(得分:5)
让我们看一下bracket
(在您的Haskell Wiki链接中引用的类型)的类型:
bracket :: IO a -- computation to run first ("acquire resource")
-> (a -> IO b) -- computation to run last ("release resource")
-> (a -> IO c) -- computation to run in-between
-> IO c
在您的用例中,第一个参数openFile fp WriteMode
是一个IO Handle
值,这是一个生成与fp
路径相对应的句柄的计算。第三个参数\h -> hPutChar h c
是一个带有句柄并返回对其进行写入的计算的函数。这个想法是您作为第三个参数传递的函数指定了如何使用第一个参数产生的资源。
答案 1 :(得分:4)
这里没有递归。 h
的确是Handle
。如果您使用C编程,则粗略等价项是FILE
。句柄由文件描述符,缓冲区以及对附加的文件/管道/终端/执行的任何I / O所需的其他任何内容组成。 openFile
采用路径,打开请求的文件(或设备),并提供可用来操纵请求的文件的句柄。
bracket
(openFile fp WriteMode)
hClose
(\h -> hPutChar h c)
这将打开文件以产生一个句柄。该句柄将传递给第三个函数,该函数将其绑定到h
并将其传递给hPutChar
以输出字符。然后最后,bracket
将句柄传递到hClose
以关闭文件。
如果不存在异常,则可以这样实现bracket
:
bracket
:: IO resource
-> (resource -> IO x)
-> (resource -> IO a)
-> IO a
bracket first last middle = do
resource <- first
result <- middle resource
last resource
pure result
但是bracket
实际上必须安装一个异常处理程序,以确保即使发生异常也要调用last
。
答案 2 :(得分:3)
hPutChar :: Handle -> Char -> IO ()
是一个纯Haskell函数,在给定两个参数h :: Handle
和c :: Char
的情况下,它会产生类型为IO ()
的纯Haskell值,这是一个“ IO
动作”: / p>
h :: Handle c :: Char
---------------------------------------------
hPutChr h c :: IO ()
此“操作”只是一个Haskell值,但是当它出现在IO
下的do
单子main
块内时,它将由执行 Haskell运行时系统和 then 实际上执行I / O操作,将字符c
放入由句柄h
引用的文件系统实体中。
对于lambda函数,实际的明确语法是
(\ h -> ... )
其中\
和h
之间的空格是可选的,整个(.......)
表达式是lambda表达式。因此没有 “ \h
实体”:
(\ ... -> ... )
是lambda-expression语法; h
中的\ h ->
是lambda函数的参数...
中的(\ h -> ... )
是lambda函数的主体。 bracket
调用(\h -> hPutChar h c)
lambda函数,其结果为(openFile fp WriteMode)
I / O computation 产生的结果,该结果是由引用的文件名的句柄fp
,根据模式WriteMode
打开。
了解Haskell单子IO的主要要点是“计算”不是函数:它是执行实际文件打开的实际(此处为I / O)计算-产生句柄-然后由运行时系统用来调用纯Haskell函数 (\ h -> ...)
。
(纯粹的Haskell“世界”和不纯的I / O“世界”)的分层是....的本质, Monad 。 I / O计算会执行某些操作,找到某个值,然后使用它调用纯Haskell函数,该函数会创建一个新的计算,然后运行该计算,将其结果馈送到下一个纯函数中,依此类推。等
因此,我们仅通过谈论不纯净的东西来保持我们在哈斯克尔的纯洁。说话没有做。
是吗?