看起来你最后一个问题的答案仍然让你感到困惑。
tl; dr: 停止使用>>=
and=<<
直到你掌握了 do-block 符号,你可以通过谷歌搜索“理解 haskell io”并通过教程中的大量示例来完成。
长答案...
首先,我建议暂时避免使用>>=
and=<<
运算符。尽管它们有时被命名为“绑定”,但它们不会将变量或参数绑定到方法或其他类似的东西,它们似乎会让你绊倒。您还可以从“A Gentle Introduction to Haskell”中找到关于 IO 的部分,作为对 IO 工作原理的快速介绍很有帮助。
这是一个非常简短的 IO 解释,可能会对您有所帮助,它将为回答您的问题提供基础。Google for "understanding haskell io" 得到更深入的解释:
三段超短IO讲解:
(1) 在 Haskell 中,任何类型的值IO a
都是一个 IO动作。一个 IO 动作就像一个配方,可以用来(通过执行动作)来执行一些实际的输入/输出,然后产生一个 type 的值a
。因此,type 的值IO String
是一个动作,如果执行,它将执行一些输入/输出并产生一个 type 的值String
,而 anIO ()
是一个动作,如果执行,将执行一些输入/输出并产生一个 type 的值()
。在后一种情况下,由于 type 的值()
是无用的,因此 type 的操作IO ()
通常会因其 I/O 副作用而执行,例如打印一行输出。
(2) 在 Haskell 程序中执行 IO 动作的唯一方法是给它一个特殊的名字main
。(交互式解释器 GHCi 提供了更多执行 IO 操作的方法,但我们忽略它。)
(3) IO动作可以使用do-notation组合成一个更大的IO动作。do
块由以下形式的行组成:
act -- action to be executed, with the result
-- thrown away (unless it's the last line)
x <- act -- action to be executed, with the result
-- named @x@ for later lines
let y = expr -- add a name @y@ for the value of @expr@
-- for later lines, but this has nothing to
-- do with executing actions
在上述模板中,act
可以是任何计算为 IO 操作的表达式(即IO a
some的类型值a
)。重要的是要了解 do-block 本身并不执行任何 IO 操作。相反,它会构建一个新的 IO 操作,当执行该操作时,它将按照它们在 do-block 中出现的顺序执行给定的一组 IO 操作,或者丢弃或命名执行这些操作产生的值。执行整个 do-block 产生的值将是 do-block 的最后一行(必须是上面第一种形式的行)产生的值。
一个简单的例子
因此,如果一个 Haskell 程序包括:
myAction :: IO ()
myAction = do
putStrLn "Your name?"
x <- getLine
let stars = "***"
putStrLn (stars ++ x ++ stars)
那么这定义了一个myAction
类型的值IO ()
,一个 IO 动作。它本身什么也不做,但如果它被执行,那么它将按照它们出现的顺序执行 do-block 中的每个 IO 操作(IO a
各种类型的类型值)。a
执行产生myAction
的值将是最后一行产生的值(在本例中()
为 type的值()
)。
应用于复制行的问题
有了这个解释,让我们来解决你的问题。首先,我们如何编写一个 Haskell 程序,使用循环将行从一个文件复制到另一个文件,而忽略行数问题?这是与您的代码示例非常相似的一种方式:
import System.IO
myAction :: IO ()
myAction = do
inHandle <- openFile "in.txt" ReadMode
outHandle <- openFile "out.txt" WriteMode
loop inHandle outHandle
hClose outHandle
hClose inHandle
openFile
在这里,如果我们检查GHCi中这些调用之一的类型:
> :t openFile "in.txt" ReadMode
openFile "in.txt" ReadMode :: IO Handle
>
我们看到它有 type IO Handle
。也就是说,这是一个 IO操作,在执行时会执行一些实际的 I/O(即打开文件的操作系统调用),然后产生一个 type 的值Handle
,它是表示打开文件句柄的 Haskell 值。在您的原始版本中,当您写道:
let inHandle = openFile "in.txt" ReadMode
所有这些只是为inHandle
IO 操作分配一个名称——它实际上并没有执行 IO 操作,因此实际上并没有打开文件。特别是,inHandle
类型的值本身IO Handle
并不是文件句柄,而只是用于生成文件句柄的 IO 操作(或“配方”)。
在myAction
上面的版本中,我们使用了符号:
inHandle <- openFile "in.txt" ReadMode
表示,如果以及何时myAction
执行名为 by 的 IO 操作,它将首先执行 IO 操作openFile "in.txt" ReadMode"
(即具有 type 的表达式的值IO Handle
),并且该执行将产生一个Handle
名为的inHandle
。下一行也可以生成并命名一个 open outHandle
。然后我们将把这些打开的句柄传递给loop
表达式loop inHandle outHandle
。
现在,loop
可以这样定义:
loop :: Handle -> Handle -> IO ()
loop inHandle outHandle = do
end <- hIsEOF inHandle
if end
then return ()
else do
line <- hGetLine inHandle
hPutStrLn outHandle line
loop inHandle outHandle
值得花一点时间来解释这一点。 loop
是一个接受两个参数的函数,每个Handle
. 当它应用于两个句柄时,如在表达式loop inHandle outHandle
中,结果值为 type IO ()
。这意味着它是一个 IO 动作,具体来说,是由loop
. 这个 do-block 创建一个 IO action,当它被执行时,它会按顺序执行两个 IO action,如外部 do-block 的行所给出的。第一行是:
end <- hIsEOF inHandle
它接受 IO 操作hEof inHandle
(类型的值IO Bool
),执行它(包括询问操作系统我们是否已经到达文件末尾以获取由 handle 表示的文件inHandle
),并命名结果end
- 请注意,这end
将是类型的值Bool
。
do-block 的第二行是整个if
语句。它产生一个 type 的值IO ()
,所以是第二个 IO 动作。IO 操作取决于 的值end
。如果end
为 true,则 IO 操作将是其值return ()
,如果执行,将不执行实际 I/O,并将产生()
type的值()
。如果end
为 false,则 IO 动作将是内部 do-block 的值。这个内部 do-block 是一个 IO 动作(一个 type 的值IO ()
),如果执行,它将依次执行三个 IO 动作:
IO actionhGetLine inHandle
是一个类型的值IO String
,在执行时将从中读取一行inHandle
并生成结果String
。根据 do-block,这个结果将被命名为 name line
。
IO action hPutStrLn outHandle line
,一个类型的值,执行时将IO ()
写入.line
outHandle
IO 动作loop inHandle outHandle
,递归使用由外部 do-block 产生的 IO 动作,它在执行时重新启动整个过程,从 EOF 检查开始。
如果将这两个定义(formyAction
和loop
)放在一个程序中,它们不会做任何事情,因为它们只是 IO 操作的定义。让它们执行的唯一方法是命名其中一个main
,如下所示:
main :: IO ()
main = myAction
当然,我们可以直接使用名称main
代替myAction
来获得相同的效果,就像在整个程序中一样:
import System.IO
main :: IO ()
main = do
inHandle <- openFile "in.txt" ReadMode
outHandle <- openFile "out.txt" WriteMode
loop inHandle outHandle
hClose inHandle
hClose outHandle
loop :: Handle -> Handle -> IO ()
loop inHandle outHandle = do
end <- hIsEOF inHandle
if end
then return ()
else do
line <- hGetLine inHandle
hPutStrLn outHandle line
loop inHandle outHandle
花一些时间将其与上面的“具体示例”进行比较,看看哪里不同,哪里相似。特别是,你能弄清楚我为什么写:
end <- hIsEOF inHandle
if end
then ...
代替:
if hIsEOF inHandle
then ...
复制行数
要修改此程序以计算行数,一种相当标准的方法是使计数成为loop
函数的参数,并loop
产生计数的最终值。由于表达式loop inHandle outHandle
是一个 IO 操作(上面,它是 type IO ()
),要让它产生一个计数,我们需要给它 type IO Int
,就像您在示例中所做的那样。它仍然是一个 IO 操作,但现在——当它被执行时——它会产生一个有用的Int
值而不是一个无用的()
值。
要进行此更改,main
必须使用起始计数器调用循环,命名它产生的值,并将该值输出给用户。
明确地说: main
的值仍然是由 do-block 创建的 IO 动作。我们只是在修改 do-block 的其中一行。它曾经是:
loop inHandle outHandle
它评估为IO ()
表示 IO 操作的类型值 - 当整个 do-block 被执行时 - 将在轮到将行从一个文件复制到另一个文件时执行,然后再生成()
要丢弃的值。现在,它将是:
count <- loop inHandle outHandle 0
其中右侧将评估为IO Int
表示 IO 操作的类型值 - 当执行整个 do-block 时 - 将在轮到将行从一个文件复制到另一个文件时执行,然后生成一个为后面的 do-block 步骤Int
命名的类型的计数值。count
无论如何,修改后的main
样子是这样的:
main :: IO ()
main = do
inHandle <- openFile "in.txt" ReadMode
outHandle <- openFile "out.txt" WriteMode
count <- loop inHandle outHandle 0
hClose inHandle
hClose outHandle
putStrLn (show count) -- could just write @print count@
现在,我们重写loop
以维护一个计数(通过递归调用将运行计数作为参数,并在执行 IO 操作时产生最终值):
loop :: Handle -> Handle -> Int -> IO Int
loop inHandle outHandle count = do
end <- hIsEOF inHandle
if end
then return count
else do
line <- hGetLine inHandle
hPutStrLn outHandle line
loop inHandle outHandle (count + 1)
整个程序是:
import System.IO
main :: IO ()
main = do
inHandle <- openFile "in.txt" ReadMode
outHandle <- openFile "out.txt" WriteMode
count <- loop inHandle outHandle 0
hClose inHandle
hClose outHandle
putStrLn (show count) -- could just write @print count@
loop :: Handle -> Handle -> Int -> IO Int
loop inHandle outHandle count = do
end <- hIsEOF inHandle
if end
then return count
else do
line <- hGetLine inHandle
hPutStrLn outHandle line
loop inHandle outHandle (count + 1)
你剩下的问题
现在,您询问了如何在不使用其他函数的情况下在 do-block 中使用累加器。我不知道您的意思是不使用其他功能loop
(在这种情况下,上面的答案满足要求)还是您的意思是loop
根本不使用任何显式功能。
如果是后者,有几种方法。首先,包中有可用的单子循环组合器monad-loops
,可以让您执行以下操作(复制而不计数)。我也切换到使用withFile
来代替显式的打开/关闭调用:
import Control.Monad.Loops
import System.IO
main :: IO ()
main =
withFile "in.txt" ReadMode $ \inHandle ->
withFile "out.txt" WriteMode $ \outHandle ->
whileM_ (not <$> hIsEOF inHandle) $ do
line <- hGetLine inHandle
hPutStrLn outHandle line
你可以用状态单子计算行数:
import Control.Monad.State
import Control.Monad.Loops
import System.IO
main :: IO ()
main = do
n <- withFile "in.txt" ReadMode $ \inHandle ->
withFile "out.txt" WriteMode $ \outHandle ->
flip execStateT 0 $
whileM_ (not <$> liftIO (hIsEOF inHandle)) $ do
line <- liftIO (hGetLine inHandle)
liftIO (hPutStrLn outHandle line)
modify succ
print n
关于do
从上述定义中删除最后一个块,loop
没有充分的理由这样做。这不像do
块有开销或引入一些额外的处理管道或其他东西。它们只是构建 IO 操作值的方法。所以,你可以替换:
else do
line <- hGetLine inHandle
hPutStrLn outHandle line
loop inHandle outHandle (count + 1)
和
else hGetLine inHandle >>= hPutStrLn outHandle >> loop inHandle outHandle (count + 1)
但这是纯粹的句法变化。两者在其他方面是相同的(并且几乎可以肯定会编译为等效代码)。