typelevel / cats-effect

The pure asynchronous runtime for Scala
https://typelevel.org/cats-effect/
Apache License 2.0
2.04k stars 523 forks source link

`ioMain` annotation for blazing fast getting started experience (Scala 3.2+) #3149

Open keynmol opened 2 years ago

keynmol commented 2 years ago

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.

djspiewak commented 2 years ago

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.

keynmol commented 2 years ago

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.

armanbilge commented 2 years ago

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.

djspiewak commented 2 years ago

What about @IO.main?

keynmol commented 2 years ago

I like it.

Taking it further - @IOApp.main? To stick with IOApp being the thing hosting all methods of actually launching nuclear rockets

armanbilge commented 2 years ago

Then should it be @IOApp.run ?

keynmol commented 2 years ago

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

keynmol commented 2 years ago

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"

djspiewak commented 2 years ago

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?

armanbilge commented 2 years ago

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 😛

keynmol commented 2 years ago

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.

TonioGela commented 1 year ago

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)
        ...
yisraelU commented 2 months ago

I would like to take a crack at this @samspills

samspills commented 2 months ago

That's awesome!! I'll assign you :D

TonioGela commented 2 months ago

I would like to take a crack at this @samspills

There's this repo ready for it :) https://github.com/typelevel/cats-effect-main

TonioGela commented 2 months ago

@yisraelU sadly this annotation got removed here: https://github.com/scala/scala3/pull/19937 but there's a SIP about MacroAnnotations to replace it: https://github.com/scala/improvement-proposals/pull/80.

yisraelU commented 2 months ago

@TonioGela TY , I saw that, I've been looking at using MacroAnnotations