Before the IO monad was introduced, main was a function of type [Response] -> [Request] . A Request will be an I / O operation, such as writing to a channel or file, reading input or reading environment variables, etc. A Response will be the result of such an action. For example, if you ReadChan or ReadFile , the corresponding Response would be Str str , where str would be a String containing the read input. When executing an AppendChan , AppendFile or WriteFile request, the response will simply be Success . (Assuming, in all cases, that the action was truly successful, of course).
So, the Haskell program will work by creating a list of Request values ββand reading the appropriate responses from the list specified in main . For example, a program for reading a number from a user may look like this (without any simplicity):
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
As Stephen Tetley noted in a comment, a detailed specification of this model is given in Chapter 7 1.2 of the Haskell Report .
Can I / O work without IO Monad in modern Haskell?
No. Haskell no longer supports the Response / Request method for entering IO directly, and the main type is now IO () , so you cannot write a Haskell program that does not include IO , and even if you could, you still would not have an alternative way input / output.
However, you can write a function that takes the main function of the old style and turns it into an I / O action. Then you can write everything using the old style, and then use only IO in main , where you just call the conversion function on your real main function. It would almost certainly be more cumbersome than using the IO monad (and confuse the hell out of any modern Haskeller reading your code), so I would definitely not recommend it. However, this is possible. Such a conversion function might look like this:
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
Then you can use this function as follows:
-- 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