Pre-SIP: Extend App and @main to handle exit codes

#1

While the JVM requires that a main method return void, I think it’s possible for Scala to do better in the cases where we already provide syntactic sugar.

In order to avoid situations where this behavior is triggered by accident, the returned value would be a wrapper around Int1:

package scala

class ExitCode (val code: Int) extends AnyVal
object ExitCode {
  def failure(code: Int): ExitCode = new ExitCode(code)
  def success: ExitCode = new ExitCode(0)
}

Scala 2.x

This would involve changes to DelayedInit, and an App variant, as I couldn’t see a way forward to combine the two:

trait DelayedInit {
  def delayedInit[A](x: => A): A
}
// Only these signatures would need changed, the rest would remain as-is
trait App extends DelayedInit {
  private val initCode = new ListBuffer[() => Unit]

  @deprecated("the delayedInit mechanism will disappear", "2.11.0")
  override def delayedInit[Unit](body: => Unit) {
    initCode += (() => body)
  }
}

trait UnixApp extends DelayedInit {
  /** The time when the execution of this program started, in milliseconds since 1
    * January 1970 UTC. */
  @deprecatedOverriding("executionStart should not be overridden", "2.11.0")
  val executionStart: Long = currentTime

  /** The command line arguments passed to the application's `main` method.
   */
  @deprecatedOverriding("args should not be overridden", "2.11.0")
  protected def args: Array[String] = _args

  private var _args: Array[String] = _

  private val initCode = new ListBuffer[() => ExitCode]

  /** The init hook. This saves all initialization code for execution within `main`.
   *  This method is normally never called directly from user code.
   *  Instead it is called as compiler-generated code for those classes and objects
   *  (but not traits) that inherit from the `DelayedInit` trait and that do not
   *  themselves define a `delayedInit` method.
   *  @param body the initialization code to be stored for later execution
   */
  @deprecated("the delayedInit mechanism will disappear", "2.11.0")
  override def delayedInit[ExitCode](body: => ExitCode) {
    initCode += (() => body)
  }

  /** The main method.
   *  This stores all arguments so that they can be retrieved with `args`
   *  and then executes all initialization code segments in the order in which
   *  they were passed to `delayedInit`.
   *  @param args the arguments passed to the main method
   */
  @deprecatedOverriding("main should not be overridden", "2.11.0")
  def main(args: Array[String]) = {
    this._args = args
    val retValue = initCode.map(_.apply()).head
    if (util.Properties.propIsSet("scala.time")) {
      val total = currentTime - executionStart
      Console.println("[total " + total + "ms]")
    }
    System.exit(retValue.code)
  }
}

Scala 3.x

Dotty is a bit easier, as the expansion of the @main annotation triggers the creation of a synthesized method which could more easily be customized.

A method returning Unit would retain the same expansion, while methods which return ExitCode would have their value passed to System.exit.

For example, this method:

@main def goodOrBad(input: String): ExitCode = {
      input.toLowerCase match {
         case Some("good") => ExitCode.success
         case _ => ExitCode.failure(1)
      }
    }

