• #writing-a-program-with-cont

  • #handling-errors

  • #structured-concurrency

    • #arrow-fx-coroutines

      • #parzip

      • #partraverse

      • #racen

      • #bracketcase--resource

    • #kotlinx

      • #withcontext

      • #async

      • #launch

      • #strange-edge-cases

Cont<R, A> represents a function of suspend () -> A that can fail with R (and Throwable), so it's defined by suspend fun <B> fold(f: suspend (R) -> B, g: suspend (A) -> B): B.

So to construct a Cont<R, A> we simply call the cont<R, A> { } DSL, which exposes a rich syntax through the lambda receiver suspend ContEffect<R>.() -> A.

What is interesting about the Cont<R, A> type is that it doesn't rely on any wrappers such as Either, Ior or Validated. Instead Cont<R, A> represents a suspend function, and only when we call fold it will actually create a Continuation and runs the computation (without intercepting). This makes Cont<R, A> a very efficient generic runtime.

Writing a program with Cont

Let's write a small program to read a file from disk, and instead of having the program work exception based we want to turn it into a polymorphic type-safe program.

We'll start by defining a small function that accepts a String, and does some simply validation to check that the path is not empty. If the path is empty, we want to program to result in EmptyPath. So we're immediately going to see how we can raise an error of any arbitrary type R by using the function shift. The name shift comes shifting (or changing, especially unexpectedly), away from the computation and finishing the Continuation with R.

object EmptyPath

fun readFile(path: String): Cont<EmptyPath, Unit> = cont {
if (path.isNotEmpty()) shift(EmptyPath) else Unit

Here we see how we can define a Cont<R, A> which has EmptyPath for the shift type R, and Unit for the success type A.

Patterns like validating a Boolean is very common, and the Cont DSL offers utility functions like kotlin.require and kotlin.requireNotNull. They're named ensure and ensureNotNull to avoid conflicts with the kotlin namespace. So let's rewrite the function from above to use the DSL instead.

fun readFile2(path: String?): Cont<EmptyPath, Unit> = cont {
ensureNotNull(path) { EmptyPath }
ensure(path.isEmpty()) { EmptyPath }

You can get the full code guide/example/example-readme-01.kt.

Now that we have the path, we can read from the File and return it as a domain model Content. We also want to take a look at what exceptions reading from a file might occur FileNotFoundException&SecurityError, so lets make some domain errors for those too. Grouping them as a sealed interface is useful since that way we can resolve all errors in a type safe manner.


Link copied to clipboard