1. Introduction
In this article, we’re going to talk about how to implement main methods in Scala 3. In particular, we’ll focus on the @main annotation.
We’ll first see how the @main annotation behaves, and then we’ll compare it with the old-fashioned way of implementing main methods in Scala 2.
2. The @main Annotation
Scala 3 has introduced the annotation @main to allow a method to be converted into the entry point of an executable program.
Let’s see how that works with a simple hello-world program:
@main
def helloWorld() = println("Hello, world")
If we save helloWorld as a file, like HelloWorld.scala, we can run it simply by executing scala helloWorld, after compiling it with scalac. The name of the program is the name of the method itself. Upon running it, we’ll see “Hello, world” printed out on the console.
A method annotated with the @main annotated can be defined either at the top level, as above, or inside a statically accessible object:
object HelloWorldObject {
@main
def helloWorldFromAnObject() = println("Hello, world. I'm inside an object.")
}
In both cases, the program is named after the method itself, without any prefixes indicating the object it’s defined within. Again, if we run helloWorldFromAnObject we’ll see the message “Hello, world. I’m inside an object” on the console.
2.1. Command-Line Arguments
The @main annotation also helps to deal with command-line arguments in a very declarative way. As a matter of fact, main methods can have typed parameters. The Scala compiler will generate all the necessary code to parse the arguments and make the values available in the method’s parameters.
Let’s create a main method that takes a name parameter:
@main
def helloName(name: String) = println(s"Hello, $name")
If we run it using scala helloName Baeldung, the program will print “Hello, Baeldung”.
We can add as many parameters as we want. As for the types, the only condition is that there must be an instance of scala.util.FromString to turn the String taken from the command line into a value of the type we specified. At the moment, there are such instances for String, Boolean, Byte, Short, Int, Long, Float, and Double.
2.2. Generated Code
Behind the scenes, the Scala compiler generates a lot of code for us. Let’s look at some code for a simple main method like helloName above:
final class helloName {
import scala.util.{CommandLineParser as CLP}
def main(args: Array[String]): Unit =
try
helloName(CLP.parseArgument[String](args, 0))
catch {
case error: CLP.ParseError => CLP.showError(error)
}
}
Scala generates a class named after the method, helloName, implementing a method main with the usual signature for the parameters. Scala 3 generates such a method as a static method of the class helloName. Only the Scala compiler can define a static method in a class, rather than inside an object.
Scala also adds code to parse command-line arguments to the generated main method using CommandLineParser. CommandLineParser is an object introduced in Scala 3, whose most important methods are parseArgument and parseString. Let’s see how Scala 3 implements them:
def parseString[T](str: String, n: Int)(using fs: FromString[T]): T = {
try fs.fromString(str)
catch {
case ex: IllegalArgumentException => throw ParseError(n, ex.toString)
}
}
def parseArgument[T](args: Array[String], n: Int)(using fs: FromString[T]): T =
if n < args.length then parseString(args(n), n)
else throw ParseError(n, "more arguments expected")
parseArgument takes an array of args and an index n, indicating which parameter we are to parse. It also has a type parameter representing the target type. parseArgument* relies on parseString, which basically converts a String into a value of type T if and only if there is an instance of fromString defined for that specific *T.
If no such instance exists, parseString throws an exception, and the parsing fails. The generated code in the main method above takes care of this scenario, simply showing the parsing error via CommandLineParser.showError. Let’s see how a parsing error looks like:
@main
def timesTwo(value: Int) = println(s"Result: ${value * 2}")
Running timesTwo with argument 5 produces “Result: 10” in the console. However, if we ran it with “Baeldung” as an argument, the result would be “Illegal command line: java.lang.NumberFormatException: For input string: “Baeldung””. Additionally, running it without any arguments at all will produce the error message “Illegal command line: more arguments expected”.
3. Comparison with Scala 2
Most of the code generated by the Scala compiler in Scala 3 had to be written by hand in Scala 2. Let’s see how helloName would look like in Scala 2:
object helloName extends App {
// manual parsing of the command line arguments
}
The object helloName has to extend App, which, in turn, extends the DelayedInit trait. In brief, App defined a main method itself (def main(args: Array[String])). Furthermore, all of the code defined as a body in the object was placed into a ListBuffer and run inside main. This kind of behavior is no longer available. App still exists, but it will be deprecated in the future.
Additionally, App didn’t support automatic parsing of command-line arguments, which the new @main annotation provides out of the box.
4. Conclusion
In this article, we saw how to use the @main annotation to implement main methods in Scala 3. We took a look at the generated code and how it deals with command-line parameters. Lastly, we compared it with an implementation of the same method in Scala 2.
As usual, you can find the code over on GitHub.