Haskell:立即从控制台读取输入字符,而不是在换行符后

34

我尝试过这个:

main = do
    hSetBuffering stdin NoBuffering 
    c <- getChar

但它会等待用户按下回车键,这不是我想要的。我希望能够立即读取用户按下的字符。

我正在使用 Windows 7 上的 GHC v6.12.1。

编辑:对我来说解决方法是从 GHC 切换到支持此功能的 WinHugs。


4
这并不是一个好的解决方法。真正的解决方法是明确选择字符缓冲I/O,使用系统conio.h中的getch函数。Artelius的链接包含了相应的示例代码。 - jrockway
1
如果这个方法适合他的话,那么这是一个不错的解决方案!使用另一款没有这个 bug 的工具是有道理的。如果你不需要最近 ghc 编译器的语言特性,根据我的经验,WinHugs 比 ghci 或 winghci 更快。它可以无需麻烦地运行,并且看起来更漂亮。你也不需要在编辑代码后执行":r",这一点我很喜欢。 - AndrewC
它与 main = do hSetBuffering stdin NoBuffering; interact $ map Data.Char.toUpper 有什么关系?在我的情况下,它会在任何输出出现之前等待一个新行。(Ubuntu,GHC 7.6.3。) - ominug
6个回答

23

是的,这是一个 bug。以下是一种解决方法,可以避免用户不断点击和滚动:

{-# LANGUAGE ForeignFunctionInterface #-}
import Data.Char
import Foreign.C.Types
getHiddenChar = fmap (chr.fromEnum) c_getch
foreign import ccall unsafe "conio.h getch"
  c_getch :: IO CInt

因此,您可以将对getChar的调用替换为对getHiddenChar的调用。

请注意,这只是针对Windows上的ghc/ghci的一种解决方法。例如,winhugs没有这个bug,因此在winhugs中不起作用。


3
我喜欢这个解决方案。请注意,然而,unsafe 会导致一个阻塞调用 getch 的操作阻塞程序中的所有其他线程。我是 hidden-char 包在 Hackage 上的作者,它使用了不安全的 FFI 来实现此函数的变体。 - Richard Cook
1
如果在 GHCi 中出现类似以下内容的错误: ByteCodeLink: can't find label During interactive linking, GHCi couldn't find the following symbol: getch 请尝试将 getch 替换为 _getch 以进行外部导入。 - Oly

21

可能是个bug:

http://hackage.haskell.org/trac/ghc/ticket/2189

下面的程序会重复输入的字符,直到按下Esc键。

import IO
import Monad
import Char

main :: IO ()
main = do hSetBuffering stdin NoBuffering
          inputLoop

inputLoop :: IO ()
inputLoop = do i <- getContents
               mapM_ putChar $ takeWhile ((/= 27) . ord) i

由于 hSetBuffering stdin NoBuffering line,不需要在按键之间按下回车键。这个程序在 WinHugs(2006年9月版本)中可以正常工作。然而,在 GHC 6.8.2 中,直到按下回车键之前,字符才会被重复打印。在 Windows XP Professional 上,使用 cmd.exe 和 command.com,所有 GHC 可执行文件(ghci、ghc、runghc、runhaskell)都出现了这个问题...


4

嗯...实际上我认为这个功能并不是一个bug。当你读取stdin时,意味着你想要使用一个“文件”,而当你关闭缓冲时,你是在说不需要读取缓冲区。但这并不意味着模拟该“文件”的应用程序不应该使用写入缓冲区。对于Linux,如果你的终端处于“icanon”模式下,它不会发送任何输入,直到发生某些特殊事件(例如按下Enter或Ctrl + D)。可能Windows的控制台也有类似的模式。


谢谢。听起来很真实,但如果您看到错误描述:它确实完全符合我的要求,所以现在我将标记Artelius的答案。 - Steves
3
在Windows和Linux下,它的工作方式是不同的。在Linux下,Steves发布的代码可以在不等待的情况下工作。因此,我认为这是一个错误,应该予以修复。 - Dmitry Bespalov
这部分是(部分)不正确的。当使用控制台仿真软件或MinGW控制台时,缓冲仍然被忽略。Windows在操作系统级别忽略缓冲,而不是在控制台级别。 - yyny

4
Haskeline 包对我很有用。
如果您需要单个字符,请稍微更改示例即可。
  1. getInputLine 变成 getInputChar
  2. "quit" 变成 'q'
  3. ++ input 变成 ++ [input]
main = runInputT defaultSettings loop
    where 
        loop :: InputT IO ()
        loop = do
            minput <- getInputChar "% "
            case minput of
                Nothing -> return ()
                Just 'q' -> return ()
                Just input -> do outputStrLn $ "Input was: " ++ [input]
                                 loop

0

我使用了haskeline包,这是其他答案中提到的,用于组合这个简单的getChar替代方案。如果getInputChar返回Nothing,它会再次请求输入。这对我解决了问题,您可以根据需要进行修改。

import System.Console.Haskeline
  ( runInputT
  , defaultSettings
  , getInputChar
  )

betterInputChar :: IO Char
betterInputChar = do
  mc <- runInputT defaultSettings (getInputChar "")
  case mc of
    Nothing -> betterInputChar
    (Just c) -> return c


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