Cont
#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.