缩短处理IO的代码

时间:2012-02-28 00:05:31

标签: scala haskell

我编写了一小段代码来处理控制台的输入:

main :: IO ()
main = do
  input <- readLine "> "
  loop input

loop :: String -> IO ()
loop input = do
  case input of
    [] -> do
      new <- readLine "> "
      loop new
    "quit" ->
      return ()
    _ -> do
      handleCommand input
      new <- readLine "> "
      loop new

handleCommand :: String -> IO ()
handleCommand command = do
  case command of
    "a" -> putStrLn "it was a"
    "b" -> putStrLn "it was b"
    _ -> putStrLn "command not found"

readLine :: String -> IO String
readLine prompt = do
  putStr prompt
  line <- getLine
  return line

代码工作正常,但它看起来很丑陋而且多余。在Scala中,我成功地将其写得更短:

object Test extends App {
  val reader = Iterator.continually(readLine("> "))
  reader takeWhile ("quit" !=) filter (_.nonEmpty) foreach handleCommand

  def handleCommand(command: String) = command match {
    case "a" => println("it was a")
    case "b" => println("it was b")
    case _ => println("command not found")
  }
}

我尝试在Haskell中使用IO Monad的高阶函数,但是我失败了。有人能举例说明如何缩短Haskell代码吗?

另一个问题是输出顺序不同。在Scala中它是正确的:

$ scala Test
> hello
command not found
> a
it was a
> b
it was b
> quit

而在Haskell中则不是:

$ ./test
hello
> command not found
a
> it was a
b
> it was b
quit
> %

如何解决这个问题?

4 个答案:

答案 0 :(得分:14)

import System.IO

main = putStr "> " >> hFlush stdout >> getLine >>= \input ->
    case input of
        "quit" -> return ()
        "a"    -> putStrLn "it was a" >> main
        "b"    -> putStrLn "it was b" >> main
        _      -> putStrLn "command not found" >> main

比Scala imo更短更清晰。

答案 1 :(得分:10)

这是一个更简洁的Haskell版本,并按照您的预期打印提示:

import System.IO

main :: IO ()
main = readLine "> " >>= loop

loop :: String -> IO ()
loop ""     = readLine "> " >>= loop
loop "quit" = return ()
loop input  = handleCommand input >> readLine "> " >>= loop

handleCommand :: String -> IO ()
handleCommand "a" = putStrLn "it was a"
handleCommand "b" = putStrLn "it was b"
handleCommand _   = putStrLn "command not found"

readLine :: String -> IO String
readLine prompt = putStr prompt >> hFlush stdout >> getLine

如果你想避免显式递归,你可以使用Control.Monad.forever(顺便说一下,它有一个奇怪而漂亮的类型:Monad m => m a -> m b):

import Control.Monad (forever)
import System.Exit (exitSuccess)
import System.IO (hFlush, stdout)

main :: IO ()
main = forever $ putStr "> " >> hFlush stdout >> getLine >>= handleCommand
  where
    handleCommand ""     = return ()
    handleCommand "quit" = exitSuccess
    handleCommand "a"    = putStrLn "it was a"
    handleCommand "b"    = putStrLn "it was b"
    handleCommand _      = putStrLn "command not found"

请参阅this FAQ answer,了解如何在没有hFlush stdout的情况下“无序”打印提示。

答案 2 :(得分:2)

出现加扰输出,因为stdout是行缓冲的(它只在每个换行符时写入终端)。您应该切换到stderr,这是始终应该用于交互式应用程序的,或者您应该关闭stdout的缓冲:

import System.IO
-- ...
hSetBuffering stdout NoBuffering

其余代码非常简洁,但您不必拥有单独的循环函数:

main = do
  command <- readLine "> "
  case command of
    "quit" -> return ()
    ""     -> main
    _      -> handleCommand command >> main

您当然也可以避免额外的case..of表达式和一些do块,但有些人更喜欢使用更明确的样式。

答案 3 :(得分:2)

我将如何做到这一点:

prompt :: String -> IO String
prompt str = putStr str >> hFlush stdout >> getLine

main :: IO ()
main = do
  cmd <- prompt "> "
  case cmd of
    "" -> main
    "quit" -> return ()
    _ -> putStrLn (handleCommand cmd) >> main

handleCommand :: String -> String
-- define the usual way

如果你试图音译Scala,你可以试试这个,虽然它会错误

promptForever :: String -> IO [String]
promptForever str = sequence (repeat $ prompt str)

main = do
  reader <- promptForever "> "
  forM_ (takeWhile (/= "quit") . filter (not . null) $ reader)
    (putStrLn . handleCommand)

问题,有趣的是,在这种情况下,Haskell太严格了:它确实会永远提示你,即使你可能希望它会因为懒惰而在路上吐出答案。由于Haskell的IO如何与类型系统一起工作,Iterator.continually(readLine("> "))的概念根本不能(据我所知)被直接翻译成Haskell。