Some time ago, I watched the talk FP to the max. While the end of the talk is quite Scala-ish, the beginning can be of interest regardless of the language.
In the talk, the speaker tries to migrate a standard imperative-like application using a functional approach. I wanted to check if it was possible to do the same in Kotlin.
This is the 1st post in the From Imperative to Functional Programming focus series.Other posts include:
- From Imperative to Functional Programming using Arrow (this post)
- From Imperative to Functional Programming, an approach
- From Imperative to Functional Programming: a grouping issue (and how to solve it)
- From Imperative to Functional Programming: the Dijkstra algorithm
The original code
Translated in Kotlin, the original code looks like the following:
private val random = SecureRandom()
fun main(args: Array<String>) {
println("What is your name?")
val name = readLine()
println("Hello, $name, welcome to the game!")
var exec = true
while(exec) {
val number = random.nextInt(5) + 1
println("Dear $name, please guess a number from 1 to 5:")
val guess = readLine()?.toInt()
if (guess == number) println("You guessed right, $name!")
else println("You guessed wrong, $name! The number was $number")
println("Do you want to continue, $name?")
when(readLine()) {
"y" -> exec = true
"n" -> exec = false
}
}
}
Partial functions
Functions are at the foundation of Function Programming.
Among them, partial functions are an issue.
Those are functions that cannot be executed for some specific values.
For example, /
is a partial function, because no number can be divided by 0.
In the above snippet, there are 2 partial functions.
In Java, calling a partial functions with an invalid value would cause an exception. This exception could be caught and managed at any place in the calling stack. This is unfortunately not possible with FP.
Let’s check how we can handle that in a FP-friendly way.
- Calling
toInt()
on aString
Invalid values are those that are not numbers.
The FP approach is to wrap the call in a Try
object.
It’s trivial to create and use:
fun parseInt(input: String?) = Try { input?.toInt() }
val guess: Try<Int?> = parseInt(readLine())
when(guess) {
is Failure -> println("You did not enter a number!")
is Success<Int?> -> {
if (guess.value == number) println("You guessed right, $name!")
else println("You guessed wrong, $name! The number was $number")
}
}
Actually, this is still imperative programming.
Try
offers true FP capabilities with the fold()
method.
It requires two lambda parameters, the first one executed in case of failure, the second one in case of success:
(readLine() as String) (1)
.safeToInt() (2)
.fold(
{ println("You did not enter a number!") }, (3)
{ (4)
if (it == number) println("You guessed right, $name!")
else println("You guessed wrong, $name! The number was $number")
}
)
private fun String.safeToInt() = Try { this.toInt() }
1 | Needs to be cast to the non-nullable String , as it cannot be null in that case |
2 | The `parseInt() can better be handled as an extension function |
3 | Will be called if the String cannot be parsed to a number |
4 | Will be called if it can |
- Deciding whether to continue, or not
The snippet handles only y
and n
, but not other values.
We just need a third branch, to handle values that are invalid.
Let’s add it:
var cont = true
while(cont) {
cont = false
println("Do you want to continue, $name?")
when (readLine()?.toLowerCase()) {
"y" -> exec = true
"n" -> exec = false
else -> cont = true (1)
}
}
1 | Irrelevant inputs are managed |
Introducing IO
The IO pattern is a widespread pattern in FP. It involves wrapping values, exceptions and lambdas in IO instances, and passing them around.
Arrow is FP library for Kotlin.
It provides FP abstractions, such as IO
.
The following diagram is an (incomplete) overview of the IO class in Arrow:
With IO
, every system call can be wrapped in pure functions with IO instances:
private fun putStrLn(line: String): IO<Unit> = IO { println(line) }
private fun getStrLn(): IO<String?> = IO { readLine() }
From loops to recursivity
FP eschews state: a pure function’s return value should only depend on its input parameter(s) - and not state.
Loops, whether for
or while
, use state to break out of the loop.
Hence, loops are not FP-friendly.
In FP, loops should be replaced with recursive calls.
For example, the continue loop can be replaced with:
private fun checkContinue(name: String?): Boolean {
println("Do you want to continue, $name?")
(readLine() as String).transform { it.toLowerCase() }.transform {
when (it) {
"y" -> true
"n" -> false
else -> checkContinue(name)
}
}
}
private fun <T> String.transform(f: (String) -> T) = f(this) (1)
1 | The default String.map() method iterates over the characters of the String |
Let’s wrap the above snippet in IO:
private fun checkContinue(name: String?): IO<Boolean> = IO.monad().binding { (1)
putStrLn("Do you want to continue, $name?").bind()
(getStrLn()).map { it?.toLowerCase() }.map {
when (it) {
"y" -> true.liftIO() (2)
"n" -> false.liftIO()
else -> checkContinue(name) (3)
}
}
.flatten() (4)
.bind() (5)
}
.fix() (6)
1 | The IO.monad().binding method provides a context for IO |
2 | Transforms a simple scalar value into an IO |
3 | Cannot bind() here as the binding context is not available |
4 | Flattens the IO<IO<?>> into a simple IO<?> |
5 | Binds the IO to its wrapped value |
6 | Downcasts the type, from IOOf<A> to IO<A> |
Triggering the wrapper
IO are wrappers around computations, but no computation actually occurs until a termination method is called. They are several such methods available:
unsafeRunSync()
unsafeRunAsync()
runAsync()
- etc.
For our example, the unsafeRunSync()
is more than enough in our case:
fun main(args: Array<String>) {
println("What is your name?")
val name = readLine()
println("Hello, $name, welcome to the game!")
gameLoop(name).unsafeRunSync() (1)
}
1 | Triggers the computation |
This can be pushed "to the max" like that:
fun main(args: Array<String>) {
mainIO(args).unsafeRunSync()
}
private fun mainIO(args: Array<String>): IO<Unit> = IO.monad().binding {
putStrLn("What is your name?").bind()
val name = getStrLn().bind()
putStrLn("Hello, $name, welcome to the game!").bind()
gameLoop(name).bind()
}.fix()
Some more nitpicking
IO
offers a map()
method.
It can be used to transform the value returned after reading the value input by the user, e.g.:
private fun checkContinue(name: String?): IO<Boolean> = IO.monad().binding {
putStrLn("Do you want to continue, $name?").bind()
(getStrLn()).map { it.toLowerCase() }.map { (1)
when (it) {
"y" -> true.liftIO()
"n" -> false.liftIO()
else -> checkContinue(name)
}
}.flatten()
.bind()
}.fix()
private fun gameLoop(name: String?): IO<Unit> = IO.monad().binding {
putStrLn("Dear $name, please guess a number from 1 to 5:").bind()
getStrLn().safeToInt().fold(
{ putStrLn("You did not enter a number!").bind() },
{
val number = nextInt(5).map { it + 1 }.bind()
if (it.bind() == number) println("You guessed right, $name!")
else putStrLn("You guessed wrong, $name! The number was $number").bind()
}
)
checkContinue(name).map {
(if (it) gameLoop(name)
else Unit.liftIO())
}.flatten()
.bind()
}.fix()
private fun nextInt(upper: Int): IO<Int> = IO { random.nextInt(upper) }
private fun IO<String>.safeToInt() = Try { map { it.toInt() }}
1 | No need for transform() anymore |
Conclusion
Kotlin, with the help of Arrow, makes it possible to move from legacy imperative code to a more functional-oriented one. Whether it’s relevant or not depends one on’s context. In all cases, that’s one more trick in one’s sleeve.