16.
Handling Side Effects
Written by Massimo Carli
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
Scannerto read the name you type as input and save it toname. - Use
nameto 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 aStringto 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) -> SideEffectsays something crucial. It says thatprintStringdoesn’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 thenameyou read from the standard input. -
printStringwith a name, and then the function of type(World) -> Worldwith 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
Stringinput. - The output of
readName, which is aPair<String, Unit>of theStringin input and the new state of theWorldyou previously defined usingUnit. - The output you get using
printinprintString. - The output of
printString, which is again aUnitrepresenting 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.
Given readName and printString, you can add the following code to Greetings.kt:
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
}
In this code, you:
- Define
askNameAndPrintGreetingsas a function of type(World) -> World, which you can also refer to asSideEffectif you prefer. - Return a function with the input parameter
w0of typeWorld. Withw0, you represent the initial state for the world. - Invoke
printString, passing the message to ask the name along with the current state of the world,w0. Here, you get the new state of the world you store inw1. - Pass the current state of the world,
w1, as an input parameter forreadName, getting aPair<String, World>. Using destructuring, you save theStringinnameand the new state of the world inw2. - Invoke
printString, passing the greeting message to print along with the state of the world,w2, you got in the previous step.printStringalso returns the final state of the world you use as the return value foraskNameAndPrintGreetings.
Test this code by running the following code:
fun main() {
askNameAndPrintGreetings()(World) pipe ::println
}
Here, you invoke askNameAndPrintGreetings, passing the initial state of the world in input. When the function completes, you send the output to println.
After you insert a String in input, you’ll get:
Here, you:
- Provide a
Stringas input. - Print the greeting message.
- Print the output for
askNameAndPrintGreetings, which is theUnityou used as the value representing the state of the world.
Besides the fact that askNameAndPrintGreetings works, you should also note these other crucial points:
-
askNameAndPrintGreetingsaccepts the current state of the world as input and provides a new one. - The code in
askNameAndPrintGreetings‘s body looks like it’s imperative. You invokeprintString,readNameandprintStringagain, one after the other. This way of composing functions is called monad comprehension. It’s the name for a programming idiom available in multiple languages, like JavaScript, F#, Scala or Haskell. Monad comprehension describes a way to compose sequential chains of actions in a style that feels natural for programmers used to procedural languages. - At each step, you pass the current state of the world, getting the new state. You’re not using
Worldat all.
The last point is fundamental because it would be very useful to remove the requirement of passing the world ahead. This problem is similar to the one related to the State<S, T> monad you learned in Chapter 15, “Managing State”.
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>
Note: You find the
State<S, T>definition in the State.kt file in the lib sub-package in this chapter’s material.
Here, you can follow the same process, defining the WorldT type like this in the World.kt file:
typealias WorldT<T> = (World) -> Pair<T, World>
Just be careful that T is a generic type parameter, but World is the actual type you defined earlier as a simple type alias of Unit. If you now look at the readName you defined in Greetings.kt, you can see that its type is exactly the same as WorldT<String>. You can add the following definition without any compilation errors in Greetings.kt:
val readNameT: WorldT<String> = readName
What about printString? Just remember that its type is (String) -> (World) -> World, which is basically equivalent to (String)-> WorldT<Unit>. This is a little bit more complicated, because it requires a little change. In Greetings.kt, add the following definition:
val printStringT: (String) -> WorldT<Unit> = { str: String ->
{ w: World ->
Unit to printString(str)(w)
}
}
Now, you need to solve a problem you’re used to: composition. You need to compose printStringT with readNameT and printStringT again to implement your beautiful Greetings program in a pure, functional way.
Before doing that, it’s useful to review the types of all the functions in play:
-
printStringThas the type(String) -> WorldT<Unit>, which is(String) -> (World) -> Pair<Unit, World>. -
readNameThas the typeWorldT<String>, which is(World) -> Pair<String, World>.
In the implementation for the Greetings app, you need to compose:
-
printStringTof type(String) -> WorldT<Unit>with -
readNameTof typeWorldT<String>with -
printStringTagain.
One way to do that is the definition of a function — you temporarily define myOp in World.kt with the following signature:
infix fun <A, B> WorldT<A>.myOp( // 1
fn: (A) -> WorldT<B> // 2
): WorldT<B> = TODO() // 3
Note: You’ll find out the best name for
myOplater. You might already know! :]
Here, you define myOp:
- As an infix extension function of the
WorldT<A>type. Remember that this is equivalent to having the receiver of typeWorldT<A>as the first argument. - With an input parameter
fnof type(A) -> WorldT<B>. - Returning a
WorldT<B>.
This means that if you have a WorldT<A> and a function of type (B) -> WorldT<B>, you can use the myOp operator and get a WorldT<B>.
To make all the types explicit, write its type like the following:
WorldT<A> // 1
-> (A) -> WorldT<B> // 2
-> WorldT<B> // 3
Remember that WorldT<A> is just an alias for the (World) -> Pair<A, World> type, which allows you to rewrite the previous definition like the following:
(World) -> Pair<A, World> // 1
-> (A) -> (World) -> Pair<B, World> // 2
-> (World) -> Pair<B, World> // 3
Now, look at 2. Remember that uncurry is a higher-order function that allows you to start with a function of type (A) -> (B) -> C and get an equivalent function of type (A, B) -> C. If you consider the equivalence between a function of two input parameters of type A and B, and a Pair<A, B>, you can write:
(World) -> Pair<A, World> // 1
-> (Pair<A, World>) -> Pair<B, World> // 2
-> (World) -> Pair<B, World> // 3
This means you can compose the WorldT<A> in 1 with the uncurried version of fn at 2. This allows you to write the implementation of myOp like the following, which you should write in World.kt:
infix fun <A, B> WorldT<A>.myOp(
fn: (A) -> WorldT<B>
): WorldT<B> = this compose fn.uncurryP()
Note how you need to use uncurryP, a function you find in Curry.kt in the lib sub-package of this chapter’s material. It uses Pair<A, B> as input in place of two parameters of types A and B, respectively.
fun <T1, T2, R> ((T1) -> (T2) -> R).uncurryP():
Fun<Pair<T1, T2>, R> = { p: Pair<T1, T2> ->
this(p.first)(p.second)
}
Now, it’s time to use it.
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)
}
Now it’s time to get rid of the world — wow! — and implement askNameAndPrintGreetingsT, like the following you can write in Greetings.kt:
fun askNameAndPrintGreetingsT(): WorldT<Unit> = // 1
printStringT("What's your name? ") myOp { _ -> // 2
readNameT myOp { name -> // 3
printStringT("Hello $name! \n") // 4
}
}
In this code, you:
- Define
askNameAndPrintGreetingsTas a function returning aWorldT<Unit>, which is basically the world transformation encapsulatingUnitas a value. - Invoke
printStringTfor printing a message. Remember that this returns aWorldT<Unit>. UsingmyOp, you composeprintStringTwith a lambda function that ignores the input parameter, which is of typeUnit. - Use
readNameT, which returns aWorldT<String>. To access theStringit returns, you usemyOpto composereadNameTwith a function you define using a lambda expression withnamein input. - Finally, use
nameto print the greetings usingprintStringT.
If you don’t believe this works, just add this to Greetings.kt:
fun main() {
askNameAndPrintGreetingsT()(World) pipe ::println
}
Here, you just invoke askNameAndPrintGreetingsT, getting the value of type World<Unit> you then use, passing World as a parameter. You’ll get an output similar to the one you saw earlier:
Here, you:
- Enter your
nameafter the “What’s your name?” message. - Get the greeting in output.
- See the output of
askNameAndPrintGreetingsT, which is aPair<Unit, World>. You get the doubleUnitbecause you representedWorldasUnitat the beginning of the chapter.
As you can see, you’ve got some good news and some bad news. The good news is that the World is now hidden. In the askNameAndPrintGreetingsT body, you don’t have to receive any World and pass it on to the following methods.
The bad news is that you probably don’t want to indent the code using all those {} and create many lambdas as arguments of myOp.
Don’t worry. In the previous code, you definitely saw a lot of what you learned in Chapter 15, “Managing State”. It’s now time to follow the same process and define the IO<T> monad.
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.
Now, you need to:
- Encapsulate
WorldT<T>into a data type you’ll callIO<T>. - Implement
liftto encapsulate anyWorldT<T>into anIO<T>. - Give
IO<T>the power of a functor. - Extend
IO<T>with the power of a functor applicative. - Give
IO<T>the superpower of a monad. - Finally, you’ll use
IO<T>to solve the indentation problem you got withaskNameAndPrintGreetingsT, implementing a sort of monad comprehension.
If you follow the same process you did for State<S, T>, replacing S with World, the first five points are simple. You could do them as an exercise or just follow along. :]
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>
)
In the case of IO<T>, you just know that S in State<S, T> is the type World and that instead of StateTransformer<S, T>, you have WorldT<T>.
Knowing how these types relate to each other, open IO.kt and add the following definition:
data class IO<T>(val wt: WorldT<T>)
Congratulations! You just created the IO<T> data type. As you’ll see, it’s very simple and powerful. Now, it’s time to add even more power, starting with the implementation of lift.
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
}
}
Here, you:
- Implement
liftas a static function in acompanion object. - Define
Tas the input parameter type. - Set
IO<T>as the type for the output. - Create the
IO<T>using the default constructor, passing a lambda of typeWorldT<T>that simply returns aPair<T, World>in output.
To make the previous code simpler, the same way you did for State<S, T>, add the following code:
operator fun <T> IO<T>.invoke(w: World) = wt(w)
This allows you to apply the WorldT<T> transformation in IO<T> using the () operator directly.
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
}
This is the classic implementation of map as an extension function of IO<A> accepting a Fun<A, B> as input and returning an IO<B> as output.
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
}
You also add the infix version with this:
infix fun <A, B> IO<(A) -> B>.appl(a: IO<A>) = a.ap(this)
Again, you just started from the same functions for State<S, T>, replaced State with IO and removed S. In the implementation, you used wn instead of sn to represent the n-th state of the world.
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)
}
But, hey! You’ve seen this already, right? Earlier, you implemented myOp like this:
infix fun <A, B> WorldT<A>.myOp(
fn: (A) -> WorldT<B>
): WorldT<B> = this compose fn.uncurryP()
Besides the fact that myOp is an extension function for WorldT<T> and flatMap for IO<A>, they both accept a function that reminds you of the Kleisli category.
The former accepts a function of type (A) -> WorldT<B> and the latter one of type (A) -> IO<B>. Yeah, they represent the same concept!
But how can you use all this magic? With a monadic greeting, of course!
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")
}
}
Using printStringT and readNameT, you implemented as:
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)
}
}
In this code, note how readNameT is just an alias for readName you used to make its type, WorldT<String>, explicit. In any case, now you need to work with IO<T>. To do this, write the following code in IOGreetings.kt:
val readNameM: IO<String> = IO(readNameT) // 1
val printStringM: (String) -> IO<Unit> =
printStringT compose ::IO // 2
In this code, you define:
-
readNameMas a monadic version ofreadNameTyou get just by encapsulating it into anIO<String>. -
printStringMas a function accepting aStringas input and returning anIO<Unit>. BecausereadNameTreturns aWorldT<Unit>, you just need to compose it with the primary constructor ofIO<Unit>.
Now, you have two functions working with IO<T> without exposing WorldT<T> anymore. If you look at readNameM, you realize it returns an IO<String> with the name you might’ve inserted during execution. Of course, you need to access that value. To do this, just add the following code to the same file:
fun <T> IO<T>.bind(): T = this(World).first
Because IO<T> represents a way to encapsulate a WorldT<T>, and because you have just one representation of the world, which is World, you can extract the value of type T by invoking the transformation with it to get a Pair<T, World>. Then, you get the first property of the Pair<T, World> and return it.
This is simple and powerful because you can finally write the following in IOGreetings.kt:
fun askNameAndPrintGreetingsIO() : () -> Unit = { // 1
printStringM("What's your name? ").bind() // 2
val name = readNameM.bind() // 3
printStringM("Hello $name! \n").bind() // 4
}
In this code, you:
- Define
askNameAndPrintGreetingsIOas the monadic version of the greeting app. Note how it returns a value of type() -> Unityou put as explicit here just to emphasize it. - Invoke
printStringMwith the input message. Remember how this returns anIO<Unit>. Withbind, you actually extract theUnitvalue you don’t even use. This means you could also avoid invokingbind(), but you invoke it here so all the instructions follow the same pattern. - Invoke
bindonreadNameMand assign theStringyou get toname. In this case, invokingbindis necessary because you need thenameto pass to the next step. - Print the output using
printStringM. In this case, invokingbind()allows you to returnUnitfromaskNameAndPrintGreetingsIO. Without that, you’d have returnedIO<Unit>.
This is great! You’ve learned some critical points. Here, you:
- Don’t explicitly pass the reference to the world ahead anymore.
- Don’t have all the indentations you had in
askNameAndPrintGreetingsT. - Write your code in a way that’s familiar to a procedural approach.
The only thing you need now is to test if it works. In the same IOGreetings.kt file, add and run the following code:
fun main() {
askNameAndPrintGreetingsIO().invoke()
}
And you’ll get:
Here, you:
- Type your name as input.
- Print the greeting.
Just note how you don’t have to pass any World to the askNameAndPrintGreetingsIO. The IO<T> monad does this all under the hood.
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)
Where:
val readNameT: WorldT<String> = readName
val readName: (World) -> Pair<String, World> = { w: World ->
Scanner(System.`in`).nextLine() to World
}
What if readName fails for some reason? In that case, you should write a safe version of it. Open Safe.kt, and write the following code:
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
In this code, you define:
-
safeReadNameas a function that returns theStringyou read from the standard input, encapsulated into aResult<String>. -
safeReadNameError, which is a failing version ofsafeReadName. -
safeReadNameTas a function of typeWorldT<Result<String>>.
Now, add the following:
val safePrintStringT: (String) -> WorldT<Result<Unit>> =
{ str: String ->
{ w: World ->
Result.success(Unit) to printString(str)(w)
}
}
This version of printStringT returns a WorldT<Result<Unit>> instead of a WorldT<Unit>. Now, you can create the monadic versions by adding the following code:
val safeReadNameM: IO<Result<String>> = IO(safeReadNameT) // 1
val safePrintStringM: (String) -> IO<Result<Unit>> =
safePrintStringT compose ::IO // 2
In this case, you define:
-
safeReadNameMas encapsulatingsafeReadNameTinto aIO<Result<String>>. -
safePrintStringMas a function of typeIO<Result<Unit>>encapsulatingsafePrintStringT.
Now, the version of the greeting app becomes the following:
fun safeAskNameAndPrintGreetingsIO(): () -> Result<Unit> = { // 1
safePrintStringM("What's your name? ").bind() // 2
.flatMap { _ -> safeReadNameM.bind() } // 3
.flatMap { name ->
safePrintStringM("Hello $name!\n").bind() // 4
}
}
In this code, you:
- Define
safeAskNameAndPrintGreetingsIOas a function returning aResult<Unit>. - Invoke
safePrintStringM, passing a message, and get theResult<Unit>usingbind. - Use
flatMap, passing a lambda that returns aResult<String>with the message in input or the error if something went wrong. - Invoke
flatMapagain, passing a lambda that invokessafePrintStringMwith the greeting in output.
This function is pure and works perfectly. To test this, just run the following code:
fun main() {
safeAskNameAndPrintGreetingsIO().invoke().fold(
onSuccess = { _ ->
// All good
},
onFailure = { ex ->
println("Error: $ex")
}
)
}
Getting the usual output:
To test how this works in case of error, just replace safeReadName with safeReadNameError in the definition of safeReadNameT, like this:
val safeReadNameT: WorldT<Result<String>> = safeReadNameError
Run main again, and you’ll get:
This is very good, but it might look complicated. This is why some frameworks like Arrow chose to use suspend functions instead of the IO<T> monad, which is an excellent decision.
Open Coroutines.kt and write the following code:
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
}
}
In this code, you:
- Write the logic for reading the input
StringintoreadStringCo, which is a suspendable function. It’s important to note how, in this case, thesuspendis redundant, but it allows you to mark it as a function that has a side effect. - Do the same for
printStringCothat just prints the inputStringto the standard output. - Just like
IO<T>, a suspendable function allows you to wrap a side effect into a block. This is very important because when doing this, you’re not actually running that code — you’re just describing it. In this case, you’re saying that there will eventually be some other component that runs that suspendable function. In this case, that component is aSchedulerin theCoroutineContextused byrunBlocking. - Invoke
printStringCoas a normal function. - Use
asyncto wait for the inputStringyou ask for. - Finally, use
printStringCoto print the greeting message.
Coroutines also allow you to handle exceptions in a robust and easy way.
Note: To learn more about coroutines, check out Kotlin Coroutines by Tutorials.
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
SinState<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>, whereSis 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! :]