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

时间:2018-07-24 14:10:22

标签: haskell io-monad

你好,我有以下问题: 我想逐行读取文件,然后将行写入另一个文件。但是,我想返回行数。

因此,在一个纯函数内部,我将使用一个累加器,例如:

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并将其结果绑定到一个方法。具体来说:< / p>

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

在这种情况下,我不了解的是:
如果将inHandle>>=提供给\a ->,然后将结果传递给...>>=\b,则外部作用域内的变量是否在\b中闭包了?!
如果不是>>=\a->..>>= \a b,不是吗?内部作用域不应该保存与外部作用域的结果相对应的参数吗?

消除辅助方法内的操作
我想知道这是否是一种将多个动作粘合在一起而又不会在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可能是运算符?(我不知道这就是我要问的原因)的原因。

1 个答案:

答案 0 :(得分:5)

看起来您对最后一个问题的答案仍然让您感到困惑。

tl; dr:停止使用>>==<<,直到掌握了do-block表示法,您可以通过谷歌搜索“了解haskell io”并进行大量操作来完成教程中的示例。

长答案...

首先,我建议暂时避免使用>>==<<运算符。即使它们有时被称为“绑定”,它们也不会将变量或参数绑定到方法或其他类似方法,并且似乎使您绊倒。您可能还会发现section about IO from "A Gentle Introduction to Haskell"有助于快速了解IO的工作原理。

以下是有关IO的简短说明,可能会对您有所帮助,它将为回答您的问题提供基础。 Google对“了解haskell io”进行了更深入的解释:

超短IO的三段说明:

(1)在Haskell中,任何类型IO a的值都是IO 操作。 IO操作就像一个配方,可用于(通过执行操作)执行一些实际的输入/输出,然后产生类型为a的值。因此,类型为IO String的值是一个动作,如果执行,它将执行一些输入/输出并产生类型为String的值,而IO ()则是一个动作,如果执行后,将执行一些输入/输出并产生类型为()的值。在后一种情况下,由于类型()的值是无用的,因此通常会执行类型为IO ()的操作,因为它们具有I / O副作用,例如打印输出行。

(2)在Haskell程序中执行IO操作的唯一方法是为其赋予特殊名称main。 (交互式解释器GHCi提供了更多执行IO操作的方法,但让我们忽略它。)

(3)可以使用do标记将IO操作组合为更大的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的类型为a的值)。重要的是要了解do块本身不会执行任何IO操作。取而代之的是,它构建一个新的IO动作,该IO动作(在执行时)将按照它们在do块中出现的顺序执行给定的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块中执行每个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 操作,在执行时,它会执行一些实际的I / O(即打开文件的操作系统调用),然后产生类型为Handle的值,这是代表打开文件句柄的Haskell值。在您的原始版本中,当您撰写时:

let inHandle = openFile "in.txt" ReadMode

所有这些操作都为IO操作分配了名称inHandle -它实际上没有执行IO操作,因此也没有真正打开文件。特别是,类型inHandle的{​​{1}}的值本身不是文件句柄,而只是用于产生文件句柄的IO操作(或“配方”)。

在上述IO Handle的版本中,我们使用了表示法:

myAction

表示,如果并且当执行由inHandle <- openFile "in.txt" ReadMode 命名的IO操作时,将从执行IO操作myAction开始(即,该表达式的类型为{ {1}}),该执行将产生一个openFile "in.txt" ReadMode",其名称为IO Handle。同上,下一行将产生并命名一个开放Handle。然后,我们将这些打开的句柄传递给表达式inHandle中的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。这意味着它是一个IO操作,特别是由loop inHandle outHandle定义中的外部do-block创建的IO操作。此do-block创建一个IO操作,该IO操作在执行时按顺序执行两个IO操作,如外部do-block的行所示。第一行是:

IO ()

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

do-block的第二行是整个end语句。它产生类型为Bool的值,因此执行第二个IO操作。 IO操作取决于if的值。如果IO ()为true,则IO操作将为end的值,如果执行该操作,则将不执行任何实际的I / O,并将产生类型为{{1}的值end }。如果return ()为假,则IO操作将为内部do块的值。此内部do块是一个IO操作(值为()类型),如果执行,将按顺序执行三个IO操作:

  1. IO操作(),类型为end的值,该值将在执行时从IO ()中读取一行并产生结果hGetLine inHandle。按照do-block,此结果将被命名为IO String

  2. IO操作inHandle,类型为String的值,执行后会将line写入hPutStrLn outHandle line

  3. IO操作IO (),对外部do-block产生的IO操作的递归使用,该外部do-block在执行时将重新开始整个过程​​,从EOF检查开始。

如果将这两个定义(用于lineoutHandle)放在程序中,它们将无能为力,因为它们只是IO操作的定义。执行它们的唯一方法是命名其中一个loop inHandle outHandle,如下所示:

myAction

当然,我们可以使用名称loop代替main来获得与整个程序相同的效果:

main :: IO ()
main = myAction

花一些时间将其与上面的“具体示例”进行比较,看看它有何不同和相似之处。特别是,您能找出我为什么写的信吗?

main

代替:

myAction

复制具有行数的行

要修改此程序以对行进行计数,一种相当标准的方法是将count设为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 ... 产生count的最终值。由于表达式if hIsEOF inHandle then ... 是一个IO操作(上面,它的类型为loop),要使其产生计数,我们需要像在您的操作中将其赋予类型loop。例。它将仍然是IO操作,但是现在-当它执行时-它会产生一个有用的loop inHandle outHandle值,而不是一个无用的IO ()值。

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

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

()

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

loop inHandle outHandle

,其中右侧将求值为IO ()类型的值,表示一个IO操作-当执行整个do-block时-将在轮到从以下位置复制行时执行将一个文件复制到另一个文件,然后生成类型为()的计数值命名为count <- loop inHandle outHandle 0 ,以供以后的do-block步骤使用。

无论如何,修改后的IO Int看起来像这样:

Int

现在,我们重写count以保持计数(通过递归调用将运行计数作为参数,并在执行IO操作时产生最终值):

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@

其余的问题

现在,您询问了如何在不使用其他功能的情况下在do-block中使用累加器。我不知道您是要使用loop以外的其他功能(在这种情况下,上面的答案可以满足要求),还是您根本不使用任何显式的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) 软件包中有单子循环组合器,可以使您执行以下操作(无需计数即可进行复制)。我还改用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 块有开销,还是引入了一些额外的处理管道之类的东西。它们只是构造IO操作值的方法。因此,您可以替换:

do

使用

loop

但这是纯粹的语法更改。否则两者是相同的(并且几乎肯定会编译为等效代码)。