Failure is not an Option - Functional Error Handling in Kotlin. Part 5 - Embracing Either

This is Part 5 in a series looking at functional error handling in Kotlin. The parts are

Encapsulating failure

Let’s start this episode with a case where using the functional Either type has some real advantages over exceptions.

Let’s say that I’m processing a potentially huge file, translating every line to a number that I want to sum.

fun sumLines(reader: BufferedReader): Long =
    reader.lineSequence().map { Integer.parseInt(it) }.fold(0L, Long::plus)

We run this for 5 minutes before discovering that some lines are corrupt, so that parseInt throws, aborting the whole process. Sigh. An expedient approach is just to catch the exception and log in place.

fun sumLines(reader: BufferedReader): Long =
    reader.lineSequence()
        .map {
            try {
                Integer.parseInt(it)
            } catch (e: Exception) {
                System.err.println(e.message)
                0
            }
        }
        .fold(0L, Long::plus)

but this hard-codes our error handling and, as is common with exceptions, hides the happy path which is the point of the algorithm.

Remembering our functional definition of parseInt

fun parseInt(s: String): Either<Exception, Int> = resultOf { Integer.parseInt(s) }

we could delay our gratification and then fold over the sequence

fun sumFile(reader: BufferedReader): Long {
    val ints: Sequence<Either<Exception, Int>> = reader.lineSequence().map(::parseInt)
    return ints.fold(0L) { acc, intResult ->
        intResult.fold(
            { exception -> System.err.println(exception.message); acc },
            { result -> acc + result }
        )
    }
}

Hmmm, I’m not sure that is much better, especially with those two nested folds that really don’t mean the same thing unless you close your eyes and imagine very hard. The least we can do is to de-nest them

fun sumFile(reader: BufferedReader): Long {
    val ints: Sequence<Int> = reader.lineSequence()
        .map(::parseInt)
        .map { intResult ->
            intResult.fold(
                { exception -> System.err.println(exception.message); 0 },
                { result -> result }
            )
        }
    return ints.fold(0L, Long::plus)
}

We can make things better by writing a little extension to allow us to not use Either.fold when we just want to use the value

inline fun <L, R> Either<L, R>.orElse(whenLeft: (L) -> R): R = when(this) {
    is Left -> whenLeft(this.l)
    is Right -> this.r
}

so now we have

fun sumFile(reader: BufferedReader): Long {
    val ints: Sequence<Int> = reader.lineSequence()
        .map(::parseInt)
        .map {
            it.orElse {
                exception -> System.err.println(exception.message)
                0
            }
        }
    return ints.fold(0L, Long::plus)
}

and we can pull that idea up a level

fun <L, R> Sequence<Either<L, R>>.eachOrElse(whenLeft: (L) -> R): Sequence<R> = this.map {
    it.orElse(whenLeft)
}

leaving us with the the still-a-bit-ugly

fun sumFile(reader: BufferedReader): Long {
    val ints: Sequence<Either<Exception, Int>> = reader.lineSequence().map(::parseInt)
    return ints.eachOrElse( { exception -> System.err.println(exception) ; 0 }).fold(0L, Long::plus)
}

but we’ve successfully separated what to do in the case of an error from our fundamental flow, allowing us to move the responsibility to the caller

fun sumFile(reader: BufferedReader): Long =
    sumFile(reader) { exception ->
        System.err.println(exception)
        0
    }

fun sumFile(reader: BufferedReader, onError: (Exception) -> Int): Long =
    reader.lineSequence().map(::parseInt).eachOrElse(onError).fold(0L, Long::plus)

This is typical of the functional programming approach - push all the nastiness to the outside of the system, leaving nice pure declarative and referentially transparent core algorithms. We could of course do the same thing with exception-based code

fun sumFile(reader: BufferedReader, onError: (Exception) -> Int): Long =
    reader.lineSequence()
        .map {
            try {
                Integer.parseInt(it)
            } catch (e: Exception) {
                onError(e)
            }
        }
        .fold(0L, Long::plus)

which you might find easier to read at first, and is certainly a bit more efficient, but is lacking a certain, I don’t know, je ne sais quois.

Dealing with Control Flow

If you try this functional style, sooner or later you’ll run into places where, well, things get icky. Let’s interpret three strings as ints and add them, the old-fashioned way, with exceptions

fun addAsInts(s1: String, s2: String, s3: String): Either<Exception, Int> = resultOf {
    Integer.parseInt(s1) + Integer.parseInt(s2) + Integer.parseInt(s3)
}

Now how about with our functional parseInt and map / flatMap?

fun addAsInts(s1: String, s2: String, s3: String): Either<Exception, Int> =
    parseInt(s1).flatMap { i1 ->
        parseInt(s2).flatMap { i2 ->
            parseInt(s3).map { i3 ->
                i1 + i2 + i3
            }
        }
    }

“Really Duncan - that’s better than exceptions?” I hear you cry.

No, I can’t say that it is; not with a straight face anyway. We can make things a bit better if we’re prepared to use our orElse and early returns

fun addAsInts(s1: String, s2: String, s3: String): Either<Exception, Int> = resultOf {
    parseInt(s1).orElse { return Left(it) } +
        parseInt(s2).orElse { return Left(it) } +
        parseInt(s3).orElse { return Left(it) }
}

which in turn can be sweetened a bit with

inline fun <L, R> Either<L, R>.onLeft(abortWith: (Left<L>) -> Nothing): R = when(this) {
    is Left -> abortWith(this)
    is Right -> this.r
}

to give us

fun addAsInts(s1: String, s2: String, s3: String): Either<Exception, Int> = resultOf {
    parseInt(s1).onLeft { return it } +
        parseInt(s2).onLeft { return it } +
        parseInt(s3).onLeft { return it }
}

but frankly my functional programmer friends hate early returns (which also complicate referential transparency) as much as they hate exceptions, so this is frowned upon.

What is required here is what Haskell calls do-notation, which is a way to sequence expressions only evaluating the next if the previous didn’t ‘fail’. This is of course the role of exceptions and/or early returns in other languages.

Functional programmers value referential transparency so much that Scala programmers are resigned to using their for-comprehensions to solve this problem, and Arrow bends Kotlin’s coroutines to the same end.

Personally I’d rather keep things on a level that I can understand, and maybe even implement myself. So I’d be inclined to define

fun <R> Either<Exception, R>.orThrow(): R = when(this) {
    is Left -> throw this.l
    is Right -> this.r
}

and use it in moderation in cases like this

fun addAsInts(s1: String, s2: String, s3: String): Either<Exception, Int> = resultOf {
    parseInt(s1).orThrow() +
        parseInt(s2).orThrow() +
        parseInt(s3).orThrow()
}
[ If you liked this, you could share it on Twitter. ]