1

我有以下问题:

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

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

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

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

具体例子

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获取参数的方式

PS 2(在KA Buhr彻底回应后)

、我真正想要达到的,是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可能是运营商?我不知道,这就是我问的原因。

4

1 回答 1

6

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

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 asome的类型值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

所有这些只是为inHandleIO 操作分配一个名称——它实际上并没有执行 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 动作:

  1. IO actionhGetLine inHandle是一个类型的值IO String,在执行时将从中读取一行inHandle并生成结果String。根据 do-block,这个结果将被命名为 name line

  2. IO action hPutStrLn outHandle line,一个类型的值,执行时将IO ()写入.lineoutHandle

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

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

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

于 2018-07-24T23:41:58.973 回答