A Scala “functional programming style” To-Do List application written with Cats

NOTE: If you read my previous article, the new content in this article starts down at The Scala/FP Code section.

Back when I was writing Functional Programming, Simplified I started to write a little Scala/FP “To-Do List” application that you can run from the command line, based on a similar application in the Learn You A Haskell For Great Good book. For reasons I don’t remember, I decided not to include it in the book, and forgot about it until I started using GraalVM (“Graal”) recently.

Graal includes a native image feature lets you compile JVM classes and JAR files into native executables, so as I thought about things I can make faster, I was reminded of the To-Do List app and thought about how cool it would be if it started instantaneously. So I found the old project, blew the dust off of it (updated all of its dependencies), and made a few additions so I could create (a) a single, executable JAR file with sbt-assembly, and (b) a native executable with Graal.

Because I wrote the application for my book, I wrote the code using Scala in a functional programming (FP) style. If you read the book, the code should look very familiar, with the main exception being that I use some tools from the Cats project.

Back to top

How the application works

As mentioned, this is a command-line To-Do List application, and here’s a short demonstration of how it, starting with the v command (which stands for “view”) to show that there’s nothing in the initial list, followed by some add commands and a rm command:

> ./todo 
Command ('h' for help, 'q' to quit)
==> v


Command ('h' for help, 'q' to quit)
==> add wake up
1. wake up

Command ('h' for help, 'q' to quit)
==> add make coffee
1. wake up
2. make coffee

Command ('h' for help, 'q' to quit)
==> add go to work
1. wake up
2. make coffee
3. go to work

Command ('h' for help, 'q' to quit)
==> rm 3
1. wake up
2. make coffee

Command ('h' for help, 'q' to quit)
==> h

Possible commands
-----------------
add <task>       - add a to-do item
h                - show this help text
rm [task number] - remove a task by its number
v                - view the list of tasks
q                - quit
Back to top

The Scala/FP code

If you’re interested in functional programming with Scala, the source code is available at this URL:

Here’s a short overview of how the code works:

  • ToDoListFIO extends App is where the action starts.
  • As its name implies, mainLoop is the application’s main loop. It runs until you enter q.
  • This line of code in the loop converts the user’s input into a Command: scala cmd <- getLine.map(Command.parse _)
  • Commands are then processed in the processCommand method.

Cats’ unsafeRunSync

Something new you’ll see in the code is a call to the Cats Scalaz’s unsafeRunSync function. The Scaladoc for unsafeRunSync states:

Produces the result by running the encapsulated effects as impure side effects.

As the name says, this is an UNSAFE function as it is impure and performs side effects, not to mention blocking, throwing exceptions, and doing other things that are at odds with reasonable software.  You should ideally only call this function *once*, at the very end of your program.

Another way to state that is that with Cats you write all of your I/O code using its IO monad, and then make a single call to unsafeRunSync when you’re ready to trigger the execution of your application. In fact, if you don’t call unsafeRunSync, your application won’t do anything. (Side effects are a very big deal in functional programming, hence the scary “unsafe” method name.)

In functional programming, side effects are kind of a big deal

Thanks to (a) Eric Torreborre and (b) some good Cats documentation, the code for this Cats To-Do List application is improved over the similar code I wrote using Scalaz. Most of the code in ToDoListFIO.scala is significantly better than the code in the original Scalaz version of this app. Here’s the new main loop:

def mainLoop: IO[Unit] = for {
    _     <- IOFunctions.putStr(prompt)
    cmd   <- getLine.map(Command.parse _)
    _     <- if (cmd == Quit) {
                 IO.unit
             } else {
                 processCommand(cmd) >> mainLoop
             }
} yield ()

And here’s the new and improved view method:

def view: IO[Unit] = for {
    lines  <- readFile(datafile)
    result <- IO((for ((line,i) <- lines.zip(Stream from 1)) yield s"$i. $line").mkString("\n"))
    _      <- putStrLn(result + "\n")
} yield ()

I encourage you to read the cats-effect tutorial. I love good documentation, and it helped me to understand their IO implementation. Because of the documentation, when I have some more free time I’ll also update the To-Do List app to use their IOApp rather than the built-in Scala App.

Back to top

Update: Automatically releasing resources with Cats `bracket`

As a brief update, I created readFile and writeFile functions using the Cats bracket function, which follows an acquire/use/release pattern:

