Haskell中的递归IO

5
在Haskell中,我可以轻松地定义一个递归函数,该函数接受一个值并返回一个字符串:
Prelude> let countdown i = if (i > 0) then (show i) ++ countdown (i-1) else ""
Prelude> countdown 5
"54321"

我希望使用相同的设计从文件句柄中读取可用数据。在这种特殊情况下,我需要以与hGetContents相同的方式读取数据,但不会使句柄处于“半关闭”状态,以便我可以循环与使用createProcess打开的stdin / stdout处理交互:

main = do
    -- do work to get hin / hout handles for subprocess input / output

    hPutStrLn hin "whats up?"

    -- works
    -- putStrLn =<< hGetContents hout

    putStrLn =<< hGetLines hout

    where
        hGetLines h = do
            readable <- hIsReadable h
            if readable
                then hGetLine h ++ hGetLines h
                else []

出现以下错误:
Couldn't match expected type `IO b0' with actual type `[a0]'
In the expression: hGetLine h : hGetLines h

我知道有各种库可用于完成我想要完成的任务,但由于我正在学习,所以我的问题实际上是如何执行递归IO。谢谢!

3个回答

12

朴素解法,严格的O(n)

你仍然需要使用do表示法,这将导致以下代码:

import System.IO
import System.IO.Unsafe (unsafeInterleaveIO)

-- Too strict!
hGetLines :: Handle -> IO [String]
hGetLines h = do
    readable <- hIsReadable h
    if readable
        then do
            x  <- hGetLine h
            xs <- hGetLines h
            return (x:xs)
        else return []

但请注意我的评论,这个版本的hGetLines太严格了!

慵懒、流式的版本

它直到读取所有输入后才会返回你的列表。你需要更懒惰的东西。为此,我们有unsafeInterleaveIO

-- Just right
hGetLines' :: Handle -> IO [String]
hGetLines' h = unsafeInterleaveIO $ do
    readable <- hIsReadable h
    if readable
        then do
            x  <- hGetLine h
            xs <- hGetLines' h
            return (x:xs)
        else return []

现在你可以逐行开始将结果流式传输到你的消费者代码:

*Main> hGetLines' stdin
123
["123"345
,"345"321
,"321"^D^CInterrupted.

6

如果你在ghci中检查(++)的类型,你会得到:

Prelude> :t (++)
(++) :: [a] -> [a] -> [a]

意思是您只能将列表附加在一起(请记住,String[Char] 的别名,因此它也是一个列表)。hGetLine 的类型是 Handle -> IO String,而 hGetLines 的类型应该是 IO [String],因此您不能附加这些值。在这里使用 (:),它的类型为 a -> [a],效果更好。
if readable
  then do
    -- First you need to extract them
    a <- hGetLine h
    b <- hGetLines h
    -- a and b have type String
    -- Now we can cons them and then go back into IO
    return (a : b)

同样适用于else []。您需要返回类型为IO [String]的值。将其更改为return []
此外,您无法仅使用putStrLn打印这些行,因为(=<< hGetLines h)给出的是[String]而不是String,而putStrLn所需的是String
有几种方法可以解决这个问题:一种是先连接这些值。putStrln . concat =<< (hGetLines h)。或者您可以使用mapM_ putStrLn (hGetLines h)打印每一行。

你是不是想在第二个调用中调用 hGetLines 函数? - Don Stewart
糟糕,我错过了递归调用,应该使用“:”代替。 - Adam Bergmark
请注意,此示例不会流式传输,并使用*O(n)*堆栈。 - Don Stewart

-1
这段话的意思是代码的一部分期望 hGetLines h 的类型为 IO a,而另一部分发现它的类型为 [a]。你可能想要将 if 语句改为:
if readable
    then return hGetLine h ++ hGetLines h
    else return []

2
你的代码有点奇怪...它甚至无法编译。这样怎么样:if readable then hGetLine >>= \a -> hGetLine >>= \b -> return $ a + b else return []?另一个问题是,它不支持流。 - fuz

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