Scala3’s @main
methods are a stripped down version of the @main
methods provided by Ammonite (docs). Ammonite’s @main
methods, as well as it’s own CLI flags, and those of Mill and Sjsonnet, have since been extracted into https://github.com/com-lihaoyi/mainargs: a shared library providing enough functionality to satisfy all the use cases of these widely-used tools.
The world of CLI argument parsing is messy and does not lend itself to simplistic APIs. Here’s some of the things that the mainargs library has to support that Scala3’s @main
methods do not:
-
--help
support - Default values for parameters
- Default values in combination with varargs (
String*
does not work, so we needed a customLeftover[T]
type) - Custom short flags e.g.
-f
, custom long flags e.g.--my-num
, automatic selection of short v.s. long flags -
Flag
parameters, which are passed or not but do not take a value -
doc
s for each parameter,doc
s for the main method as a whole - Support for multiple main methods
- Support for constructing a
case class
rather than call ing a method directly - Re-using argument sets between
@main
methods andcase class
es by embedding acase class
in the argument list -
Option[T]
andSeq[T]
parameter support for repeated parameters - Choice of positional v.s. name CLI args, or mixed (e.g. lots of applications use a positional CLI arg for the script name and named CLI args for configuration)
- Usage in non-entrypoint positions (e.g. inside Mill’s background daemon, or inside Ammonite’s script-launcher)
There are other use cases that mainargs still does not support, e.g. space-less short flags, POSIX-style multi-flags like -xzvf
, etc. While mainargs is already useful and has a rich API, it is by no means done, and I expect it to have to continue to evolve over time.
What’s the plan for Scala3’s @main
methods then? As implemented, they are sufficiently stripped down and under-specified that they as-described are unlikely to be usable for most “real” use cases. At the same time, the Scala3 implementation seems prettymuch hardcoded as to what it does. Scala3’s @main
methods seem like they will be useful for intro-to-Scala level teaching use cases and not much beyond that. In particular, ~none of the use cases that mainargs currently supports that inspired the Scala3 @main
method implementation can be satisfied by the Scala3 language feature.
Some possible options of what to do are:
-
We accept that the built-in
@main
is a Scala-101 teaching tool and no more, similar to Scala2’s built-in scripts -
We try to generalize it and make it extensible so that it can serve as a platform for more advanced use cases
-
We just vendor the entirety of mainargs into the Scala3 standard library
-
We move
@main
methods into a separate standalone launcher, that can be installed separately and used specifically for learning Scala (similar to how www.handsonscala.com relies on Ammonite being installed), and can evolve more quickly to satisfy the intro-to-Scala teaching use case (e.g. it could include pretty-printing via PPrint and other things) -
Make a separate launcher that is just a thin wrapper around mainargs, which already implements much more functionality than the Scala3
@main
methods do -
Once Ammonite is ported to Scala3 (currently WIP), we just make Ammonite the “intro to Scala” launcher and dispense with the standard-library
@main
methods entirely
While mainargs is a small library (currently ~1000 lines split over 12 files, with as much test code) it is a small library filled with domain-specific concerns and edge-cases, of the sort that I would not expect the Scala language-maintainers to have expertise in. We’ve all seen the outcome of projects like scala-xml, scala-actors, scala-parallel-collections, etc., similar domain-specific libraries that were frozen into the language and standard library, dooming them to stagnation, obsolescence, and eventual removal. Others, like scala.sys.process, have not been removed but are clearly showing their age.
Nevertheless, mainargs is small enough and the domain static enough (CLIs conventions don’t change often…) that embedding the whole thing into the language and standard library is not out of the question. Especially if we take care to define proper hooks and extension points, it seems plausible that we could make it “good enough” to cover 99% of use cases for the next 10 years, which is probably as long as most standard library code can be expected to last.
Just going all-in on Ammonite is another approach worth considering. While the Ammonite Scala3 port needs to be completed, and the codebase could use some refurbishment besides, it already covers a huge range of quality-of-life features that the Scala3 built-in functionality can never match. Ammonite can do this because it relies heavily on libraries and tools that the Scala language and standard library is prohibited from depending on. And why should a teaching tool be limited to only what’s in the Scala standard library? It’s not like we demand that the Metals or IntelliJ folk work with only what’s in the Scala standard library with no external dependencies.
I don’t have a strong preference to either, mainargs works and will continue to work for the foreseeable future, and seems straightforward to port to Scala3’s macros. But it would be good to discuss to have clarity and consensus on what the plan going forward is going to be.