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 Int
1:
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.