Feedback requested: Simple proposal to set program exit code by main's value

Hi all,

I have written a simple proposal that would allow for the value yielded by main to set the program’s exit status, in a well-typed way. The committee has requested more feedback from users, if you have an opinion on this either way (especially if you’re a new user or are frequently involved in teaching Haskell) your feedback would be appreciated!

From the motivation section:

Many languages, perhaps most notably C, allow specifying a program’s exit
code by returning an appropriate value from main. Haskell98 does
allow a main which yields values of any type, but surprisingly it does not
do anything with the result: any successful execution will result in a successful
program exit, even if main yields 1 or ExitFailure 1.

Before this proposal, the following program would exit with code 0 even
when there are no results:

main :: IO ExitCode
main = do
  results <- doSomeWork
  case results of
    [] -> pure (ExitFailure 1)
    _ -> print results >> pure ExitSuccess

With this proposal in place, the program would exit with code 1 in that
case.

This proposal follows Rust in using a typeclass for return types which can signal exit codes.

This is a minor quality of life improvement, but one that I’ve seen hit
newcomers now and then and matches reasonable expectations of program
behavior.

8 Likes

Looking at it this proposal it feels so non-controversial I’m surprised they’d want more feedback. Consider me* a +1.

*I am a student, though I would no longer classify myself as a new user, for what its worth…

While I sympathize with the idea of properly returning a value instead of carrying it through an exception, it feels like this deserves a language extension, not a typeclass.

Then if GHC ever decides to do a cleanup and write a whole new standard that extension could simply be promoted to a default behavior.

1 Like

For some time now I’ve been contemplating a generalisation of main:

{-# LANGUAGE Haskell2010 #-}
import Foreign.Marshal  -- see page 269 of 329 in the H. 2010 Report

class Mainly a where
    mainly :: a -> r

instance Mainly (IO a) where
    mainly act = unsafeLocalState (act >> exitSuccess)
                  -- or e.g. primMainlyIO :: IO a -> r 

With a suitable instance, any value can be “returned” by main :: Mainly a => a e.g:

instance Mainly ExitCode where
    mainly ecd = mainly (exitWith ecd)

instance Mainly Int where
    mainly n = mainly (ExitFailure n)  -- ...but also see page 299 of the Report

instance Mainly Bool where
    mainly b = mainly (if b then 0 else 1)

instance Mainly () where
    mainly () = mainly (pure ())

              ⋮ 

From Using unsafePerformIO safely?

The presence of Mainly.mainly (or something like it) could help to bring more clarity to such matters. For example, if throw could be defined using throwIO:

throw e = mainly (throwIO e)

then if mainly m = ⊥ for all m :: IO a (as intended), throw e would also equal , the result of an infinite regression or loop: let y = y in y. Similarly, if m :: IO a was going to end the program in some other way, then so would mainly m :: r .

While I would have maybe preferred that Haskell’s main function had always been IO Int, making it into IO ExitCode seems a bit of a hassle in regards to backwards compatibility, and having a type class that changes the a in main :: IO a into an ExitCode seems like a lot of unnecessary abstractions.

I really feel like this would be a good idea, had we not been in the situation we are in right now.

To be honest, forcing main to be of type IO Void actually feels like a good option to me, so that any exiting is explicit. This way, people new to making a (Haskell) executable also get confronted with the concept of “exiting a program” and the choices that need to/can be made. :thinking:
Downside is, it breaks all executable source code out there :sweat_smile:

6 Likes

I think an important aspect of return in other languages is that they allow early return from a function. Haskell’s return is a false friend in this way.

So even if this proposal was accepted, new users would still be confused by return ExitSuccess >> print "Foo" printing "Foo". And we should still be recommending exitWith instead

4 Likes

I’m curious what happens in GHCJS (and any other flavour of Haskell that compiles to JS). Does it even have main? Can it terminate? Does it have an “exit code”?

2 Likes

I think something like the proposal would have been maybe a little cleaner from the start.

That said, the current situation is largely fine, and I think it would cause more confusion and churn to try to alter things at this point, since this goes straight to the spec of the Haskell language.

Personally, I very rarely write programs that need to signal explicit failure with an exit code – and when I do, the existing mechanism of throwing is far cleaner and more straightforward to me – i.e. I’d never want to explicitly return such a code anyway, since my exits will come rather deeply nested, regardless.

1 Like

…particularly considering that JS apparently has no notion of a main symbol:

https://stackoverflow.com/questions/9015836/why-doesnt-javascript-need-a-main-function


Have people ever found do in Haskell confusing because do in other languages can indicate the start of a loop (and then went looking for while in Haskell)? I think abstract monadic I/O would have confused more new Haskellers than return being a overloaded method.

But return is being deprecated in favour of pure, though:

taint :: ... => a -> m a

probably would have been a better replacement.

I don’t know if I’d call it straightforward, my assumption would be that every current program is effectively

mainWithExit :: IO ExitCode
mainWithExit = ExitSuccess <$ main

The meaningful question here is whether the user should have the ability to return this code directly when they’re at the very end of the program.

If we use IO Void, what would be the exit code of main = pure undefined? This currently exits successfully with code 0.