Two ZIO, Scala CLI, and Scala 3 examples

If you’d like to get started working with the Scala ZIO functional programming (FP) library, here are two little ZIO 101 and ZIO 102 “Hello, world” examples that you can run with Scala CLI and Scala 3.

FP: ZIO + Scala CLI example: “Hello, world” 101

First, here’s a complete ZIO “Hello, world” example that shows everything you need to get started using ZIO with Scala CLI:

//> using scala "3"
//> using lib "dev.zio::zio::2.0.2"

// run me like this:
//     scala-cli Hello1.scala
//     scala-cli Hello1.scala --watch

import zio.{ZIOAppDefault, Console}

object ZioHelloWorld101 extends ZIOAppDefault:
    def run = Console.printLine("Hello, World!")

Assuming that you’re new to ZIO:

  1. ZIOAppDefault is the ZIO interpreter. In Scala 2, an application starts with:

    object Hello extends App

    where App is a type that’s provided by the Scala SDK. But when using ZIO, you extend ZIOAppDefault instead, and its interpreter runs your application.
    1. To be clear, when I say interpreter, this does not mean that it’s an interpreted language or library. ZIO is a high-performance library, and Scala/ZIO applications are compiled to run on the JVM, and power huge websites and applications like Caesars and many others. In this case it just means that this is where the action starts, specifically with a run function you define.
  2. When you create a run function inside a ZIOAppDefault object, that function is where your application starts running. So in this example, run is invoked by ZIOAppDefault, and it calls Console.printLine. (Note that Console is provided by the zio.Console import statement.)

Running the ZIO app with Scala CLI

To run this little application, save that code to a file named Hello1.scala, and run it once like this:

$ scala-cli Hello1.scala

Or continuously compile and run it like this:

$ scala-cli Hello1.scala --watch

The second approach is really terrific when you want to make modifications to your application, and have it be automatically recompiled and run.

Explicit import statements

Assuming you’re new to ZIO, note that I intentionally use this explicit import statement:

import zio.{ZIOAppDefault, Console}

instead of this:

import zio.*

When you’re first starting to work with a new tool like ZIO, I think it’s important to see where things are coming from.

ZIO + Scala CLI example: “Hello, world” 102

Here’s the next step in working with ZIO, where I first create a ZIO variable named hello, and then assign that to the body of the run function:

//> using scala "3"
//> using lib "dev.zio::zio::2.0.2"

// run me like this:
//     scala-cli Hello2.scala
//     scala-cli Hello2.scala --watch

import zio.{ZIOAppDefault, ZIO, Console}
import java.io.IOException

object ZioHelloWorld102 extends ZIOAppDefault:
    val hello: ZIO[Any, IOException, Unit] =
        Console.printLine("Hello, World!")
    def run = hello

As before, save that code to a file named Hello2.scala and then compile and run it in either of these two ways:

$ scala-cli Hello2.scala
$ scala-cli Hello2.scala --watch

The big difference in this application is that I create this instance of a ZIO variable:

val hello: ZIO[Any, IOException, Unit] =

If you read my book, Learn Functional Programming The Fast Way, you’ll already know what this type signature means:

ZIO[Any, IOException, Unit]

If you don’t know what it means, I’ll just repeat the ZIO mantra here, that a ZIO instance is a description of a workflow, and you can think of it as a function, that given an R can produce either an A or an E:

f(r: R): Either[E, A]

More information

I’d explain more, but Learn Functional Programming The Fast Way is not too expensive, and it explains a lot of things in detail, so I’ll leave it up to that.

All the best,
Alvin Alexander
(Somewhere in Colorado)