// `bracket`
def readFile(filename: String): IO[Seq[String]] = {
    // acquire
    IO(Source.fromFile(filename)).bracket { source =>
        // use
        IO((for (line <- source.getLines) yield line).toVector)
    } { source =>
        // release
        IO(source.close())
    }
}

/**
 * Use Cats `bracket`:
 * https://typelevel.org/cats-effect/tutorial/tutorial.html#what-about-bracket
 */
def writeFile(filename: String, text: String, append: Boolean): IO[Unit] = IO {
    // acquire
    IO(new BufferedWriter(new FileWriter(new File(filename), append))).bracket { bw =>
        // use
        IO(bw.write(text + "\n"))
    } { bw =>
        // release (note that you can put whatever logic you want here, such as logging)
        IO(bw.close())
    }.unsafeRunSync()
}

Note that this is similar to making sure that you close the resource in a finally block after opening it in a try block.

At the moment I don’t know why the writeFile function requires a call to unsafeRunSync(), but I can confirm that it doesn’t do anything unless that’s called.

The Cats bracket documentation also has a little information about error handling, i.e., what to do if there is a problem reading from or writing to the file.

Back to top

Update: Automatically releasing resources with Cats Resource

I also created second versions of readFile and writeFile using the Cats Resource, which is also used for resource management:

// `Resource`
def readFile(filename: String): IO[Seq[String]] = {
    val acquire: IO[BufferedSource] = IO(Source.fromFile(filename))
    Resource.fromAutoCloseable(acquire)
        .use { source =>
            IO {
                val lines = (for (line <- source.getLines) yield line).toVector
                IO(lines)
            }
        }
        .unsafeRunSync()
}

/**
 * Use cats `Resource`
 * Definitely need `unsafeRunSync()` with Resource:
 * https://typelevel.org/cats-effect/datatypes/resource.html
 */
def writeFile(filename: String, text: String, append: Boolean): IO[Unit] = IO {
    val acquire = IO {
        new BufferedWriter(new FileWriter(new File(filename), append))
    }  
    // BufferedWriter extends java.lang.AutoCloseable, so this works
    Resource.fromAutoCloseable(acquire)
        .use(bw => IO(bw.write(text + "\n")))
        .unsafeRunSync()
}

Note in the last comment that fromAutoCloseable works because BufferedWriter extends java.lang.AutoCloseable.

In this case the Cats Resource documentation clearly shows that unsafeRunSync() is required when using Resource (implying that it is lazily evaluated).

Back to top

Hopefully the rest of the code makes sense

If you read my FP book, hopefully the rest of the code will make sense. All of the I/O functions use an IO monad, and the rest of the code consists of pure functions.

Back to top

The Cats documentation

The Typelevel people behind Cats and Cats-Effects have created some nice documentation here:

Back to top

Two bugs (oops)

The code (still) has two bugs that I haven’t fixed.

First, if the todo.dat file doesn’t exist, the “view” command will throw an exception. (You can create an empty initial file to avoid that bug, or fix the code.)

Second, I noticed that I hadn’t fixed a bug related to the “remove” process. If you type in something like this:

rm foo

instead of this:

rm 1

the application will blow up. The remove function assumes that it receives an Int, but I don’t do anything to validate the user input.

I was going to fix these bugs, but I’m short on time, so I thought I’d leave them as an exercise for the interested reader. (My apologies that I’m a little rushed tonight.)

Back to top

Creating a single, executable JAR file

To create a single, executable JAR file, use the sbt assembly command, or use the assembly command at the SBT prompt. That command should “just work” for you, but if you need more information, I wrote this sbt-assembly tutorial as part of the Scala Cookbook.

Back to top

GraalVM

As mentioned, I got back into this project because of Graal. Over the last few weeks I’ve been updating some of my JVM-based shell scripts to use Graal so they’ll start faster, and for however my brain works, it triggered a, “Hey, you wrote a To-Do List app a few years ago” thought. Indeed, using Graal, the app starts up immediately after you type todo.

