-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Implement @main functions #6898
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
||
/** An annotation that designates a main function | ||
*/ | ||
class main extends scala.annotation.Annotation {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
final
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we not making it final
?
tests/neg/main-functions.scala
Outdated
} | ||
|
||
object Test2 { | ||
@main val x = 2 // does nothing, should this be made an error? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd agree that @main val
shouldn't be allowed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I second this. I can't see any good use cases for tagging a val
with @main
, and thinking about its semantics already stretches the imagination beyond the result being reasonable.
tests/neg/main-functions.scala
Outdated
} | ||
|
||
class Foo { | ||
@main def f = () // does nothing, should this be made an error? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably more trouble than it's worth.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Technically this is a correct program that does nothing. I think we should allow empty programs.
Where can I discuss the deprecation of |
In what way |
@soronpo If we had annotation macros |
A `@main` annotation on a method turns this method into an executable program. | ||
Example: | ||
```scala | ||
@main def happyBirthday(age: Int, name: String, others: String*) = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we encourage the use of : Unit
here or not?
What is stopping us from annotating an object MyApp extends App {
//do something
} I think we should also allow @main object MyApp extends App {
//do something
} In my code base I have: trait DSLApp extends App {
//DSL Library stuff here
}
object DSLUserApp extends DSLApp {
//User stuff here
} I would very much like the ability to extend |
Along the lines of @soronpo's point: the FP community makes pretty heavy use of |
The general problem with annotating an object for main is that the main program is the run in the object's initializer code. The JIT tends to be less aggressive in optimizing such code. This is mainly a problem for micro-benchmarks, but that matters, since you get your reputation ruined quickly if people run the same trivial program in Java and Scala and the Scala version is x times slower. Note that that's not a problem for |
I forgot the more important reason why we do not want to put initalization code in object initializers: It's prone to deadlocks if your program is multi-threaded. |
package scala.util | ||
|
||
/** A utility object to support command line parsing for @main methods */ | ||
object CommandLineParser { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand the goal of simplifying the way to implement application entry points. In practice, most of the users just write a def main(args: Array[String]): Unit
method, and manually parse the application arguments. For these cases, this PR definitely improves the developer experience.
That being said, by introducing a way to parse the program arguments in the compiler, we will also establish a “standard” way of doing that. And, I’m a little bit worried about the introduction of the CommandLineParser
and FromString
abstractions. For now, they are simple but I think we can already anticipate the following requests soon: can we also support optional parameters? Can we support named parameters? How could we customize the error message in case of invalid parameters? Can we print all the error messages instead of stopping at the first one? Should we use Either
instead of relying on exceptions to model parsing failures? All these requests are legitimate, in my opinion, but if we try to fulfill them the size of the CommandLineParser
and FromString
modules will be 10x bigger. Then, the question will be “should we keep that thing in the compiler, or should we move it to the library land (like scopt, or optparse-applicative)?”. We can make the decision of keeping CommandLineParser
simple and not honor these requests, “by design”, but then I’m worried about introducing a “standard” way of parsing command-line arguments which does not scale to advanced use cases.
Unfortunately, I don’t have a simple idea of how to decouple @main
expansion from CommandLineParser
(so that users could plug in the parsing + reporting logic they want). I still think the most flexible way to go is to require users to forward a plain old main
method to whatever they want:
def happyBirthday(age: Int, name: String, others: String*): Unit = …
def main(args: Array[String]): Unit = {
val happyBirthdayParser = … // some code that relies on a library for arguments parsing
happyBirthdayParser.run(args)(happyBirthday)
}
But I agree that this approach is not beginner-friendly…
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe we can stay firm and keep CommandLineParser
simple. If one wants something else, one can perfectly well define a traditional main method, or define the entry point like this:
@main def f(args: String*) = ...
I find the following is more natural, no semantic magic with names: The code @main class HappyBirthday(age: Int, name: String, others: String*) {
// code
} expands to class HappyBirthday(age: Int, name: String, others: String*) {
// code
}
object HappyBirthday {
import scala.util.{CommndLineParser => CLP}
def main(args: Array[String]): Unit = {
try
new HappyBirthday(
CLP.parseArgument[Int](args, 0),
CLP.parseArgument[String](args, 1),
CLP.parseRemainingArguments[String](args, 2))
catch {
case error: CLP.ParseError => CLP.showError(error)
}
}
} |
I don’t see how benchmarking is an issue, as usage of JMH will requires the code to be in a method. |
@odersky is supporting both forms ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks fine to me, except the points specified below. Another thing: doesn't work with SBT.
From Dotty repo, on this PR branch, do dotty-bootstrapped/publishLocal
. Then:
sbt new lampepfl/dotty.g8 --name mainmethods
cd mainmethods
In buld.sbt
, set:
val dottyVersion = "0.18.0-bin-SNAPSHOT"
Then in Main.scala
, write:
object Main {
// def main(args: Array[String]): Unit = println("foo")
@main def f(): Unit = {
println("Hello world!")
println(msg)
}
def msg = "I was compiled by dotty :)"
}
Then do sbt run
:
[error] java.lang.RuntimeException: No main class detected.
[error] at scala.sys.package$.error(package.scala:26)
[error] (Compile / bgRun) No main class detected.
[error] Total time: 11 s, completed Jul 26, 2019 2:27:44 PM
However, if you compile with dotc
instead, it works (I tested it from SBT console):
dotc ../ecosystem/mainmethods/src/main/scala/Main.scala
dotr f
(change the argument to dotc
to the path to Main.scala
on your machine).
Now in Main.scala
, uncomment def main(args: Array[String]): Unit
and run dotc
again. Then from Dotty repo in bash, do:
tsf-428-wpa-0-238:dotty anatolii$ javap Main$ f
Compiled from "Main.scala"
public final class Main$ implements scala.Serializable {
public static final Main$ MODULE$;
public static {};
public void main(java.lang.String[]);
public void f();
public java.lang.String msg();
}
Compiled from "Main.scala"
public final class f {
public f();
public static void main(java.lang.String[]);
}
I believe sbt
may not be prepared to the fact that main methods may be static
and/or come from classes and not objects (i.e. classes that end with $
). IMO it is best to keep the new main methods compliant to the way the main methods are usually expressed in Scala.
} | ||
``` | ||
**Note**: The `<static>` modifier above expresses that the `main` method is generated | ||
as a static method of class `happyBirthDay`. It is not available for user programs in Scala. Regular "static" members are generated in Scala using objects instead. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we make this design decision here? Looks like a limitation to me. E.g. I may want to write a library of utility functions that may depend on each other:
@main def disableFile(path: Path): Unit =
// Change the `path`'s extension from `scala` to `disabled`
@main def enableFile(path: Path): Unit =
// Change the `path`'s extension from `disabled` to `scala`
@main def filterFolder(folder: Path, fileNamePattern: Regex): Unit = {
// Disable all the files except those the names of which match the given regex pattern
val allFiles: List[Path] = // get all the files present in the folder
allFiles.foreach(enableFile) // Make sure we start with a clean folder, unaffected by previous runs of this or sister programs
allFiles.filterNot(file => fileNamePattern.matches(file.name)).foreach(disableFile)
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is not available for user programs in Scala
Also, it doesn't seem that this limitation is true:
object Main {
def main(args: Array[String]): Unit = { f(); println("foo") }
@main def f(): Unit = {
println("Hello world!")
println(msg)
}
@main def g(): Unit = f()
def msg = "I was compiled by dotty :)"
}
Compiles and runs fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't that mean that the synthetic method main
can't be called, rather than that the @main
method written in source can't be called?
the required types. If a check fails, the program is terminated with an error message. | ||
Examples: | ||
``` | ||
> scala happyBirthday 22 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the exact lifecycle of such a program here? What's happyBirthday
? Do we need to compile it with dotc
and run scala
from the folder in which the class files end up? Can we do scala happyBirthday.scala 22
– that is without the necessity to compile the source? I think we need to elaborate on this in the docs.
|
||
/** An annotation that designates a main function | ||
*/ | ||
class main extends scala.annotation.Annotation {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we not making it final
?
tests/neg-custom-args/fatal-warnings/main-functions-nameclash.scala
Outdated
Show resolved
Hide resolved
tests/neg/main-functions.scala
Outdated
} | ||
|
||
object Test2 { | ||
@main val x = 2 // does nothing, should this be made an error? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I second this. I can't see any good use cases for tagging a val
with @main
, and thinking about its semantics already stretches the imagination beyond the result being reasonable.
tests/neg/main-functions.scala
Outdated
} | ||
|
||
class Foo { | ||
@main def f = () // does nothing, should this be made an error? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Technically this is a correct program that does nothing. I think we should allow empty programs.
println("Hello, world!") | ||
|
||
object A { | ||
@main def foo(x: Int, y: String, zs: Float*) = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this code executed anywhere?
We control main class detection, not sbt: https://github.com/lampepfl/dotty/blob/232b3fa9e17a771e55088299d5e461ee25aed9e5/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala#L233-L235 |
Thanks for the hint @smarter! I tried a fix. @anatoliykmetyuk Can you test whether it works now with sbt? |
@@ -230,7 +230,7 @@ private class ExtractAPICollector(implicit val ctx: Context) extends ThunkHolder | |||
|
|||
allNonLocalClassesInSrc += cl | |||
|
|||
if (sym.isStatic && defType == DefinitionType.Module && ctx.platform.hasMainMethod(sym)) { | |||
if (sym.isStatic && ctx.platform.hasMainMethod(sym)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you need to drop sym.isStatic
to allow classes to have main
method.
We should add a scripted sbt test to verify the main class detection works: https://github.com/lampepfl/dotty/tree/master/sbt-dotty/sbt-test/sbt-dotty |
As of |
18c2061
to
5cdf2d6
Compare
Check applicability of annotations only once they are completed
If we make this method take an implicit function rather than a normal one we get a build error with trace: ``` [error] ## Exception when compiling 9 sources to /Users/odersky/workspace/dotty/sbt-bridge/src/target/classes [error] Type scala.ImplicitFunction1 not present [error] sun.reflect.generics.factory.CoreReflectionFactory.makeNamedType(CoreReflectionFactory.java:117) [error] sun.reflect.generics.visitor.Reifier.visitClassTypeSignature(Reifier.java:125) [error] sun.reflect.generics.tree.ClassTypeSignature.accept(ClassTypeSignature.java:49) [error] sun.reflect.generics.repository.ConstructorRepository.getParameterTypes(ConstructorRepository.java:94) [error] java.lang.reflect.Executable.getGenericParameterTypes(Executable.java:283) [error] java.lang.reflect.Method.getGenericParameterTypes(Method.java:283) [error] sbt.internal.inc.ClassToAPI$.parameterTypes(ClassToAPI.scala:566) [error] sbt.internal.inc.ClassToAPI$.methodToDef(ClassToAPI.scala:318) [error] sbt.internal.inc.ClassToAPI$.$anonfun$structure$1(ClassToAPI.scala:182) [error] sbt.internal.inc.ClassToAPI$.$anonfun$mergeMap$1(ClassToAPI.scala:400) [error] scala.collection.TraversableLike.$anonfun$flatMap$1(TraversableLike.scala:240) [error] scala.collection.IndexedSeqOptimized.foreach(IndexedSeqOptimized.scala:32) [error] scala.collection.IndexedSeqOptimized.foreach$(IndexedSeqOptimized.scala:29) [error] scala.collection.mutable.WrappedArray.foreach(WrappedArray.scala:37) [error] scala.collection.TraversableLike.flatMap(TraversableLike.scala:240) [error] scala.collection.TraversableLike.flatMap$(TraversableLike.scala:237) [error] scala.collection.AbstractTraversable.flatMap(Traversable.scala:104) [error] sbt.internal.inc.ClassToAPI$.merge(ClassToAPI.scala:411) [error] sbt.internal.inc.ClassToAPI$.mergeMap(ClassToAPI.scala:400) [error] sbt.internal.inc.ClassToAPI$.structure(ClassToAPI.scala:182) [error] sbt.internal.inc.ClassToAPI$.x$2$lzycompute$1(ClassToAPI.scala:133) [error] sbt.internal.inc.ClassToAPI$.x$2$1(ClassToAPI.scala:133) [error] sbt.internal.inc.ClassToAPI$.instance$lzycompute$1(ClassToAPI.scala:133) [error] sbt.internal.inc.ClassToAPI$.instance$1(ClassToAPI.scala:133) [error] sbt.internal.inc.ClassToAPI$.$anonfun$toDefinitions0$1(ClassToAPI.scala:140) [error] xsbti.api.SafeLazyProxy$$anon$1.get(SafeLazyProxy.scala:26) [error] xsbti.api.SafeLazy$Impl.get(SafeLazy.java:58) ```
@anatoliykmetyuk I think everything we agreed on is done. Merge? |
Has any thought been given to user-pluggable help messages for bad user input? The output messages are quite minimalistic, which is fine as a default, but not very friendly to the user. A minimal addition would be the ability to define a pluggable help message (or better, a method) that is called to create the equivalent of
I think something like this will be mandatory if |
The system will be extensible to some degree (there is a discussion on cotributors.scala-lang.org), but if you need more power you should fallback to a user-land solution such as scopt or decline. |
See:
https://github.com/dotty-staging/dotty/blob/add-main-fn/docs/docs/reference/changed-features/main-functions.md