Introduction

Life was easy until now. Our whole program runs in memory, and we don’t have to worry about unreliable network connections, timeouts, or other external factors. But as we start to integrate with the outside world, we need to consider these aspects and how they affect our application.

In this part, we’ll introduce Cats Effect library that helps us deal with side effects and asynchronous programming in a functional way.

Dependencies

First, let’s add Cats Effect to our dependencies. Update project/Dependencies.scala:

import sbt.*

object Dependencies {
  private object Version {
    val catsEffect      = "3.6.3"
    val iron            = "3.0.0"
    val munit           = "1.1.1"
    val munitScalaCheck = "1.1.0"
  }

  lazy val cats: ModuleID = "org.typelevel" %% "cats-effect" % Version.catsEffect

  lazy val iron: ModuleID = "io.github.iltotore" %% "iron" % Version.iron

  lazy val test: Seq[ModuleID] = {
    Seq("org.scalameta" %% "munit" % Version.munit, "org.scalameta" %% "munit-scalacheck" % Version.munitScalaCheck)
      .map(_ % "test")
  }
}

And update build.sbt:

[...]
lazy val dependencies = {
  Dependencies.test :+
    Dependencies.cats :+
    Dependencies.iron
}
[...]

Cats Effect

Cats Effect is a runtime layer that helps you build and scale complex, high-performance asynchronous and parallel software. It provides a powerful abstraction for dealing with side effects, concurrency, and resource management in a functional way. We’ll focus on practice, but I highly encourage you to read through the Cats Effect docs to better understand the fundamentals.

Understanding IO

When working with Cats Effect, you’ll use the IO type extensively. Here’s the key insight: IO doesn’t run your code immediately – it creates a blueprint for code that will run later.

Think of it this way:

  • Writing println("Hello") immediately prints to the console.
  • Writing IO(println("Hello")) creates a description of printing to the console, but doesn’t actually print anything until you run it (e.g., with IOApp that we’ll create in the next paragraph).

This separation is powerful because:

  1. You can compose operations before running them.
  2. You control when effects happen.
  3. You get consistent error handling (IO captures exceptions and lets you handle them functionally).
  4. You can safely work with concurrent operations.

IOApp

To use Cats Effect in our app, we need to update Main.scala and wrap our program in an IOApp.Simple:

package dev.kamgy

import cats.effect.{IO, IOApp}

object Main extends IOApp.Simple {
  override val run: IO[Unit] = {
    IO.println("Hello, world!")
  }
}

IOApp takes care of properly setting up and tearing down the runtime needed to run IO operations. This also avoids the need to run IO.unsafeRunSync on your own.

You should be able to run this code and see Hello, world! printed to the console.

As we mentioned previously, IO allows you to compose multiple operations together. To demonstrate this, we can update Main.scala to:

package dev.kamgy

import scala.concurrent.duration.DurationInt

import cats.effect.{IO, IOApp}

object Main extends IOApp.Simple {
  override val run: IO[Unit] =
    for {
      _ <- IO.println("Creating resources...")
      _ <- IO.sleep(500.millis)
      _ <- IO.println("Resources created successfully!")
      _ <- IO.println("Starting the application...")
      _ <- IO.sleep(500.millis)
      _ <- IO.println("Application started successfully!")
    } yield ()
}

Summary

In this part, we laid the foundation for connecting our Idle RPG game to the outside world by introducing Cats Effect.

We’re now equipped with the tools to safely handle external interactions like database operations, HTTP requests, and file operations.

In the next part, we’ll work on storing data in a database.

As always, you can find the complete code in the GitHub repository .