Would produce additional code equivalent to this class (following the conventions for <static> from the main-func docs:

final class goodOrBad {
    import scala.util.{CommandLineParser => CLP}
    <static> def main(args: Array[String]): Unit =
      System.exit {
        try goodOrBad(CLP.parseArgument[String](args, 0)).code
        catch {
          case error: CLP.ParseError => CLP.showError(error)
        }
      }
  }

1: ErrorCode would wrap Int primarily for historic reasons, ideally, it’d be something which communicates the 0-255 range better, but that may be less familiar for programmers coming from other languages. Alternately, it could throw an IllegalArgumentException if the value is out of bounds.

1 Like
#2

I’m afraid this is not going to work as intended when multi-threaded.

Normally, the JVM terminates only after all non-demon threads terminate, but if you call System.exit, it will terminate immediately and not wait for other threads.

1 Like
#3

I’m not sure that’s going to be an issue in practice.

When you’re working multi-threaded, you can’t really determine what a sensible return value would be with just what happens before main ends, so you’d return Unit and the existing behavior would be retained.

Admittedly it’d be a nasty surprise, but no more so than when someone forgets to wait on a Future and the JVM terminates before the work is done, or forgets to close an ActorSystem and the JVM never terminates.

#4

Perhaps.

Many apps i have have main methods that exit rapidly and other threads handle the lifecycle.

Either way, an Int should not encode this (primitive fixation anti pattern), a custom type should, perhaps something like

case class ExitCode(code: Int)

Then this would never be accidental. Otherwise someone might have an int value at the end of the method by accident and shut down the vm.

2 Likes
#5

I like the wrapper suggestion! Think I’ll borrow that :slight_smile:

#6

Another consideration… Perhaps exit should be called only if the code is not zero.

#7

I poked around a bit, and it looks like there isn’t really a good reason to avoid calling it. The shutdown hooks still run, so it’s unlikely to cause problems there.

I think it might be better to keep the behavior as simple as possible:

  • Return Unit from main when you have background stuff going on, or don’t care about the return code.
  • Return ExitCode from main when you want the program to exit with that code.

Adding an “unless you happen to have background stuff you forgot about” to the exit behavior when you return an ExitCode seems to muddy the waters a bit.

#8

My reasoning is the following: a main method calling two others in sequence (or any sort of nesting). Not the best design perhaps, but certainly unexpected if the second one does not run when the first succeeds, and I am sure some will trip over it.

This just makes me think about a monadic structure with early termination… but we dont need yet another one of those.

#9

I’m not quite sure I’m understanding that use case, do you mean something like this:

object Main1 extends App {
  println("Main1")
}

object Main2 extends App {
  Main1.main(args)
  println("Main2")
}

To be honest, if that’s what they’re doing, I’m kind of happy if anything I suggest leads them away from that dark path.

#10

This wouldnt lead them away, it would cause frustration and end up at a stack overflow page.

Leading people away is when doing the wrong thing is harder or awkward, not when it adds confusion and a page to Scala Puzzlers.

What about someone writing a unit test to cover the main method and having their build/test tool exit silently as a result?

I’ve been horribly annoyed at third party tools or libraries that call system.exit in the past on the assumption that everyone has only the use cases the author imagined. System.exit has a unique side effect…

Perhaps the special result type can hold more than a code.

A sealed trait with a companion perhaps…

ExitCode.success

ExitCode.error(4)

ExitCode.continue

1 Like
#11

Fair enough, scopt has this issue and it has been pretty frustrating from time to time

#12

I liked this proposal aside from the magic of Int at first impression.

But now I’m wondering what the value is. How does code with it compare to code without it? Using the example above it doesn’t seem to be much of an improvement:

@main def goodOrBad(input: String): ExitCode =  input.toLowerCase match {
  case Some("good") => ExitCode.success
  case _ => ExitCode.failure(1) 
}

The above can’t be tested if it automatically exits, so, it gets refactored to:

@main def goodOrBad(input: String): ExitCode = goodOrBadEx(input)

private[mypackage] def goodOrBadEx(input: String): ExitCode = input.toLowerCase match {
  case Some("good") => ExitCode.success
  case _ => ExitCode.failure(1)
}

But how is this much better than calling exit by hand?

@main def goodOrBad(input: String): Unit = System.exit(goodOrBadEx(input))

private[mypackage] def goodOrBadEx(input: String): Int = input.toLowerCase match {
  case Some("good") => 0
  case _ => 1
}
#13

Yeah, I think the way the JVM is setup works against us here to the point where it’s not really worth trying to circumvent.

Ah well

#14

A similar SystemExit was added to the compiler source.

It’s a ControlThrowable to make it easy to throw and get out.

I like the idea of main catching it and also taking it (or int) as a result value to exit with.

#15

Why, what’s wrong with that?

#16

Yeah, I think so, too.

If it was implemented, then there definitely should be a wrapper, and it should be like:

EndOfMainBehavior.Default

EndOfMainBehavior.HaltImmediatelyWithCode(n: Int)

And of course people will insist that you can put nothing to get the default behavior, so in the rare case where you do want to exit immediately, it’s easier and clearer to call System.exit directly.

#17

Calling main from main has a couple of problems:

  1. main provides zero information about what it’s supposed to do other than be a “start here” point
  2. It makes the system a pain to trace, because the edges of the system aren’t always the edges of the system

The first is a mild annoyance, the second can be really aggravating because, as main is an entrypoint, it needs to handle wiring up dependencies, parsing arguments, cleaning up resources, etc so the actual work can be done. If that’s actually in the middle of the call graph somewhere, it means all of the setup & teardown has to be either somehow conditional or wastefully nested. Both tend to be confusing.