我正在看一个来自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)
hPutChar :: Handle -> Char -> IO ()
是一个纯Haskell函数,给定两个参数h :: Handle
和c :: Char
,产生一个IO ()
类型的纯Haskell值,一个“IO
动作”:
h :: Handle c :: Char
---------------------------------------------
hPutChr h c :: IO ()
这个“动作”只是一个Haskell值,但当它出现在IO
下的do
monad main
块内时,它会被Haskell运行时系统执行,然后它实际执行将字符c
放入一个I / O操作文件系统的实体由句柄h
引用。
至于lambda函数,实际的明确语法是
(\ h -> ... )
其中\
和h
之间的空白是可选的,整个(.......)
表达式是lambda表达式。所以没有“\h
实体”:
(\ ... -> ... )
是lambda表达式语法;h
中的\ h ->
是lambda函数的参数,...
中的(\ h -> ... )
是lambda函数的主体。bracket
用(\h -> hPutChar h c)
I / O计算产生的结果调用(openFile fp WriteMode)
lambda函数,这是fp
引用的文件名的句柄,根据模式WriteMode
打开。
关于Haskell monadic IO的主要理解是“计算”不是一个函数:它是执行实际文件打开的实际(此处,I / O)计算 - 它产生句柄 - 然后由运行时系统用它来调用纯Haskell函数(\ h -> ...)
。
这种分层(纯粹的Haskell“世界”和不纯的I / O“世界”)是......的精华......是的,Monad。 I / O计算执行某些操作,找到一些值,使用它来调用纯Haskell函数,该函数创建一个新的计算,然后运行,将其结果提供给下一个纯函数,等等。
因此,我们只谈论不纯净的东西,在Haskell中保持纯洁。说话没有做。
或者是吗?
让我们看一下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
是一个函数,它接受一个句柄并返回一个写入它的计算。这个想法是你传递的函数作为第三个参数指定了如何使用第一个参数产生的资源。
这里没有递归。 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
。