Pre-SIP: program Foo = { println("Hello world!") }

(1) The script-ish code is 4 lines, the pre-SIP would be 6 lines, and with main method, it would be 8 lines. True, 8 is double of 4. But is 8 lines of code so terrible?

Yes! It’s enough to stop people from doing it at all. They had to spend half their file writing things that weren’t what they wanted to do. That is a huge hike to the cognitive cost of writing your (probably disposable) script.

(2) Is this a real world example?

It’s representative of common scripting tasks. Imagine all the horror shows people write in sed/awk. I wrote a script yesterday to 1. run BLAST (a bioinformatics tool) and pipe a string to stdin, collect the stdout into a string 2. parse the CSV output 3. group by the value in the first column 4. print out the max of the last column per group.

(3) As you point out, the real burden is not to write the code, but to specify how to build it, include dependencies, deploy.

The burden shifts as the size/complexity of the task grows. However, we should be working hard to minimise the burden at each and every step. At every stage, somebody gives up. Let’s have fewer people give up.

(4) Languages like Perl or Python are more oriented towards tasks like processing text files than Java or Scala.

This is fair. But it comes down to what is in the stdlib, the prelude, and just how fat it is. As you point out, this can be handled by tooling that runs the script.

I am also in the camp that thinks that explicit main methods are not an acceptable alternative for extends App. I resent if the compiler forces me to write stuff I don’t care about, and everything having to do with main falls into that category. It’s pure boilerplate. There’s nothing in

  object Foo {
    def main(args: Array[String]): Unit = ...
  }

that tells me more than I"want to run the code in ... as program Foo". Furthermore, the annoyance is very visible for new and occasional users of the language. It would feel like a big step backwards to them from the previous Foo extends App idiom.

That said, and after thinking some more about it, program Foo { ... } feels like a large language footprint for something that is comparatively minor. Sure, program will be a soft keyword, and the construct is very easy to implement. But still, it feels like we spend a new language construct on something very specialized.

So, how about the following alternative instead? Allow a @main annotation on arbitrary methods. E.g.

    @main def hi() = println("hello world!")

The @main annotation would be permitted on arbitary toplevel methods. The example above would expand to something like:

    def hi() = println("hello world!")

    class hi {
       static def main(args: Array[String]) = hi()
    }

so you could call it with java hi.

Advantages:

  • The generated code could come from a macro or, if annotation macros are not available, from a later phase in the compiler. It’s pure code generation. The typer would not know about the main method and the class it is in.

  • Since this has to do with host interop (i.e. what method is callable form the host environment?) an annotation seems the right way to express this. Similar to how we express bean getters and setters, say.

  • It makes it clear that the program code is run in a method, not as the initialization code of an object.

  • We can provide for many ways to pass arguments. Either no arguments at all, or as an array, or as something else (immutable array, list, or finite number of strings). I.e. the following could all work:

  @main def hi(args: Array[String]) = ...
  @main def hi(args: IArray[String]) = ...
  @main def hi(args: Seq[String]) = ...
  @main def hi(arg1: String, arg2: String) = ...

That last point is to discuss. The point is, we have a lot of flexibility what kind of methods can be annotated with @main.

7 Likes

@odersky basically everything you’ve described with @main methods has been implemented in Ammonite scripts. It works very well!

The ammonite @main functionality and the way it maps the arguments of the main method to program arguments (in an extensible way since it’s based on a Read type class) is really amazing.

In repeat.sc:

@main def repeat(str: String, i: Int = 3) = str * i

In terminal:

$ amm repeat.sc hi
Compiling .../repeat.sc
"hihihi"
$ amm repeat.sc hi 4
"hihihihi"

It does seem like a lot of effort went into making Ammonite so user-friendly and powerful, so making it the standard, advertized way of doing Scala scripting would seem appropriate (as long as it stops silently sending logs over the internet by default).

I also agree that having a post-typer macro annotation that generates a main class, using the same conventions as ammonite, would already go a long way towards making Scala applications more pleasant to write.

2 Likes

It hasn’t done this for a while now, but nobody noticed

1 Like

Cool, good to hear.
(Also, how do you expect someone to notice, when it was silent in the first place? :sweat_smile:)

The only reason to use main function is that the java command line tool requires the signature, along with some complicated -cp, -X, -XX, and -D arguments.

