Search code examples
haskellioprogram-entry-point

What's the use of main returning IO Something rather than IO()?


I'm reading http://learnyouahaskell.com/ ... And something surprised me:

Because of that, main always has a type signature of main :: IO something, where something is some concrete type.

? So main doesn't have to be of type IO(), but rather can be IO(String) or IO(Int)? But what's the use of this?

I did some playing...

m@m-X555LJ:~$ cat wtf.hs
main :: IO Int
main = fmap (read :: String -> Int) getLine
m@m-X555LJ:~$ runhaskell wtf.hs
1
m@m-X555LJ:~$ echo $?
0
m@m-X555LJ:~$

Hmm. So my first hypothesis is disproven. I thought this was a way for a Haskell program to return exit status to the shell, much like a C program starts from int main() and reports the exit status with return 0 or return 1.

But nope: the above program consumes the 1 from input and then does nothing, and in particular doesn't seem to return this 1 to the shell.

One more test:

m@m-X555LJ:~$ cat wtf.hs
main = getContents
m@m-X555LJ:~$ runhaskell wtf.hs
m@m-X555LJ:~$ 

Wow. This time I tried returning IO String. For reasons unknown to me, this time Haskell doesn't even wait for input, as it did when I was returning IO Int. The program seems to simply do nothing.

This hints that the value is really not returned anywhere: apparently, since the results of getContents are nowhere used, the whole instruction was skipped due to laziness. But if this was the case, why was returning IO Int not skipped? Well yes: I did fmap read on the IO action; but same stuff seems to apply, computing the read is only necessary if the result of the action is used, which - as the main = getContents example seems to hint - is not used, so laziness should also skip the read and hence also the getLine, right? Well, wrong - but I'm confused why.

What's the use of returning IO Something from main rather than only IO ()?


Solution

  • This is actually multiple questions, but in order:

    1. The 'result' of main doesn't have a meaning, that is why it can be () or anything else. It's not used at all.
    2. The reason types other than IO () are allowed for main is for convenience; Otherwise you'd always have to do something like main = void $ realMain to discard results (you may well want to have an action that could return a result which you don't care about as the last thing that happens) which is a bit tedious. IMHO silently discarding things is bad and so I'd prefer if main was forced to be :: IO (), but you can always get that effect by just supplying the type signature yourself so it's not really a problem in practice.
    3. Side point: If you want to exit with a specific exit code, use System.Exit
    4. The reason fmap read getLine consumes output and getContents doesn't is because getContents is lazy and getLine isn't - i.e. getLine does read a line of text where you'd think it does, whereas getContents only does any actual IO if the result is "needed" in the Haskell world; Since the result of IO isn't used for anything that means it doesn't do anything if getContents is your whole main.