如何在递归 IO 操作中使用累加器

How to use an accumulator in a recursive IO action

我有以下问题:

我想逐行读取一个文件并将这些行写入另一个文件。但是,我要return行数。

因此,在纯函数中我会使用这样的累加器:

function parameters=method 0 ......
                    method accu  {end case scenario} =accu
                    method accu  {not end case} = accu+1 //and other stuff

如何在不使用其他函数的情况下在 do 块中实现相同的功能?

具体例子

module Main where 

    import System.IO
    import Data.Char(toUpper)


    main::IO()
    main=do
        let inHandle=openFile "in.txt" ReadMode
        let outHandle=openFile "out.txt" WriteMode
        inHandle>>= \a ->outHandle>>= \b ->loop a b 0>>=print . show 


    loop::Handle->Handle->Int->IO Int
    loop inh outh cnt=hIsEOF inh>>= \l ->if l then return elem
                                        else
                                            do
                                                hGetLine inh>>=hPutStrLn outh
                                                loop inh outh (cnt+1)

编辑

重构了 loop 获取参数的方式

P.S 2(在 K. A. Buhr 的 彻底回应之后)

I. 我真正想要实现的是main方法的最后一个表达式。我想获取多个 IO Actions 并将它们的结果绑定到一个方法。具体来说:

inHandle>>= \a ->outHandle>>= \b ->loop a b 0>>=print . show

在这个案例中我不明白的是:

如果将inHandle>>=提供给\a ->,然后将结果传递给...>>=\b,那么外部范围内的变量是否在\b中被关闭?

如果不是,不应该是>>=\a->..>>= \a b吗?内部作用域不应该保存一个与外部作用域的结果对应的参数吗?

消除辅助方法中的do

我想知道的是,是否有一种方法可以将多个动作粘合在一起,而无需将它们放在 do 块中。

就我而言:

loop::Handle->Handle->Int->IO Int
        loop inh outh cnt=hIsEOF inh>>= \l ->if l then return elem
                                            else
                                                do
                                                    hGetLine inh>>=hPutStrLn outh
                                                    loop inh outh (cnt+1)

我不能这样说吗:

if ... then ...
else
hPutStrLn=<<action1 [something] v2=<<action2 [something] loop inh outh (cnt+1)

其中 something 可能是运算符?我不知道,所以我才问。

看起来你上一个问题的答案仍然让你感到困惑。

tl;dr: 停止使用 >>==<< 直到你掌握了可以通过谷歌搜索 "understanding haskell io" 并完成教程中的大量示例。

长答案...

首先,我建议暂时避免使用 >>==<< 运算符。即使它们有时被命名为 "bind",它们也不会绑定变量或将参数绑定到方法或任何其他类似的东西,它们似乎会让您感到困惑。您可能还会发现 section about IO from "A Gentle Introduction to Haskell" 作为对 IO 工作原理的快速介绍很有帮助。

这里是对 IO 的一个非常简短的解释,可能会对您有所帮助,它会为回答您的问题提供基础。 Google "understanding haskell io" 以获得更深入的解释:

三段超短IO讲解:

(1) 在 Haskell 中,任何 IO a 类型的值都是一个 IO action。一个 IO 动作就像一个食谱,可以用来(通过 执行 动作)执行一些实际的 input/output 然后产生一个类型 a 的值。因此,IO String 类型的值是一个操作,如果执行,它将执行一些 input/output 并产生一个 String 类型的值,而 IO () 是一个操作,如果执行,将执行一些 input/output 并产生类型 () 的值。在后一种情况下,因为 () 类型的值是无用的,所以通常会执行 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 操作的表达式(即,对于某些 a,类型为 IO a 的值)。重要的是要了解 do-block 本身并不执行任何 IO 操作。相反,它构建了一个新的 IO 操作,当执行时,将按照它们在 do 块中出现的顺序执行给定的 IO 操作集,丢弃或命名通过执行这些操作产生的值。执行整个 do-block 产生的值将是 do-block 最后一行产生的值(必须是上面第一种形式的一行)。

一个简单的例子

因此,如果 Haskell 程序包括:

myAction :: IO ()
myAction = do
  putStrLn "Your name?"
  x <- getLine
  let stars = "***"
  putStrLn (stars ++ x ++ stars)

然后这定义了一个 IO () 类型的值 myAction,一个 IO 操作。就其本身而言,它什么都不做,但如果它曾经被执行过,那么它将按照它们执行的顺序执行 do 块中的每个 IO 操作(类型 IO a 的各种类型 a 的值)出现。执行 myAction 产生的值将是最后一行产生的值(在本例中,类型 () 的值 ())。

应用于复制行问题

有了这个解释,让我们来解决你的问题。首先,我们如何编写一个 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

在这里,如果我们在 GHCi 中检查其中一个 openFile 调用的类型:

