Effect polymorphism with Arrow FX

With Arrow FX improving on every Arrow release, we can experiment with it and try to get as much as possible from its innovative approach to side effects handling. In this post, using the modules structure presented in a previous post, we are going to make our code completely independent from the effect implementation (usually IO), making it ready to be used also with other runtime implementations like the ones based on Reactor or Rx. Also, differently from the Tagless Final approach, we’ll be able to do this without even use the Higher Kinded Types feature (if not just in a little utility function).

Note: the features of Arrow used in this post are currently based on what the release version 0.10.0 will be. It hasn’t been release yet, but it will be very soon. If you want to try it, just use the SNAPSHOT builds.

A little recap of how Arrow FX works

If this is the first time you are hearing about Arrow FX, I suggest you to watch this very good introductory talk from Jorge Castillo. Otherwise, let’s just make a very small recap about it. One of the features of the Kotlin programming language is the support for functions marked as suspend; a suspend function can be called only from another suspend function or from a specific context that basically provides a runtime environment where the functions will be executed. When you mark a function this way, the compiler will translate the suspend function to a state machine (this process is described here very well). So, under the hood, when you define a suspend function, you are defining a state machine that describes the computation, and when you call the function you are basically instantiating this state machine. Only when it will process the code, it will generate the real return value of the computation. If you notice, this is the description of a mechanism able to defer side effects and that’s exactly what the IO monad does. So, in Arrow FX, a suspend function returning a value type (e.g Option<String>) can be considered the same of an IO returning the same value type (IO<Option<String>>); also mapping functions to convert between the two cases are provided. So, just to sum up, in Arrow FX effectful functions must be suspend functions, and that’s all.

The side effects handling module

First of all, we need to define a new module that will define how to manage the side effects. We’ll base this module on the Concurrent interface already provided by Arrow; our module name will be HasSideEffectHandling, defined as follow:

interface HasSideEffectHandling<F> : Concurrent<F> {
    suspend fun <A> Kind<F, A>.asSuspended(): A
    suspend fun <A> attempt(f: suspend () -> A): Either<Throwable, A>
}

Basically all the functions needed for the effect handling are taken from the Concurrent interface; but since we need also an abstracted way to convert an effect instance in a suspended function, we are adding the asSuspended extension function in our environment. The attempt function, instead, is just an utility function that is not strictly needed in order to make everything work.

Here is the implementation of this interface for the IO effect:

object IOSideEffectHandling : HasSideEffectHandling<ForIO>,
        Concurrent<ForIO> by IO.concurrent() {

    override suspend fun <A> Kind<ForIO, A>.asSuspended(): A {
        return this.fix().suspended()
    }

    override suspend fun <A> attempt(f: suspend () -> A): Either<Throwable, A> {
        return effect(f).attempt().asSuspended()
    }
}

We are delegating to the IO.concurrent() instance the Concurrent implementation, while our two additional functions are easily implemented using the constructs that IO already provides. We can consider this code as library code; we don’t need to edit it while working on our production code and maybe in the future these functions will be provided directly by Arrow.

An example module with side effects

In order to understand how to use the HasSideEffectHandling module, we can define a Console module that exports just two functions:

interface HasConsole<ENV> {
    suspend fun ENV.printToConsole(s: String)
    suspend fun ENV.readFromConsole(): Option<String>
}

As you can see, both functions are marked as suspend. This means that they will generate a side effect that we are going to defer. In this example, we can consider that the printToConsole function can’t fail, while the readFromConsole function could fail, and in case of failure, we are going to return an empty Option value. Here is a possible implementation:

 interface LiveConsole<F, ENV> : HasConsole<ENV>
    where ENV : HasSideEffectHandling<F> {

    override suspend fun ENV.printToConsole(s: String) {
        println(s)
    }

    override suspend fun ENV.readFromConsole(): Option<String> {
        return attempt { readLine() }
                .fold({ Option.empty() },
                      { Option.fromNullable(it) })
    }
}

First of all, the implementation requires that the environment provides a side effect handling implementation. We need to add an additional generic type F that is the effect we are going to use.

Then, we are implementing the printToConsole function that just calls the println function. The difference between using our printToConsole function and not the println function is that our one is marked as suspend, and so the side effect will be deferred untile the “end of the world” (that usually is the entry point of our program).

The readFromConsole function, instead, has a bit more logic than the other one. Since it is going to generate a side effect, we are marking it as suspended too. Also, we are wrapping the Kotlin standard readLine function inside an attempt block. Attempt basically means “try executing this effectful code and catch potential exceptions, returning an Either instance (right if the execution is successful, left if an exception has been thrown). Then we can fold over the Either in order to create the return value we want.

About the attempt call, I strongly suggest to use it every time something wrong can happen while executing side effects, and then map the Throwable left type into a more type safe error description (like a domain sum type).

Executing the program

A simple program that uses the Console module is the following:

private suspend fun Env.program(): Option<String> {
    printToConsole("Hello")
    return readFromConsole()
}

That can be executed in this way:

fun main() {
    val output = unsafe { runBlocking {
        env.effect { env.program() }
    } }
    println(output)
}

Ok, but we are still missing the environment definition and the corresponding instance.

How to compose the environment

The environment type definition will be the following; this is the first time we specify the real effect type that we are going to use (IO):

interface Env: HasConsole<Env>, HasSideEffectHandling<ForIO>

The definition for this example is quite simple; here is the corresponding composition for getting a real instance:

val env: Env = object : Env,
        HasConsole<Env> by object : LiveConsole<ForIO, Env> {},
        HasSideEffectHandling<ForIO> by IOSideEffectHandling
{}

In order to create the environment, we need to provide an implementation of the HasSideEffectHandling for the IO effect, that is the one we have implemented at the beginning of this post. Obviously, when wiring the LiveConsole module instance, we need to use the same effect we are using for the HasSideEffectHandling module.

Conclusions

With Arrow FX we have defined a module that triggers side effects and handles them in an abstract way, without specifying the concrete effect. So, we can write our code without thinking about Reactor, Rx, IO or any other effect implementation. The, when composing the environment for our application, we can specify the effect we are going to use. This is the same advantage we can get with the Tagless Final approach, but without the burden of Higher Kinded Types, typeclasses over the effect and so on.

You can find the full example source code here.

If you want to leave a comment, send a reply here or send me a direct message.

2 thoughts on “Effect polymorphism with Arrow FX

  1. Hi Emanuele,

    Thanks for the blog post! It helps me understand the Arrow Fx better.

    Just found a typo in “The readFromConsole function, instead, has a bit more logic that the other one”, “that the other one” should be “than the other one”.

    Besides, it seems the “reCAPTCHA” has blocked the Post Comment button at the bottom on this page.(Or maybe it’s just me with a slow network)

Comments are closed.