【问题标题】:Haskell pre-monadic I/OHaskell pre-monadic I/O
【发布时间】:2013-06-04 19:46:55
【问题描述】:

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

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

【问题讨论】:

标签: haskell io monads


【解决方案1】:

在引入 IO monad 之前,main[Response] -> [Request] 类型的函数。 Request 表示 I/O 操作,例如写入通道或文件、读取输入或读取环境变量等。Response 将是此类操作的结果。例如,如果您执行了ReadChanReadFile 请求,则对应的Response 将是Str str,其中str 将是包含读取输入的String。当执行AppendChanAppendFileWriteFile 请求时,响应将只是Success。 (当然,假设在所有情况下给定的操作实际上是成功的)。

因此,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 Report 的第 7 章给出了该模型的详细规格。


在现代 Haskell 中可以不使用 IO Monad 来完成 I/O 吗?

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

但是,您可以做的是编写一个函数,该函数采用旧式 main 函数并将其转换为 IO 操作。然后,您可以使用旧样式编写所有内容,然后仅在 main 中使用 IO,您只需在真正的主函数上调用转换函数。这样做几乎肯定会比使用 IO monad 更麻烦(并且会让任何现代 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

【讨论】:

  • 您可以使用 unsafeInterleaveIO 代替 unsafePerformIO。那将是旧 Haskell 过去使用的惰性 I/O 模型。您可能还可以通过使用 io-streams、管道或管道来给它一个现代的旋转。
  • unsafeInterleaveIO 绝对是这样做的方法。
【解决方案2】:

我更喜欢一个适用于现代 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 确实出现了。

【讨论】:

    【解决方案3】:

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

    对此我感到非常抱歉。我认为没有 unsafePerformIO 是不可能写出这个函数的

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

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

    (注意: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)
    

    【讨论】:

    • 这对我来说看起来很不对劲,因为您在 dialogToIOMonad 的实现中多次调用 dialog。一个正确的实现应该只调用一次dialog,并且应该为它提供一个惰性响应列表。我认为您可以使用unsafeInterleaveIO 而不是unsafePerformIO,但您不能在不重复工作的情况下以纯粹的方式做到这一点。
    • 使用 unsafeInterleaveIO 仍然会导致使用 unsafe* 函数,我想尽一切办法避免这种情况。我希望看到只调用一次对话框的实现。
    猜你喜欢
    • 1970-01-01
    • 2017-12-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-07-05
    • 1970-01-01
    • 1970-01-01
    • 2012-06-02
    相关资源
    最近更新 更多