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:
-
restrict f domain A to those elements where f is defined, i.e.
f': A' -> B
whereA'
is made of alla in A
such that it exists ab in B
whereb = f(a)
Thinking about
f
as a function we can write in Kotlin or Java this path seems a bit tortuous; -
augment B with a synthetic value that maps all a in A where f is not defined
f': A -> B'
where B' isB union { epsilon }
and for all a in A where f is not defined thenepsilon = f'(a)
This second approach is the one we can pursue in our code using data types like
Option
andEither
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 returnNone
. (Same goes forEither
, whenever we cannot returnRight(a)
we will returnLeft(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()
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()
}
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 sameEither
value, be it aRight
value or aLeft
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
[...]
}
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)
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?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.