Haskell - 循环读取用户输入

14

我有一个Haskell程序,需要从用户读取任意行的输入,当用户完成后,累积的输入必须发送到一个函数中。

在一种命令式编程语言中,它会是这个样子:

content = ''
while True:
    line = readLine()
    if line == 'q':
        break
    content += line
func(content)

我发现在 Haskell 中这样做非常困难,因此我想知道是否有类似 Haskell 的替代方法。

4个回答

21

Haskell中迭代的等效方法是递归。如果要读取输入行,您还需要使用IOmonad。总体来说:

import Control.Monad

main = do
  line <- getLine
  unless (line == "q") $ do
    -- process line
    main

如果您只想累积content中的所有读取行,您不必这样做。只需使用getContents,它将(惰性地)检索所有用户输入。当您看到'q'时,请停止。在相当习惯用的Haskell中,所有读取可以在一行代码中完成:

main = mapM_ process . takeWhile (/= "q") . lines =<< getContents
  where process line = do -- whatever you like, e.g.
                          putStrLn line

如果你从右到左读代码的第一行,它的意思是:

  1. 获取用户提供的所有输入(不用担心,这是惰性的);

  2. 按照输入的顺序将其拆分成多行;

  3. 只取长度不等于 "q" 的行,直到遇到这样的行为止;

  4. 对每一行调用 process 函数。

如果你还没有理解,你需要仔细阅读 Haskell 教程!


我会使用 getLine 而不是 readLn 来获取可执行版本,并且添加 import Control.Monad - jev
@jev,同意。我使用了readLn使其更通用,但对于只想读取行的人可能会感到困惑。 - nickie

9
在Haskell中,这相当简单。最棘手的部分是你想要累积用户输入序列。在命令式语言中,你使用循环来实现,而在Haskell中,规范的方式是使用递归辅助函数。它看起来应该像这样:
getUserLines :: IO String                      -- optional type signature
getUserLines = go ""
  where go contents = do
    line <- getLine
    if line == "q"
        then return contents
        else go (contents ++ line ++ "\n")     -- add a newline

这实际上是一个返回String的IO操作的定义。由于它是一个IO操作,您使用<-语法而不是=赋值语法访问返回的字符串。如果您想快速了解概述,我建议阅读The IO Monad For People Who Simply Don't Care

您可以在GHCI提示符下使用此函数如下:

>>> str <- getUserLines
Hello<Enter>     -- user input
World<Enter>     -- user input
q<Enter>         -- user input
>>> putStrLn str
Hello            -- program output
World            -- program output

2
现在,你真的会用Haskell写这个吗?每次将“line”附加到“contents”会导致性能下降。最终想要的“contents”是单次调用“getContents”将给出的前缀。 - nickie
1
这是一个很中肯的观点 - 但我认为值得解释一下如何从头开始做到这一点,以便对在IO单子(可能是新手最困惑的Haskell部分)中工作有所感觉。 它还有一个好处,就是将用户输入与要在输入中处理的处理分开,而您的答案没有。 我会添加关于 getContents 的附录。 - Chris Taylor
好的,我明白了,我撤回我的一开始的-1。但是,撇开教育目的不谈,我认为像这样的代码是糟糕的Haskell。对于关心的人来说,至少... :-) - nickie

2

使用即将发布的 pipes-4.0 版本:

import Pipes
import qualified Pipes.Prelude as P

f :: [String] -> IO ()
f = ??

main = do
    contents <- P.toListM (P.stdinLn >-> P.takeWhile (/= "q"))
    f contents

这会将所有行加载到内存中。然而,您也可以在生成每一行时进行处理:

f :: String -> IO ()

main = runEffect $
    for (P.stdinLn >-> P.takeWhile (/= "q")) $ \str -> do
        lift (f str)

那将会流式传输输入,不会在内存中加载超过一行的内容。

1
你可以像这样做:

import Control.Applicative ((<$>))

input <- unlines . takeWhile (/= "q") . lines <$> getContents

用户输入的内容将是 q 之前(但不包括 q)用户编写的内容。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接