IOApp
IOApp is a safe application type that describes a
main
which executes a cats.effect.IO, as an entry point to
a pure FP program.
Status Quo
Currently in order to specify an entry point to a Java application,
which executes a pure FP program (with side effects suspended and
described by
IO), you have to do something like this:
import cats.effect._ import cats.syntax.all._ import scala.concurrent.duration._ import scala.concurrent.ExecutionContext object Main { // Needed for `IO.sleep` implicit val timer: Timer[IO] = IO.timer(ExecutionContext.global) def program(args: List[String]): IO[Unit] = IO.sleep(1.second) *> IO(println("Hello world!")) def main(args: Array[String]): Unit = program(args.toList).unsafeRunSync }
That’s dirty, error prone and doesn’t work on top of JavaScript.
Pure Programs
You can now use
cats.effect.IOApp to describe pure programs:
import cats.effect._ import cats.syntax.all._ object Main extends IOApp { def run(args: List[String]): IO[ExitCode] = args.headOption match { case Some(name) => IO(println(s"Hello, $name.")).as(ExitCode.Success) case None => IO(System.err.println("Usage: MyApp name")).as(ExitCode(2)) } }
Things to note:
- the command line arguments get passed as a pure
Listinstead of an
Array
- we use an
ExitCodeto specify success or an error code, the implementation handling how that is returned and thus you no longer have to deal with a side-effectful
Runtime.exitcall
- the
Timer[IO]dependency is already provided by
IOApp, so on top of the JVM there’s no longer a need for an implicit
ExecutionContextto be in scope
In terms of the behavior, the contract is currently this:
- if the program returns an
ExitCode.Success, the main method exits and shutdown is handled by the platform — this is meant to play nice with Spark and also be consistent with Java’s behavior (e.g. non-daemon threads will block the app from exiting, unless you do something about it)
- if completed with an exit code different from zero, the app is exited with that as an error code (via
sys.exit)
- if the
IOterminates in error, it is printed to standard error and
sys.exitis called
Cancelation and Safe Resource Release
The
cats.effect.IO implementation is cancelable and so is
IOApp.
This means that when
IOApp receives a
SIGABORT,
SIGINT or another
interruption signal that can be caught, then the
IO app will cancel
and safely release any resources.
For example:
import cats.effect.ExitCase.Canceled import cats.effect._ import cats.syntax.all._ import scala.concurrent.duration._ object Main extends IOApp { def loop(n: Int): IO[ExitCode] = IO.suspend { if (n < 10) IO.sleep(1.second) *> IO(println(s"Tick: $n")) *> loop(n + 1) else IO.pure(ExitCode.Success) } def run(args: List[String]): IO[ExitCode] = loop(0).guaranteeCase { case Canceled => IO(println("Interrupted: releasing and exiting!")) case _ => IO(println("Normal exit!")) } }
If you run this sample, you can get two outcomes:
- if you leave it for 10 seconds, it will print “normal exit” and exit normally
- if you press
Ctrl-Cor do a
kill $pidfrom the terminal, then it will immediately print “interrupted: releasing and exiting” and exit
Therefore
IOApp automatically installs an interruption handler for you.
Why Is It Specialized for IO?
IOApp doesn’t have an
F[_] parameter, unlike the other data types
exposed by Cats-Effect. This is because different
F[_] data types have
different requirements for evaluation at the end of the world.
For example
cats.effect.IO now needs a
ContextShift[IO] in scope
for working with
Concurrent and thus for getting the
ConcurrentEffect necessary to evaluate an
IO. It also needs a
Timer[IO] in scope for utilities such as
IO.sleep and
timeout.
ContextShift and
Timer are provided by the environment and
in this case the environment is the
IOApp. Monix’s
Task however has global
ContextShift[Task] and
Timer[Task] always in scope and doesn’t
need them, but it does need a
Scheduler to be
available for the necessary Effect instance. And both
Cats-Effect’s
IO and Monix’s
Task are cancelable, in which case it
is desirable for the
IOApp /
TaskApp to install shutdown handlers
to execute in case of interruption, however our type classes can also
work with non-cancelable data types, in which case handling
interruption is no longer necessary.
Long story short, it’s better for
IOApp to be specialized and
each
F[_] can come with its own app data type that is better suited
for its needs. For example Monix’s
Task comes with its own
TaskApp.
That said
IOApp can be used for any
F[_], because any
Effect
or
ConcurrentEffect can be converted to
IO. Example:
import cats.effect._ import cats.data.EitherT object Main extends IOApp { type F[A] = EitherT[IO, Throwable, A] val F = implicitly[ConcurrentEffect[F]] def run(args: List[String]) = F.toIO { EitherT.right(IO(println("Hello from EitherT"))) .map(_ => ExitCode.Success) } }
Final Words
IOApp is awesome for describing pure FP programs and gives you functionality
that does not come for free when using the normal Java main protocol, like the
interruption handler.
And we worked hard to make this behavior available on top of JavaScript, via Scala.js, so you can use this for your Node.js apps as well.