App 2: Using Either Instead of Try

The next step on your learning path is that we’ll now replace all of the Try instances in the TDL app with Either instances.

Actually, I don’t think it’s worth doing all of that. Instead, all I’m going to do is to replace them in the Database class, and then I think you can imagine how to use those in the rest of the application.

So, let’s get started, recalling that all of the following functions are inside the Database class:

class Database(dbFilename: String):
    // all functions are in here
end Database

The insert database function

The first function we wrote was the insert function, and it currently looks like this:

def insert(record: String): Try[Unit] =
    writeToFile(List(record), true)

So our first task is to replace that Try with an Either. So we start by updating insert’s function signature:

def insert(record: String): Either[Throwable, Boolean] =
    writeToFile(List(record), true)

As a refresher, recall that Either returns a Left or a Right, and by convention the Left holds the error, so I write insert’s return type like this:

Either[Throwable, Boolean]
       ---------  -------
          ^          ^
       Possible   Success
        Error      Value
         In         In
        Left       Right

Because this function relies on writeToFile, I need to update it next.

writeToFile

The writeToFile function signature currently looks like this:

private def writeToFile(lines: Seq[String], append: Boolean): Try[Unit] =

and we know that we want it to look like this:

private def writeToFile(lines: Seq[String], append: Boolean):
    Either[Throwable, Boolean] =

That updates its function signature, and now a simple way to convert the function body is to convert the Success to a Right, and the Failure to a Left:

private def writeToFile(lines: Seq[String], append: Boolean):
    Either[Throwable, Boolean] =

    var bw: BufferedWriter = null
    try
        bw = BufferedWriter(FileWriter(File(dbFilename), append))
        for line <- lines do bw.write(s"$line\n")
        Right(true)
    catch
        case e: Throwable => Left(e)
    finally
        if bw != null then bw.close

So that’s a quick update.

selectAll

Similarly, selectAll’s signature currently looks like this:

def selectAll(): Try[Seq[String]] =

and we again replace Try with Either:

def selectAll(): Either[Throwable, Seq[String]] = 

After that, in the function body, we again replace Success with Right and Failure with Left:

def selectAll(): Try[Seq[String]] =
    var bufferedSource: scala.io.BufferedSource = null
    try
        bufferedSource = Source.fromFile(dbFilename)
        val lines = for line <- bufferedSource.getLines yield line
        Right(lines.toList)
    catch
        case t: Throwable => Left(t)
    finally
        if bufferedSource != null then bufferedSource.close

And that’s all there is to that.

One thing to observe here is that selectAll’s signature is a bit more complicated, because it has a list of strings as the Right parameter, but by now I suspect that you may be getting used to seeing these types (and the value of them). Here’s a breakdown of those types:

Either[Throwable, Seq[String]]
       ---------  -----------
          ^            ^
       Possible     Success
        Error        Value
         In           In
        Left         Right

If you can read that, you’re doing just fine.

delete

Lastly, the delete function currently returns this Try:

def delete(indexToDelete: Int): Try[Int] = ...
                                --------

and we want to replace it with this Either:

def delete(indexToDelete: Int): Either[Throwable, Int] = ...
                                ----------------------

For the purpose of converting delete so it returns an Either, a simple solution is to convert maybeNumRowsDeleted — which is currently a Try — to an Either. You do that by calling toEither on maybeNumRowsDeleted at the end of the function:

def delete(indexToDelete: Int): Either[Throwable, Int] = 
    val maybeNumRowsDeleted = for
        rows           <- selectAll()
        newRows        =  CollectionUtils
                              .removeElementByIndex(rows, indexToDelete)
        numRowsDeleted =  rows.size - newRows.size
        _              <- writeToFile(newRows, false)
    yield
        numRowsDeleted
    maybeNumRowsDeleted.toEither   // <== THE CHANGE

Fortunately Try types have this toEither method, which makes the conversion simple.

Keys

So there you have it, we rewrote the database functions to use Either instead of Try. Because Either requires that you declare both types the function can return, its signatures are a little longer to read, but they’re also very explicit, and more flexible in how they work.

As a reminder of what that last point means, in all of the database functions I declared the Left type as an Throwable, but they could have returned a String, an enum, or any other data type that makes the most sense for the current problem.

And this flexibility is very important for where we’re going next.

Attribution

The “finish line” icon in this video comes from this icons8.com link.