Haskell:使用Interact导致错误

4

我试图使用interact函数,但是以下代码出现了问题:

main::IO()
main = interact test

test :: String -> String
test [] = show 0
test a = show 3

我正在使用EclipseFP,输入一个值后出现了错误。尝试再次运行主程序,会导致以下错误:

*** Exception: <stdin>: hGetContents: illegal operation (handle is closed)

我不确定为什么这不起作用,测试类型是String -> String,显示为Show a => a -> String,因此它似乎应该是interact的有效输入。
编辑/更新:
我尝试了以下方法,它可以正常工作。使用unlines和lines如何使interact按预期工作?
main::IO()
main = interact respondPalindromes

respondPalindromes :: String -> String
respondPalindromes =
    unlines .
    map (\xs -> if isPal xs then "palindrome" else "not a palindrome") .
    lines

isPal :: String -> Bool
isPal xs = xs == reverse xs

4
这是GHCI已知的一个烦人特性。请参见这里 - n. m.
@n.m. 谢谢你提供的链接,但是链接中提供的 "解决方案",如 :load:reload:set +r 均无效。你有什么其他想法吗?我不想在每次执行 getContents >>= print 后都需要重新启动 ghci。 - vikingsteve
1个回答

7

GHCi和不安全的I/O

你可以将该问题(异常)归纳为:

main = getContents >> return ()

(interact 调用 getContents)

问题在于在调用 main 之间,GHCi 中的 stdin(实际上是 hGetContents stdin)保持已评估状态。如果查找 stdin,可以看到它的实现方式为:

stdin :: Handle
stdin = unsafePerformIO $ ...

为了说明这是个问题,您可以将其加载到GHCi中:
import System.IO.Unsafe                                                                                                           

f :: ()                                                                                                                           
f = unsafePerformIO $ putStrLn "Hi!"

然后,在GHCi中执行以下操作:
*Main> f
Hi!
()
*Main> f
()

自从我们使用了unsafePerformIO并告诉编译器f是一个纯函数,它认为不需要再次评估它。在stdin的情况下,句柄上的所有初始化都不会再次运行,并且它仍处于半关闭状态(由hGetContents引起),这会导致异常。因此,在这种情况下,我认为GHCi是“正确”的,问题在于stdin的定义,它是编译后程序的实用便利,只会评估一次stdin
关于为什么interact在输入一行后就退出而unlines . lines版本继续进行,让我们也来简化一下:
main :: IO ()
main = interact (const "response\n")

如果您测试上述版本,交互甚至在打印响应之前都不会等待输入。为什么?这是“interact”的源代码(在GHC中):
interact f = do s <- getContents
                putStr (f s)

getContents 是惰性 I/O,在这种情况下,由于 f 不需要 s,因此不会从 stdin 中读取任何内容。

如果您将测试程序更改为:

main :: IO ()
main = interact test

test :: String -> String
test [] = show 0
test a = show a

您应该注意到不同的行为。这表明在您的原始版本(test a = show 3)中,编译器足够聪明,以此只需要足够的输入来确定读取的字符串是否为空(因为如果不为空,它不需要知道 a 是什么,它只需要打印 "3")。由于输入在终端上预计是按行缓冲的,因此会一直读取,直到您按下回车键。


你能看一下我的更新并解释一下为什么新增的代码有效吗?我不确定新增的代码如何影响stdin/unsafePerformIO。 - Vidhur Vohra
我添加了一个解释,说明为什么 'interact' 在您传递给它的函数不同的情况下会有不同的行为,以及延迟I/O的工作原理。您的问题涉及两个单独的问题。希望这可以澄清。 - jekor

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