DEV Community

Angelo Sciarra
Angelo Sciarra

Posted on

How to live in a pure world

To the infinity and beyond

Hello folks,
here I am with a follow-up of the previous article A matter of purity

In it I talked about what is a pure function and why should you even care.

I can imagine some questions may have arisen while reading it:

  • how to make partial functions total?
  • how to make pure and referentially transparent functions that perform some sort of side effect (like IO)?

Let's address these issues one at a time.

Make partial functions total

Just to get everybody on the same page let me recall what a partial function is:

given f: A -> B f is partial if it is not defined for all a in A.

How can one work on f to get an f' such that f' is total?

Well, there are 2 paths one can take:

  1. restrict f domain A to those elements where f is defined, i.e.

    f': A' -> B where A' is made of all a in A such that it exists a b in B where b = f(a)

    Thinking about f as a function we can write in Kotlin or Java this path seems a bit tortuous;

  2. augment B with a synthetic value that maps all a in A where f is not defined

    f': A -> B' where B' is B union { epsilon } and for all a in A where f is not defined then epsilon = f'(a)

    This second approach is the one we can pursue in our code using data types like Option and Either because when we say that our function

    val f: (A) -> Option<B>
    

    what we are saying is exactly that whenever we cannot return Some(a) we will return None. (Same goes for Either, whenever we cannot return Right(a) we will return Left(error)).

So that should give an answer about totality.

Make side effects pure

Once we have made our functions total, have we made them pure?

What about functions performing IO?

Let's see an example

fun readFileLines(path: String): List<String> = 
    File(path).readLines()
Enter fullscreen mode Exit fullscreen mode

We know that this function may throw an IOException so, from what we said above about making partial functions total, we could turn it into

fun readFileLines(path: String): Either<IOException, List<String>> =
    try {
        File(path).readLines().right()
    } catch (e: IOException) {
        e.left()
    }
Enter fullscreen mode Exit fullscreen mode

Now the function is total (you can argue that returning IOException as the error type may not be the best design choice, but we'll leave it like that for the time being).

But is it pure? Is it true that:

given the same path input it will always return the same Either value, be it a Right value or a Left one?

The answer is no because anytime we call this function we are interacting with the external world and we have no control or knowledge of what this world may look like at any time.

In the functional universe (well, to be precise, in the Haskell orbiting part) there is an abstraction meant exactly to describe this interaction. Let me introduce you the

IO (monad)!

Leaving aside the scary m-word, let's focus on the IO abstraction.

The idea behind it is simple: given that the interaction with the external world is not predictable, we are going to work with a type that is designed like:

data class IO<A>(run: () -> A) {

    fun map<B>(f: (A) -> B): IO<B> = // omitted

    fun flatMap<B>(f: (A) -> IO<B>): IO<B> = // omitted

    [...]
}
Enter fullscreen mode Exit fullscreen mode

Can you see it? All that matters is that () -> A.

Why?

Because it is as if you are saying: provided that I promise to give you an A, you can transform it via map, if your transformation is a pure function, or via flatMap, if your transformation has again some kind of interaction with the external world.

We have transformed the real, unpredictable interaction with the external world with a description of it, simply introducing a delay. What we gained by doing so is that this description is once again a value, and we can play around with values freely.

NOTE: Kotlin has a construct built in the language, suspended functions, that can be thought a bit as our delay function (() -> A). For that reason, the creators and maintainers of Arrow (the functional companion for Kotlin) have decided to redesign their implementation of the IO data type to exploit suspended functions (and coroutines). I promise I will make an article dedicated to it, as soon as I manage to fully understand it myself :D

Conclusion

Is it easy to live in a pure world?

That is not really for me to tell you, only you can judge if what you gain is worth the pain (if you want to call it so). Nonetheless, I think that expanding your horizons and knowing about what it means to live in a pure world will provide you with a new perspective on how to interact with the world that can be useful also if you choose to remain in an impure world.

I hope to have given you some explanations of concepts you may have heard of but never managed to wrap your head around.

Well folks, what else to add?

To infinity and beyond!

Top comments (2)

Collapse
 
iquardt profile image
Iven Marquardt • Edited

Pure IO through coroutines - sounds quite interesting. The main difference to a thunk like () => A seems to be the capability to send a value to the suspended function. Are Kotlin's coroutines multi-shot, i.e. can you resume them from a certain position more than once?

Collapse
 
eureka84 profile image
Angelo Sciarra • Edited

Hi Iven, not really an expert on the coroutines subject but I think the continuation is just one. The idea with IO anyhow should be to build up a full description of your workflow and run it only once at the border of your application.