If you haven’t used Graal yet it takes a little work to set it up initially. Here’s a very short description of what it takes to create a native executable with Graal for this project:

  • Install GraalVM on your system
  • Configure your JAVA_HOME and PATH environment variables to use Graal
  • cd into this project’s Graal directory
  • In my case I don’t use Graal all the time, so I open a new MacOS Terminal tab where I only use Graal, then source the 1setup_graal file to set the necessary GraalVM parameters (i.e., . 1setup_graal)
  • After I run sbt-assembly, I then run the 2compile_graal.sh script to create the todo native image (executable)

While the app starts pretty fast with the java-jar command, it starts instantly when you create the Graal native image, and that’s a really cool feeling, especially if you’ve been sadly used to that initial Java/JVM startup lag time for 20 years.

Back to top

Summary

In summary, if you wanted to see how to write a small but complete functional programming application in Scala using Cats and Cats-Effect — or if you just wanted a command-line To-Do List application — I hope this source code and project is helpful.

Finally, as a bit of self-promotion, if that code is hard to understand, please see my Functional Programming, Simplified book. About 40% of the book is available as a free preview, so hopefully you can get a good feel about whether or not you want to buy the book from that preview.

All the best,
Al

Back to top

Update: Bonus: A look at `IO` and `runUnsafeSync`

A great feature about SBT that’s not shown in its help text is that you can start a Scala REPL from inside SBT, and it will have all of your classes and resources available to you. This gives you a simple way to experiment with things such as Cats’ runUnsafeSync. Here’s the output from a SBT/REPL session where I test my readFile function. First I start the console:

sbt:FPToDoListWithCats> console
[info] Starting scala interpreter...
Welcome to Scala 2.12.8

Then I import my IOFunctions:

scala> import todolist.IOFunctions._
import todolist.IOFunctions._

Now I run readFile on my /etc/passwd file:

scala> readFile("/etc/passwd")
res0: cats.effect.IO[Seq[String]] = IO$274228422

At this point nothing happens. This is because IO is lazily evaluated, meaning that it’s not going to do anything until you force it to. You force it to run by calling unsafeRunSync on the resulting variable, res0:

scala> res0.unsafeRunSync
res1: Seq[String] = Vector(##, # User Database, "# ", # Note that this file is consulted 
directly only when the system is running, # in single-user mode. ...

Now I see the contents of /etc/passwd, and res1 is a Seq[String].

Next, try the same thing on a file that doesn’t exist:

scala> readFile("/etc/passwd-doh")
res2: cats.effect.IO[Seq[String]] = IO$1614653826

Notice again that nothing happens (because IO is lazy). Now this time when I call unsafeRunSync on res2, I get a FileNotFoundException because I intentionally messed up the input filename:

scala> res2.unsafeRunSync
java.io.FileNotFoundException: /etc/passwd-doh (No such file or directory)
  at java.io.FileInputStream.open0(Native Method)
  at java.io.FileInputStream.open(FileInputStream.java:195)
  at java.io.FileInputStream.<init>(FileInputStream.java:138)
  at scala.io.Source$.fromFile(Source.scala:94)
  at scala.io.Source$.fromFile(Source.scala:79)
  at scala.io.Source$.fromFile(Source.scala:57)
  at todolist.IOFunctions$.$anonfun$readFile$1(IOFunctions.scala:65)
  at cats.effect.internals.IORunLoop$.cats$effect$internals$IORunLoop$$loop(IORunLoop.scala:87)
  at cats.effect.internals.IORunLoop$.start(IORunLoop.scala:34)
  at cats.effect.internals.IOBracket$.$anonfun$apply$1(IOBracket.scala:44)
  at cats.effect.internals.IOBracket$.$anonfun$apply$1$adapted(IOBracket.scala:34)
  at cats.effect.internals.IORunLoop$RestartCallback.start(IORunLoop.scala:341)
  at cats.effect.internals.IORunLoop$.cats$effect$internals$IORunLoop$$loop(IORunLoop.scala:119)
  at cats.effect.internals.IORunLoop$.start(IORunLoop.scala:34)
  at cats.effect.IO.unsafeRunAsync(IO.scala:258)
  at cats.effect.internals.IOPlatform$.unsafeResync(IOPlatform.scala:38)
  at cats.effect.IO.unsafeRunTimed(IO.scala:325)
  at cats.effect.IO.unsafeRunSync(IO.scala:240)

I hope this helps to demonstrate a little more about how IO and unsafeRunSync work, as well as how to start a console/REPL session inside SBT so you can easily test Cats, Cats-Effect, and my functions.

Back to top