16
Handling Side Effects
Written by Massimo Carli
Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as
text.You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.
In Chapter 15, “Managing State”, you implemented the State<S, T>
data type and gave it the superpowers of a functor, applicative and monad. You learned that State<S, T>
describes the context of some state of type S
that changes based on some transformations you apply every time you interact with the value of type T
in it. Making the State<S, T>
a monad means you can compose different functions of type (A) -> State<S, B>
. The State<S, B>
data type is just a way to encapsulate a StateTransformer<S, T>
. This means you can compose functions of type (A) -> StateTransformer<S, B>
that’s basically a function of type (A) -> (S) -> Pair<S, B>
. If you use uncurry
, this is equivalent to a function of type Pair<A, S> -> Pair<S, B>
.
Now, think about what an impure function is. It’s a function whose body is an expression that is not referentially transparent because, when executed, it changes the state of the world outside the function body. This means it has a side effect, which breaks composition. But if a side effect is a change in the state of the world, the question now is: Can you somehow represent the state of the world as the type S
you use in a State<S, T>
and encapsulate the side effect as a simple transformation? In other words, if you define the type World
as representing the current state of the world, can you use State<World, T>
as a data type that encapsulates any possible side effects?
The answer to this question is yes, and the specific data type is IO<T>
.
In this chapter, you’ll learn:
-
How to implement Hello World in a pure, functional way.
-
What the
IO<T>
data type is. -
How to use
IO<T>
to compose functions with side effects. -
What monad comprehension is.
-
How to use
IO<T>
in a practical example. -
How to use suspendable functions to solve basically the same problem
IO<T>
wants to solve.
This is an essential chapter, and now it’s time to do some magic! :]
From State<S, T> to IO<T>
Hello World is probably the most popular application to implement when learning a new language. This is mainly because it’s very simple and allows you to see how to execute some of the fundamental tasks in common between all applications, like compilation, execution, debugging and so on.
The app you’ll implement here is a little bit different because it’ll allow you to read a name from the standard input and then print a greeting message. Open Greetings.kt in the material for this chapter, and write the following code:
fun main() {
print("What's your name? ") // 1
val name = Scanner(System.`in`).nextLine() // 2
print("Hello $name\n") // 3
}
In this code, you:
- Print a message asking the user their name.
- Use
Scanner
to read the name you type as input and save it toname
. - Use
name
to format and print a greeting message.
Feel free to run it, and, after entering your name, you’ll get an output like the one in Figure 16.1:
Note: When you run the app, just put the cursor after the input message to insert your name, as shown in Figure 16.1. Then, type your name and press Enter.
The previous code works very well, but the expression in main
is anything but pure. Using Scanner
, you read the name
from the standard input. Using print
, you display the result on the standard output. They’re both side effects: interaction with the rest of the world. So, how can you create the previous program but handle side effects in a pure and functional way?
The introduction of this chapter already gave you a hint. What if you think of the external world as a giant state you change when you read the name
and write the greeting message?
You can follow this idea starting with the definition of a type you call World
. Add the following in the World.kt file:
typealias World = Unit
Here, you define World
as a simple alias for the Unit
type. At this point, how you define the World
type doesn’t really matter. You’ll see later if how you define World
really matters or not. In the same file, add the following:
typealias SideEffect = (World) -> World
This is interesting because you’re defining a SideEffect
as any function from an initial state of the World
to, probably, a different state of the same World
. But here, something strange is happening. If you have a function of type SideEffect
able to capture the whole World
in input and return a different version of it, you’ve essentially eliminated the concept of a side effect because everything happens in the context of that function. In this case, all the functions would be pure.
To prove that you can modify the initial program as the composition of the function, you use:
-
readName
, which reads the name from the standard input. -
printString
, which prints aString
to the standard output.
readName
’s type is (World) -> Pair<String, World>
because it receives the World
in input and provides the String
for the name and a new version of the World
in output. Add the following code to Greetings.kt:
val readName: (World) -> Pair<String, World> = { w: World ->
Scanner(System.`in`).nextLine() to World
}
printString
‘s type is a little more interesting. It’s (String, World) -> World
because it receives the String
to print and the current World
in input, returning the new state for the World
. In this case, you have two input parameters, but you can apply curry
, getting the type (String) -> (World) -> World
. With the previous definition of SideEffect
, you can say that the type of printString
is (String) -> SideEffect
. In this way, you make the definition more explicit. Then, add the following code to the same Greetings.kt file:
val printString: (String) -> SideEffect = { str: String ->
{ a: World ->
print(str) to World
}
}
Note: As you’ll see later, a type like
(String) -> SideEffect
says something crucial. It says thatprintString
doesn’t execute a side effect but returns a description of it. This is the main reason it’s a pure function now.
Now, test each of the previous functions by running the following code:
fun main() {
// ...
readName(World) pipe ::println // 1
printString("Hello Max \n")(World) pipe ::println // 2
}
In this code, you invoke:
-
readName
, passing the current state of theWorld
, printing in output thename
you read from the standard input. -
printString
with a name, and then the function of type(World) -> World
with the current state of theWorld
.
After you insert the name in input, you’ll get the output in Figure 16.2:
In the image, you can see:
- An example of a
String
input. - The output of
readName
, which is aPair<String, Unit>
of theString
in input and the new state of theWorld
you previously defined usingUnit
. - The output you get using
print
inprintString
. - The output of
printString
, which is again aUnit
representing the new state of theWorld
.
This is very interesting, but what you achieved now isn’t actually what you need. You need a way to compose readName
and printString
as pure functions and get an app that works like the initial one.
Pure greetings
To accomplish your goal, you basically need to create askNameAndPrintGreetings
, whose type is (World) -> World
. The final state of the world is the one where you asked for a name
and printed a greeting message.
fun askNameAndPrintGreetings(): (World) -> World = // 1
{ w0: World -> // 2
val w1 = printString("What's your name? ")(w0) // 3
val (name, w2) = readName(w1) // 4
printString("Hello $name! \n")(w2) // 5
}
fun main() {
askNameAndPrintGreetings()(World) pipe ::println
}
Hiding the world
In Chapter 15, “Managing State”, you implemented the State<S, T>
monad as a data type encapsulating a StateTransformer<S, T>
you defined like this:
typealias StateTransformer<S, T> = (S) -> Pair<T, S>
typealias WorldT<T> = (World) -> Pair<T, World>
val readNameT: WorldT<String> = readName
val printStringT: (String) -> WorldT<Unit> = { str: String ->
{ w: World ->
Unit to printString(str)(w)
}
}
infix fun <A, B> WorldT<A>.myOp( // 1
fn: (A) -> WorldT<B> // 2
): WorldT<B> = TODO() // 3
WorldT<A> // 1
-> (A) -> WorldT<B> // 2
-> WorldT<B> // 3
(World) -> Pair<A, World> // 1
-> (A) -> (World) -> Pair<B, World> // 2
-> (World) -> Pair<B, World> // 3
(World) -> Pair<A, World> // 1
-> (Pair<A, World>) -> Pair<B, World> // 2
-> (World) -> Pair<B, World> // 3
infix fun <A, B> WorldT<A>.myOp(
fn: (A) -> WorldT<B>
): WorldT<B> = this compose fn.uncurryP()
fun <T1, T2, R> ((T1) -> (T2) -> R).uncurryP():
Fun<Pair<T1, T2>, R> = { p: Pair<T1, T2> ->
this(p.first)(p.second)
}
A hidden greeting
The first implementation of askNameAndPrintGreetings
you created forced you to carry the world on at each step.
fun askNameAndPrintGreetings(): (World) -> World =
{ w0: World ->
val w1 = printString("What's your name? ")(w0)
val (name, w2) = readName(w1)
printString("Hello $name! \n")(w2)
}
fun askNameAndPrintGreetingsT(): WorldT<Unit> = // 1
printStringT("What's your name? ") myOp { _ -> // 2
readNameT myOp { name -> // 3
printStringT("Hello $name! \n") // 4
}
}
fun main() {
askNameAndPrintGreetingsT()(World) pipe ::println
}
The IO<T> monad
So far, you’ve worked with WorldT<T>
, which is an abstraction representing a World
transformation. This World
transformation is basically a side effect. It’s not so different from StateTransformer<S, T>
when you replace S
with the type World
.
The IO<T> data type
In the lib sub-package in this chapter’s material, you find all the files related to the State<S, T>
monad. In State.kt, you find the following definition:
data class State<S, T>(
val st: StateTransformer<S, T>
)
data class IO<T>(val wt: WorldT<T>)
Implementing lift
As you know, lift
is the function that allows you to get, in this case, an IO<T>
from a WorldT<T>
. Depending on the context, you might find the same function with a name like return
or pure
. Anyway, following the same approach you saw in the previous section, you implement it by replacing the existing code in IO.kt with the following:
data class IO<T>(val wt: WorldT<T>) {
companion object { // 1
@JvmStatic
fun <S, T> lift(
value: T // 2
): IO<T> = // 3
IO { w -> value to w } // 4
}
}
operator fun <T> IO<T>.invoke(w: World) = wt(w)
IO<T> as a functor
The next step is to give IO<T>
the power of a functor and provide an implementation of map
. This is usually very easy, and this case is no different. Open IO.kt, and add the following code:
fun <A, B> IO<A>.map(
fn: Fun<A, B>
): IO<B> =
IO { w0 ->
val (a, w1) = this(w0) // Or wt(w0)
fn(a) to w1
}
IO<T> as an applicative functor
Applicative functors are useful when you want to apply functions with multiple parameters. In the same IO.kt, add the following code:
fun <T, R> IO<T>.ap(
fn: IO<(T) -> R>
): IO<R> =
IO { w0: World ->
val (t, w1) = this(w0)
val (fnValue, w2) = fn(w1)
fnValue(t) to w2
}
infix fun <A, B> IO<(A) -> B>.appl(a: IO<A>) = a.ap(this)
IO<T> as a monad
Finally, you want to give IO<T>
the superpower of a monad, adding the implementation of flatMap
like this to IO.kt:
fun <A, B> IO<A>.flatMap(
fn: (A) -> IO<B>
): IO<B> =
IO { w0: World ->
val (a, w1) = this(w0)
fn(a)(w1)
}
infix fun <A, B> WorldT<A>.myOp(
fn: (A) -> WorldT<B>
): WorldT<B> = this compose fn.uncurryP()
Monadic greetings
In the previous sections, you implemented askNameAndPrintGreetingsT
like this:
fun askNameAndPrintGreetingsT(): WorldT<Unit> =
printStringT("What's your name? ") myOp { _ ->
readNameT myOp { name ->
printStringT("Hello $name! \n")
}
}
val readName: (World) -> Pair<String, World> = { w: World ->
Scanner(System.`in`).nextLine() to World
}
val readNameT: WorldT<String> = readName
val printStringT: (String) -> WorldT<Unit> = { str: String ->
{ w: World ->
Unit to printString(str)(w)
}
}
val readNameM: IO<String> = IO(readNameT) // 1
val printStringM: (String) -> IO<Unit> =
printStringT compose ::IO // 2
fun <T> IO<T>.bind(): T = this(World).first
fun askNameAndPrintGreetingsIO() : () -> Unit = { // 1
printStringM("What's your name? ").bind() // 2
val name = readNameM.bind() // 3
printStringM("Hello $name! \n").bind() // 4
}
fun main() {
askNameAndPrintGreetingsIO().invoke()
}
The meaning of IO<T>
The greeting example you’ve implemented so far is a great example of a practical use of IO<T>
. However, in Chapter 14, “Error Handling With Functional Programming”, you learned that sometimes things go wrong. For instance, you implemented readNameM
like:
val readNameM: IO<String> = IO(readNameT)
val readNameT: WorldT<String> = readName
val readName: (World) -> Pair<String, World> = { w: World ->
Scanner(System.`in`).nextLine() to World
}
val safeReadName: (World) -> Pair<Result<String>, World> =
{ w: World -> // 1
try {
Result.success(Scanner(System.`in`).nextLine()) to World
} catch (rte: RuntimeException) {
Result.failure<String>(rte) to World
}
}
val safeReadNameError: (World) -> Pair<Result<String>, World> =
{ w: World -> // 2
Result.failure<String>(
RuntimeException("Something went wrong!")
) to World
}
val safeReadNameT: WorldT<Result<String>> = safeReadName // 3
val safePrintStringT: (String) -> WorldT<Result<Unit>> =
{ str: String ->
{ w: World ->
Result.success(Unit) to printString(str)(w)
}
}
val safeReadNameM: IO<Result<String>> = IO(safeReadNameT) // 1
val safePrintStringM: (String) -> IO<Result<Unit>> =
safePrintStringT compose ::IO // 2
fun safeAskNameAndPrintGreetingsIO(): () -> Result<Unit> = { // 1
safePrintStringM("What's your name? ").bind() // 2
.flatMap { _ -> safeReadNameM.bind() } // 3
.flatMap { name ->
safePrintStringM("Hello $name!\n").bind() // 4
}
}
fun main() {
safeAskNameAndPrintGreetingsIO().invoke().fold(
onSuccess = { _ ->
// All good
},
onFailure = { ex ->
println("Error: $ex")
}
)
}
val safeReadNameT: WorldT<Result<String>> = safeReadNameError
suspend fun readStringCo(): String = // 1
Scanner(System.`in`).nextLine()
suspend fun printStringCo(str: String) = // 2
print(str)
@DelicateCoroutinesApi
fun main() {
runBlocking { // 3
printStringCo("What's your name? ") // 4
val name = async { readStringCo() }.await() // 5
printStringCo("Hello $name!\n") // 6
}
}
Key points
- A pure function doesn’t have any side effects.
- A side effect represents a change in the state of the world.
- The
State<S, T>
data type allows you to handle state transitions in a transparent and pure way. - You can think of the state of the world as a specific type
S
inState<S, T>
and considerStateTransformer<S, T>
as a way to describe a transformation of the world. - A transformation of the world is another way to define a side effect.
- Functions with IO operations are impure by definition.
- You can think of the
IO<T>
data type as a special case ofState<S, T>
, whereS
is the state of the world. In this way, all functions are pure. - You can easily give
IO<T>
the superpowers of a functor, applicative functor and monad. - The
IO<T>
data type is a way to decouple a side effect from its description. -
IO<T>
contains the description of a side effect but doesn’t immediately execute it. - In Kotlin, a suspendable function allows you to achieve the same result as
IO<T>
in a more idiomatic and simple way.
Where to go from here?
Congratulations! With this chapter, you took another crucial step in the study of the main concepts of functional programming with Kotlin. State management with the IO<T>
monad is one of the most challenging topics forcing you to think functionally. In the last part of the chapter, you saw how the IO<T>
monad can be easily replaced with the use of coroutines. In the following chapter, you’ll see even more about this topic and implement some more magic! :]