> :t openFile "in.txt" ReadMode
openFile "in.txt" ReadMode :: IO Handle
>

我们看到它的类型是 IO Handle。也就是说,这是一个 IO action,在执行时会执行一些实际的 I/O(即打开文件的操作系统调用),然后生成类型为 Handle,这是表示打开文件句柄的 Haskell 值。在您的原始版本中,当您写道:

let inHandle = openFile "in.txt" ReadMode

所有这一切只是为一个 IO 操作指定了一个名称 inHandle——它实际上并没有执行 IO 操作,因此实际上并没有打开文件。特别是,类型 IO HandleinHandle 的值不是 本身 文件句柄,只是一个 IO 操作(或 "recipe")用于生成文件句柄。

在上面的myAction版本中,我们使用了符号:

inHandle <- openFile "in.txt" ReadMode

表示,如果myAction命名的IO动作曾经被执行过,它将从执行IO动作openFile "in.txt" ReadMode"开始(即,具有输入 IO Handle),该执行将产生一个 Handle,它将被命名为 inHandle。同上,下一行生成并命名一个开放 outHandle。然后,我们将把这些打开的句柄传递给表达式 loop inHandle outHandle.

中的 loop

现在,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,结果值的类型为 IO ()。这意味着它是一个 IO 操作,具体来说,是由 loop 定义中的外部 do 块创建的 IO 操作。此 do 块创建一个 IO 操作,当它被执行时,按顺序执行两个 IO 操作,如外部 do 块的行所示。第一行是:

end <- hIsEOF inHandle

采取 IO 操作 hEof inHandle(类型 IO Bool 的值),执行它(包括询问操作系统是否已到达所表示文件的文件末尾通过句柄 inHandle),并将结果命名为 end -- 请注意 end 将是类型 Bool.

的值

do 块的第二行是整个 if 语句。它产生一个 IO () 类型的值,因此是第二个 IO 操作。 IO 操作取决于 end 的值。如果 end 为真,则 IO 操作将是 return () 的值,如果执行,将不会执行实际的 I/O 并将产生 () 类型的值 ()。如果 end 为 false,则 IO 操作将是内部 do 块的值。这个内部 do-block 是一个 IO 动作(IO () 类型的值),如果执行,将按顺序执行三个 IO 动作:

  1. IO 操作 hGetLine inHandle,类型为 IO String 的值,执行时将从 inHandle 读取一行并生成结果 String.根据 do 块,此结果将被命名为 line.

  2. IO 操作 hPutStrLn outHandle line,类型为 IO () 的值,执行时会将 line 写入 outHandle

  3. IO 操作 loop inHandle outHandle,递归使用由外部 do 块生成的 IO 操作,它在执行时重新开始整个过程​​,从EOF 检查。

如果你把这两个定义(myActionloop)放在一个程序中,它们不会做任何事情,因为它们只是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

花一些时间将其与上面的 "concrete example" 进行比较,看看它的不同之处和相似之处。特别是,你能明白我为什么写:

end <- hIsEOF inHandle
if end
  then ...

而不是:

if hIsEOF inHandle
  then ...

复制带有行数的行

要修改此程序以计算行数,一种相当标准的方法是将计数作为 loop 函数的参数,并让 loop 生成数数。因为表达式 loop inHandle outHandle 是一个 IO 操作(上面,它的类型是 IO ()),要让它产生一个计数,我们需要给它类型 IO Int,就像你在例子。它仍然是一个 IO 操作,但现在 - 当它被执行时 - 它会产生一个有用的 Int 值而不是一个无用的 () 值。

要进行此更改,main 必须使用起始计数器调用循环,命名它产生的值,并将该值输出给用户。

绝对清楚:main的值仍然是由 do-block 创建的 IO 操作。我们只是修改 do-block 的其中一行。曾经是:

loop inHandle outHandle

评估为 IO () 类型的值,表示一个 IO 操作 - 当整个 do-block 被执行时 - 将在轮到将行从一个文件复制到other 在产生要丢弃的 () 值之前。现在,它将是:

count <- loop inHandle outHandle 0

其中右侧将评估为 IO Int 类型的值,表示一个 IO 操作 - 当整个 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 块中使用累加器。我不知道你的意思是不使用除 loop 之外的其他功能(在这种情况下,上面的答案满足要求)还是你的意思是根本不使用任何显式 loop

如果是后者,有两种方法。首先,monad-loops 包中提供了 monadic 循环组合器,可以让您执行以下操作(复制而不计算)。我还切换到使用 withFile 代替显式 open/close 调用:

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

你可以用状态 monad 来计算行数:

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

关于从上面 loop 的定义中删除最后一个 do 块,没有充分的理由这样做。它不像 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)

但这纯粹是句法上的改变。两者在其他方面是相同的(并且几乎肯定会编译为等效代码)。