Baeldung Pro – Scala – NPI EA (cat = Baeldung on Scala)
announcement - icon

Learn through the super-clean Baeldung Pro experience:

>> Membership and Baeldung Pro.

No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.

1. Introduction

Cats Effect is a popular library in Scala ecosystem and functional programming for managing side effects in functional programming. It provides an easy-to-use API for building asynchronous, concurrent applications. In this tutorial, we’ll explore the IOApp, a trait that serves as the entry point for simplifying the way we can write Cats Effect applications.

2. Creating a Simple Application

To start implementing applications with the Cats Effect library, we first need to add the following dependency in our build.sbt file:

libraryDependencies += "org.typelevel" %% "cats-effect" % "3.5.7"

Now we are ready to implement our application. We’ll create an application that only prints a line in our console. For this, we’ll create an object as our entry point and extend the IOApp.Simple trait. Next, we’ll define the abstract method run of the trait:

object SimpleIOApp extends IOApp.Simple {
  val run: IO[Unit] = IO.println("Running with simple app")
}

The run method takes no arguments and returns an effect of type Unit. It is intended for very simple applications and will always exit with a successful exit code. The Cats Effect runtime provides all the necessary configuration, such as an execution context, and also manages threads and the whole lifecycle of the application. We can use val instead of def for our run method because the IO effect is lazily evaluated during runtime invocation.

3. Creating an Application With Arguments

Most applications accept arguments, use custom exit codes to notify the users for not expected behavior, and may use custom resources to run. The default IOApp trait helps us implement applications with all of these features.

We’ll now create an application that will accept command-line arguments. We only need to extend the IOApp trait and then define the run method. The arguments are passed as a pure List instead of an Array. The return type is IO[ExitCode] meaning that we can define our own exit codes to indicate success or failure:

object ArgumentIOApp extends IOApp {
  def run(args: List[String]): IO[ExitCode] =
    IO.println(s"Running with args: ${args.mkString(",")}").as(ExitCode.Success)
}

In this example, we accept any number of arguments and print them in the console, returning a success code. We can further enhance our application and handle the case when no arguments have been provided. We’ll then return a custom error code:

object ArgumentIOApp extends IOApp {
  def run(args: List[String]): IO[ExitCode] =
    if (args.nonEmpty) {
      IO.println(s"Running with args: ${args.mkString(",")}")
        .as(ExitCode.Success)
    } else {
      IO.println("No args provided. Aborting").as(ExitCode(2))
    }
}

Now, we handle the case where we didn’t provide any arguments and abort the program with exit code 2, which indicates that a major error occurred. The library provides a value for an error exit code ExitCode.Error, which maps to exit code 1, but we can use any other valid number we want.

4. Defining Custom Runtime Configuration

So far, we’ve created applications that utilize the runtime configuration that the IOApp provides by default. But there are also cases in which we want to modify it and use a custom one. By default, the IOApp uses the global execution context, but we may stumble upon a case where another implementation suits our needs better. We can achieve that by defining our custom execution context and using it to evaluate computations. Let’s create another application that will use a work-stealing thread pool:

object ContextIOApp extends IOApp {
  private val customExecutionContext: ExecutionContext =
    ExecutionContext.fromExecutor(Executors.newWorkStealingPool(1))

  override def run(args: List[String]): IO[ExitCode] = {
    val computation = IO {
      println(s"Running on thread: ${Thread.currentThread().getName}")
    }

    computation.evalOn(customExecutionContext).as(ExitCode.Success)
  }
}

The only thing we changed is that we’ve defined our new execution context and that we’ve used the evalOn method, upon the IO computation, so it is evaluated with our custom execution context. Pretty straightforward.

5. Using Resources

Since everything is already in place by the trait, we can also use resources in our application. We’ll demonstrate that by creating again an execution context, but handling it as a resource this time:

object ResourceIOApp extends IOApp {
  val customExecutionContext: Resource[IO, ExecutionContext] =
    Resource.make(
      IO(ExecutionContext.fromExecutorService(Executors.newWorkStealingPool(1)))
    ) { ec =>
      IO.println("Shutting down execution contest").as(ec.shutdown())
    }

  def run(args: List[String]): IO[ExitCode] = customExecutionContext
    .use { ec =>
      IO.println(s"Running on thread: ${Thread.currentThread().getName}")
    }
    .as(ExitCode.Success)
}

We’re using the Resource monad to wrap our custom execution context and safely shut it down afterward. Then, with the use method, we make our application run and use it.

6. Interrupting Applications

The Cats Effect library uses fibers for concurrency. Our IOApp can interact with the main fiber and cancel it in case it receives an interrupt signal. This means that it’ll also propagate to all the other fibers and invoke any finalizers we’ve defined, so our application can gracefully shut down. This mechanism, however, has one major side effect. If a finalizer runs forever, the main fiber can’t shut down and it’ll also run forever, and we need to issue a SIGKILL signal for cleanup.

7. Conclusion

In this article, we’ve explored how we can define simple and more complex applications easily with the IOApp trait of the Cats Effect library. We’ve demonstrated how to use custom error codes, our custom execution context and resources, as well as how the application can be interrupted.

The code backing this article is available on GitHub. Once you're logged in as a Baeldung Pro Member, start learning and coding on the project.