E/S premonĂ¡lica Haskell
io monads (2)
Me pregunto cómo se hicieron las operaciones de E / S en Haskell en los días en que todavía no se inventó la mónada IO. Cualquiera conoce un ejemplo.
Editar: ¿Puedo hacer I / O sin la IO Monad en el moderno Haskell? Prefiero un ejemplo que funcione con GHC moderno.
@ sepp2k ya aclaró cómo funciona esto, pero quería agregar algunas palabras, akhem, algunas líneas de código.
Realmente lo siento por esto. No creo que sea posible escribir esta función sin inseguroPerformIO
¡Por supuesto que puede! Casi nunca deberías usar insafePerformIO http://chrisdone.com/posts/haskellers
Estoy usando un constructor de tipo de Request
ligeramente diferente, para que no tome la versión de canal (como stdin
/ stdout
en el código de @sepp2k ). Aquí está mi solución para esto:
(Nota: getFirstReq
no funciona en la lista vacía, tendrías que agregar un caso para eso, pero debería ser trivial)
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)
Antes de que se introdujera la mónada IO, main
era una función del tipo [Response] -> [Request]
. Una Request
representaría una acción de E / S, como escribir en un canal o un archivo, o leer datos de entrada, o leer variables de entorno, etc. Una Response
sería el resultado de dicha acción. Por ejemplo, si realizó una solicitud de ReadChan
o ReadFile
, la Response
correspondiente sería Str str
donde str
sería una String
contiene la entrada de lectura. Al realizar una AppendChan
, AppendFile
o WriteFile
, la respuesta sería simplemente Success
. (Suponiendo, en todos los casos, que la acción dada fue realmente exitosa, por supuesto).
De modo que un programa Haskell funcionaría al crear una lista de valores de Request
y leer las respuestas correspondientes de la lista que se da a main
. Por ejemplo, un programa para leer un número del usuario puede verse así (dejando de lado el manejo de errores por simplicidad):
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
Como Stephen Tetley ya señaló en un comentario, en el capítulo 7 del haskell.cs.yale.edu/wp-content/uploads/2011/01/… se da una especificación detallada de este modelo.
¿Puedo hacer I / O sin la IO Monad en el moderno Haskell?
No. Haskell ya no es compatible con la forma de Response
/ Request
de hacer IO directamente y el tipo de main
ahora es IO ()
, por lo que no puedes escribir un programa Haskell que no implique IO
e incluso si pudieras, hubieras todavía no tiene forma alternativa de hacer ninguna E / S.
Sin embargo, lo que puede hacer es escribir una función que adopte una función principal antigua y la convierta en una acción IO. Luego puede escribir todo usando el estilo anterior y luego solo usar IO en main
donde simplemente invocaría la función de conversión en su función principal real. Hacerlo sería casi más complicado que usar la mónada IO
(y confundiría a cualquier Haskeller moderno leyendo tu código), así que definitivamente no lo recomendaría. Sin embargo, es posible. Tal función de conversión podría verse así:
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
A continuación, puede utilizar esta función de esta manera:
-- 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