cats-effect
cats-effect copied to clipboard
`ioMain` annotation for blazing fast getting started experience (Scala 3.2+)
How'd you like them buzzwords.
Scala 3.2 added an experimental support for custom @main annotations, see here: https://dotty.epfl.ch/docs/reference/experimental/main-annotation.html#
It would be excellent to experiment with a @ioMain annotation (or even @io?) in Cats Effect or any auxiliary project.
I went ahead and wrote a demonstrator to hwet your appetite:
//> using scala "3.3.1"
//> using lib "org.typelevel::cats-effect::3.5.3"
import scala.annotation.MainAnnotation
import scala.util.CommandLineParser.FromString
import scala.annotation.experimental
import cats.effect._
@experimental class ioMain extends MainAnnotation[FromString, IO[Unit]]:
import MainAnnotation.{Info, Parameter}
def command(info: Info, args: Seq[String]): Option[Seq[String]] =
Some(args)
def argGetter[T](
param: Parameter,
arg: String,
defaultArgument: Option[() => T]
)(using parser: FromString[T]): () => T =
() => parser.fromString(arg)
def varargGetter[T](param: Parameter, args: Seq[String])(using
parser: FromString[T]
): () => Seq[T] =
() => args.map(arg => parser.fromString(arg))
def run(program: () => IO[Unit]): Unit =
val app = new IOApp.Simple {
def run = program()
}
app.main(Array.empty)
end ioMain
@experimental
@ioMain def sum(first: Int, second: Int, rest: Int*) =
IO.println(first + second + rest.sum)
Run this program with Scala CLI:
$ scli ioMain.scala -- 25 16 100 50
Once the annotation design exits experimental phase, users should just be able to do
@ioMain def sum(first: Int, second: Int, rest: Int*) =
IO.println(first + second + rest.sum)
And enjoy that deliciously non-invasive experience.
This is great! I would bikeshed a bit on the annotation itself (the camel case looks super ugly to me), but I love the concept and it absolutely scratches the buzzword itch.
Yep, name is not great - @iomain IMO will be closer to the default @main annotation on Scala 3. I would also consider @runit, @ioapp, @innit, @dunnit
If CE was on Scala 3.2.0, then we could've just put the annotation in CE and iterate aggressively on it - it cannot be used anywhere outside of experimental scope, so it's a conscious decision by the users to use something that can break.
Also apparently MiMa has support for ignoring annotated scopes (some of them), i.e. it's used in Dotty: https://github.com/lampepfl/dotty/pull/14976/files#diff-539d15a8d5ba9a01a1724604b56cadf6851c57ec68dcf75a5073b6255d182de7R443
If the mythical 3.4.0 is indeed true, as the elders prophecised, we could just pop a simple iomain annotation in there marked experimental and let the bleeding edge users have at it with 0 guarantees.
I think we should add this to 3.4.0 and not worry about bincompat.
The whole point of bincompat is for libraries. Libraries typically don't have mains in them, and woe be upon the libraries that decided to use an experimental annotation to implement main methods that then somehow get involved in a diamond dependency downstream.
What about @IO.main?
I like it.
Taking it further - @IOApp.main? To stick with IOApp being the thing hosting all methods of actually launching nuclear rockets
Then should it be @IOApp.run ?
This is great! I would bikeshed a bit
It's like Daniel knew that people would have opinions on names for things! Never seen this before... incredible powers of prediction
I'd stick with main name tbh, to match as close as possible the naming on the Scala 3 side.
Supports the argument of "hey, just add IOApp. to the annotation on your main method, wrap it in IO and boom - you've got yourself a pure FP thing"
Taking it further - @IOApp.main? To stick with IOApp being the thing hosting all methods of actually launching nuclear rockets
I don't mind this, though I had another idea, which was cats.effect.main. Counter-point on the IOApp idea: isn't IOApp just an implementation detail of IO? Where are new users more likely to look?
I don't mind this, though I had another idea, which was
cats.effect.main.
This might be dangerous with a import cats.effect.* at the top ... oh wait you condemn that 😛
oh wait you condemn that
top-level cats-effect (+std) import is the only way to live.
After thinking long and hard, I think I like IO.main the best.
I'm just leaving this here: https://github.com/typelevel/cats-effect-main 😇 Source: https://github.com/typelevel/toolkit/issues/3#issuecomment-1444305184
It might sound obvious, but we might want to add some validation here:
def command(info: Info, args: Seq[String]): Option[Seq[String]] = Some(args)
As invoking the app w/o the correct arguments raises a variety of different exceptions, like:
❯ scala-cli run ioMain.scala -- 1
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 1 out of bounds for length 1
at scala.collection.immutable.ArraySeq$ofRef.apply(ArraySeq.scala:331)
at sum.main(ioMain.scala:36)
or
❯ scala-cli run foo.scala -- 1 2 a
Exception in thread "main" java.lang.NumberFormatException: For input string: "a"
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.base/java.lang.Integer.parseInt(Integer.java:652)
at java.base/java.lang.Integer.parseInt(Integer.java:770)
...