Haskell is a pure functional language, so aspects like File IO become a bit more complicated than most other programming languages because state can be stored in files. To address these issues, Haskell has a lot of additional syntax and operations to work with files. I understand that need and why it is necessary. What I am confused with, is why this is required for input and output from main
?
A Haskell program which reads command line arguments and standard input and writes to standard output looks something like this:
import System.Environment
main = do
progName <- getProgName
args <- getArgs
contents <- getContents
putStr (operateOn contents)
This is annoying because main
is not a pure function and has to go through file IO operations in order to access inputs to the program. However, it is semantically "pure" in that it takes in a progName
, args
, and stdin while returning the stdout content. In my opinion, it would make far more sense for Haskell to recontextualize main
to take the inputs as arguments and return the output. Consider the following hypothetical function:
pureMain :: String -> [String] -> String -> String
pureMain progName args stdin = operateOn stdin
This version of main
is pure and accepts its inputs as parameters directly while returning the result. The values of each input do not change throughout the program's execution, and stdin can be a potentially-infinite string lazily evaluated. Laziness also works for stdout as each line can be printed individually without the requiring the entire potentially-infinite stdout to be evaluated.
I can understand some implementation details causing stdin and stdout to require file IO, as they are technically file descriptiors anyways. Things like logging also require the use of putStr
, which would make this "return stdout as a string" model a little impractical.
Even with those problems however, I see no reason why progName
and args
can't be passed in as arguments. For that matter, even start time or a random number seed could be passed in as well. As long as they don't change mid-execution and provide a hook in the Haskell runtime to specify a particular time or seed, then it should be completely pure and repeatable.
This idea seems too obvious not to have been tried, so I'm curious why it was not done for Haskell? Is there something I'm missing that breaks down this model which requires these to be IO-aware calls?