My thoughts:

  1. Aren’t people from Java world who know those complicated arguments of the java command line tool also understand the convention of main? Which is more natural to them, def main or program Main?
  2. Are there other approaches to launch Scala applications for people who don’t use java command line tool? Is an average Scala starter expected to understand the java command line tool?

@odersky basically everything you’ve described with @main methods has been implemented in Ammonite scripts. It works very well!

Excellent! So let’s standardise on it.

Haha, that’s what Proposal: Simplifying the Scala getting started experience - #32 by shawjef3 is all about :stuck_out_tongue:

Am I forced to make a @main def? I’m worried that we are just shifting boilerplate around the plate, if you see what I mean.

@main def myMain(...) = { // boilerplate
... my
... script
... goes
... here
...
... indented?!?
} // boilerplate that I'm likely to get wrong, delete by accident, have to explain to friends

We could instead use an inversion of control to inject the (parsed) arguments part way through the script.

someCode
val (some: String, params: Int, go: Double, here: Option[Char]) = @args
moreCode

Sorry to be so bikesheddy about this, but I really strongly believe that getting this right is really worth getting right.

Yes you are forced to define a function if you want to take arguments with automatic deserialization. I agree with you that this isn’t the place for bikeshedding that specific a detail

If we allow top-level statements they would presumably run before the @main method.

I’m not sure how much of an advantage there is to running a compiled top level as an entry point, especially if it would not have access to the command line arguments (and I agree that the top level should not, except for scripts, but that we already have; this is about compiled code).

A compiled toplevel code in a .scala file would not contain arbitrary statements, only definitions.

I understand (although it was floated in the thread about top-level definitions – I discussed the argument against it there). My point was it’s not realistic to expect less ceremony than the @main def... approach. If we would (as Matthew implied) propose to make the entry point top level, that would imply allowing top-level statements, which would address his concern about the indentation, but at that point you don’t need the top level to be the entry point.

In other words, I’m suggesting his case is not a case for making the entry point top level, even if it remains an argument in favor of supporting top level statements (which indeed is not currently on the table).

@lihaoyi Can you point me to the docs for the rules for when a method can be a legal @main in Ammonite and how arguments are converted? It would be good to arrive at some harmonization between script code and normal code for this.

Whole documentation about @main is in this paragraphs: https://ammonite.io/#ScriptArguments and https://ammonite.io/#MultipleMainMethods
I could not find documentation about parsing arguments but quick test shows something:

//file: test.sc
case class Ex(s:String)
@main def main2(v:Int = 2, t:Ex) = println(v)

//run it with `amm test.sc 2`
//test.sc:5: could not find implicit value for evidence parameter of type scopt.Read[ammonite.$file.test.Ex]
//@main def main2(v:Int = 2, t:Ex) = println(v)
//          ^
//Compilation Failed

currently scopt.Read[T] is used.

Only top-level functions in scripts are allowed to be annotated as @main. As far as behavior is concerned, the documentation is pretty thorough, and you can play around with it yourself if you want more detail.

Apart from Ammonite, the same function-to-entrypoint logic is also used in defining Mill commands, and Cask HTTP endpoints (though using a different typeclass for deserialization). It works very, very well

I believe you :smile:. But to put this into the main compiler we need to spec it, and for that we need some wordings for rules. Also, we can’t have any dependencies (e.g. to scopt).

Why do we need to put it into the main compiler though? For getting-started level usage using it in Ammonite is enough, and for usage in larger projects built using Mill or SBT, it could easily be a third party library (using Macros in Scala 2.x, no idea about Dotty)

Even if standardized, if it can live outside the compiler codebase, it probably should live outside the compiler codebase. It’s not like newbies start using scala by reading the spec and downloading scala-compiler.jar

In this particular case, there is enough detail that goes into making @main work that it would be strange to have it all in the language spec. Maybe in the standard library, at most, but speccing things like the autogenerated help or error messages or the argument parsing implementation definitely seems like too-much-information

Thinking a bit further, there is definitely a decorator-like pattern underlying the Ammonite/Mill/Cask implementations of entrypoint functions that could conceivably become a language feature. However, extracting that out, defining it, and making sure it’s extensible in all the right ways, is a deep discussion in its own right and probably out of scope for a discussion based around simplifying main methods and getting started for newbies.

Proposal: Simplifying the Scala getting started experience lays out what I think we should do to help beginners. I don’t think it makes sense to discuss @main method language additions and specs in the abstract without the proper context of how it fits into a larger strategy.