55

我想知道在 IO monad 还没有发明的时候,Haskell 是如何完成 I/O 的。任何人都知道一个例子。

编辑:可以在没有现代 Haskell 中的 IO Monad 的情况下完成 I/O 吗?我更喜欢一个适用于现代 GHC 的示例。

4

3 回答 3

65

在引入 IO monad 之前,main它是 type 的函数[Response] -> [Request]。ARequest将代表一个 I/O 操作,例如写入通道或文件,或读取输入,或读取环境变量等。AResponse将是此类操作的结果。例如,如果您执行了ReadChanorReadFile请求,则对应的Response将是包含读取输入的 aStr str所在的位置。执行, or请求时,响应将只是. (当然,假设在所有情况下,给定的操作实际上是成功的)。strStringAppendChanAppendFileWriteFileSuccess

因此,Haskell 程序将通过建立一个Request值列表并从给定的列表中读取相应的响应来工作main。例如,从用户那里读取数字的程序可能如下所示(为简单起见,省略了任何错误处理):

main :: [Response] -> [Request]
main responses =
  [
    AppendChan "stdout" "Please enter a Number\n",
    ReadChan "stdin",
    AppendChan "stdout" . show $ enteredNumber * 2
  ]
  where (Str input) = responses !! 1
        firstLine = head . lines $ input
        enteredNumber = read firstLine 

正如 Stephen Tetley 已经在评论中指出的那样,该模型的详细规格在1.2 Haskell 报告的第 7 章中给出。


可以在没有现代 Haskell 中的 IO Monad 的情况下完成 I/O 吗?

不,Haskell 不再支持Response/Request直接做 IO 的方式,类型main是 now IO (),所以你不能写一个不涉及的 Haskell 程序,IO即使你可以,你仍然没有替代的方式做任何 I/O。

但是,您可以做的是编写一个函数,该函数采用旧式主函数并将其转换为 IO 操作。然后,您可以使用旧样式编写所有内容,然后仅main在您只需在真正的主函数上调用转换函数的地方使用 IO。这样做几乎肯定会比使用IOmonad 更麻烦(并且会让任何现代 Haskeller 阅读您的代码感到困惑),所以我绝对不会推荐它。然而这可能的。这样的转换函数可能如下所示:

import System.IO.Unsafe

-- Since the Request and Response types no longer exist, we have to redefine
-- them here ourselves. To support more I/O operations, we'd need to expand
-- these types

data Request =
    ReadChan String
  | AppendChan String String

data Response =
    Success
  | Str String
  deriving Show

-- Execute a request using the IO monad and return the corresponding Response.
executeRequest :: Request -> IO Response
executeRequest (AppendChan "stdout" message) = do
  putStr message
  return Success
executeRequest (AppendChan chan _) =
  error ("Output channel " ++ chan ++ " not supported")
executeRequest (ReadChan "stdin") = do
  input <- getContents
  return $ Str input
executeRequest (ReadChan chan) =
  error ("Input channel " ++ chan ++ " not supported")

-- Take an old style main function and turn it into an IO action
executeOldStyleMain :: ([Response] -> [Request]) -> IO ()
executeOldStyleMain oldStyleMain = do
  -- I'm really sorry for this.
  -- I don't think it is possible to write this function without unsafePerformIO
  let responses = map (unsafePerformIO . executeRequest) . oldStyleMain $ responses
  -- Make sure that all responses are evaluated (so that the I/O actually takes
  -- place) and then return ()
  foldr seq (return ()) responses

然后你可以像这样使用这个函数:

-- In an old-style Haskell application to double a number, this would be the
-- main function
doubleUserInput :: [Response] -> [Request]
doubleUserInput responses =
  [
    AppendChan "stdout" "Please enter a Number\n",
    ReadChan "stdin",
    AppendChan "stdout" . show $ enteredNumber * 2
  ]
  where (Str input) = responses !! 1
        firstLine = head . lines $ input
        enteredNumber = read firstLine 

main :: IO ()
main = executeOldStyleMain doubleUserInput
于 2013-06-08T22:21:31.053 回答
1

我更喜欢一个适用于现代 GHC 的示例。

对于 GHC 8.6.5:

import Control.Concurrent.Chan(newChan, getChanContents, writeChan) 
import Control.Monad((<=<))

type Dialogue = [Response] -> [Request]
data Request  = Getq | Putq Char
data Response = Getp Char | Putp

runDialogue :: Dialogue -> IO ()
runDialogue d =
  do ch <- newChan
     l <- getChanContents ch
     mapM_ (writeChan ch <=< respond) (d l)

respond :: Request -> IO Response
respond Getq     = fmap Getp getChar
respond (Putq c) = putChar c >> return Putp

其中类型声明来自Philip Wadler的How to Declare an Imperative的第 14 页。测试程序留给好奇的读者作为练习:-)

如果有人想知道:

 -- from ghc-8.6.5/libraries/base/Control/Concurrent/Chan.hs, lines 132-139
getChanContents :: Chan a -> IO [a]
getChanContents ch
  = unsafeInterleaveIO (do
        x  <- readChan ch
        xs <- getChanContents ch
        return (x:xs)
    )

是的 -unsafeInterleaveIO确实出现了。

于 2020-06-04T12:24:18.953 回答
0

@sepp2k已经阐明了这是如何工作的,但我想补充几句

我真的很抱歉。我认为没有 unsafePerformIO 就不可能编写这个函数

当然可以,你几乎不应该使用 unsafePerformIO http://chrisdone.com/posts/haskellers

我正在使用稍微不同的Request类型构造函数,因此它不需要通道版本(stdin/stdout就像在@sepp2k 的代码中一样)。这是我的解决方案:

(注意:getFirstReq不适用于空列表,您必须为此添加一个案例,但这应该是微不足道的)

data Request = Readline
             | PutStrLn String

data Response = Success
              | Str String

type Dialog = [Response] -> [Request]


execRequest :: Request -> IO Response
execRequest Readline = getLine >>= \s -> return (Str s)
execRequest (PutStrLn s) = putStrLn s >> return Success


dialogToIOMonad :: Dialog -> IO ()
dialogToIOMonad dialog =
    let getFirstReq :: Dialog -> Request
        getFirstReq dialog = let (req:_) = dialog [] in req

        getTailReqs :: Dialog -> Response -> Dialog
        getTailReqs dialog resp =
            \resps -> let (_:reqs) = dialog (resp:resps) in reqs
    in do
        let req = getFirstReq dialog
        resp <- execRequest req
        dialogToIOMonad (getTailReqs dialog resp)
于 2017-06-06T18:32:14.377 回答