Functional Programming with Kotlin and Arrow – More on Typeclasses
Continuing the Functional Programming with Kotlin and Arrow Part 2: Categories and Functors tutorial, you’ll now go even further, using a specific and common use case, with a better understanding of data types and typeclasses, from Functor to Monad, passing through Applicatives and Semigroups. By Massimo Carli.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Functional Programming with Kotlin and Arrow – More on Typeclasses
30 mins
- Getting Started
- Finding a Functional Fetcher
- Introducing the Result<E,T> Data Type
- Implementing Result<E,T> as Functor
- Practicing with the Bifunctor
- Defining the Applicative Functor
- Setting the Applicative to Work
- Devising a Better Syntax For Applicatives
- Using an Applicative for Validation
- Learning More About Errors With Semigroups
- Presenting Monads
- Creating the Result<E, T> Monad
- Where to Go From Here?
In the previous tutorial Functional Programming with Kotlin and Arrow Part 2: Categories and Functors, you had the opportunity to learn about functors, their meaning in the context of Category theory and how to use them in your code. You also implemented the Maybe functor using both plain Kotlin and the Arrow framework.
In this tutorial, you’ll use a specific and common-use case to go further and develop a better understanding of data types and typeclasses, from functor to monad. You’ll discover applicatives and semigroups, too.
Working through this tutorial, you’ll:
- Learn what a data type is by using the
Result<E,A>
data type in a practical example. - Create a bifunctor typeclass implementation for
Result<E,A>
. - Discover what applicatives are and how to use one to solve a classical problem of creation and validation of entities.
- Explore how a semigroup can be useful in cases of error management.
- Understand what a monad is and why you need this kind of data type in your program.
Time to do some coding magic! :]
Getting Started
Download the materials for this tutorial using the Download Materials button at the top or bottom of this page. Open the project using IntelliJ 2019.x or greater. You can check out its structure in the following image:
It’s important to note that:
- You’re going to write most of
main()
in the external src folder. This is where FunctionalFetcher.kt is. - The arrow module contains the data types and typeclasses submodules. These depend on the Arrow library. You’ll need two modules because the code generation must be done before the one for typeclasses — which usually depends on the previous one.
Start by opening FunctionalFetcher.kt and locating the following code:
// 1
class FetcherException(override val message: String) :
IOException(message)
// 2
object FunctionalFetcher {
fun fetch(url: URL): String {
try {
// 3
with(url.openConnection() as HttpURLConnection) {
requestMethod = "GET"
// 4
val reader = inputStream.bufferedReader()
return reader.lines().asSequence().fold(StringBuilder()) { builder, line ->
builder.append(line)
}.toString()
}
} catch (ioe: IOException) {
// 5
throw FetcherException(ioe.localizedMessage)
}
}
}
This code:
- Declares
FetcherException
as a custom exception. - Defines
FunctionalFetcher
. It containsfetch()
for, well, fetching some content from the network given a URL parameter. - Opens a
HttpURLConnection
with the HTTPGET
. - Reads and accumulates all the lines into a
String
using aStringBuilder
. - Throws a
FetcherException
that encapsulates the error, if any.
You can run the previous code using main()
:
fun main() {
val ok_url = URL("https://jsonplaceholder.typicode.com/todos")
val error_url = URL("https://error_url.txt")
println(FunctionalFetcher.fetch(ok_url))
}
If you use ok_url
, everything should be fine. The JSON content should be displayed as follows:
[
{
"userId":1,
"id":1,
"title":"delectus aut autem",
"completed":false
},
- - -
]
If you use the error_url
, you’ll get an exception like this:
Exception in thread "main" com.raywenderlich.fp.FetcherException: error_url.txt
True, that’s a rather simple example. But you’re going to improve upon it using some interesting functional programming concepts.
Finding a Functional Fetcher
The fetch()
you defined in the previous example has several problems. First, it’s not a pure function; it has some side effects. That’s because it can throw an exception, which isn’t one of the possible values for its return type Int
.
throw FetcherException(ioe.localizedMessage)
expression is of type Nothing
, a subtype of any type and therefore a subtype of Int
, too. Even so, the side-effect problem remains.In the first tutorial of this series, Functional Programming with Kotlin and Arrow: Getting Started, you learned how to deal with side effects. One potential solution is to move it as part of the return type for the function.
In this case, you could replace the previous code in FunctionalFetcher.kt with the following:
object FunctionalFetcher {
// 1
fun fetch(url: URL): Pair<FetcherException?, String?> {
try {
with(url.openConnection() as HttpURLConnection) {
requestMethod = "GET"
val reader = inputStream.bufferedReader()
// 2
return null to reader.lines().asSequence().fold(StringBuilder()) { builder, line ->
builder.append(line)
}.toString()
}
} catch (ioe: IOException) {
// 3
return FetcherException(ioe.localizedMessage) to null
}
}
}
With this code, you have:
- Changed the signature of
fetch()
to return aPair<FetcherException?, String?>
. This makes it compatible with all the possible results in case of error or success. It’s important to note the nullability for the two different types of thePair
. - In case of success, returned the pair instance with
null
as a value for the first property and the JSON text for the second. - In case of error, set the exception as a value of the first property and
null
as a value for the second.
The function is now pure, but it isn’t type-safe. That’s because of Pair<FetcherException?, String?>
. It can represent values where the first and second property is either present or missing. fetch()
can succeed or fail, but not both.
Introducing the Result<E,T> Data Type
A potential solution to the previous type-safety problem is Result<E,T>
. Remember that a data type allows you to add a specific context to some data. For instance, Maybe<T>
allows you to represent the context of the presence or the absence for a value of type T
. This doesn’t depend on the type T
at all. It’s another dimension.
The same holds for Result<E,T>
. Its context is the chance to contain a value of type E
or a value of type T
but not both. Again, this doesn’t depend on either E
or T
.
Result<E,T>
is very similar to Either<A,B>
. Its context offers the chance to have only a value of type A
or type B
, but not both. This is more generally compared to Result<E,T>
, where E
represents a failure, while T
represents a value that results from a successful operation.Create a Result.kt file into the data-type submodule. Then copy the following code:
// 1
sealed class Result<out E, out A>
// 2
class Success<out A>(val a: A) : Result<Nothing, A>()
// 3
class Error<out E>(val e: E) : Result<E, Nothing>()
This code:
- Defines
Result<E,T>
using a sealed class. - In case of success, creates
Success<T>
as the implementation that encapsulates a result of typeT
. - In case of failure, creates
Error<E>
as the implementation that encapsulates the exception of typeE
.
Nothing
, a subtype of any other Kotlin type.You can now use the Result<E,T>
in a new version of your FunctionalFetcher
. Replace the previous code in FunctionalFetcher.kt with the following:
object FunctionalFetcher {
// 1
fun fetch(url: URL): Result<FetcherException, String> {
try {
with(url.openConnection() as HttpURLConnection) {
requestMethod = "GET"
val reader = inputStream.bufferedReader()
val json = reader.lines().asSequence().fold(StringBuilder()) { builder, line ->
builder.append(line)
}.toString()
// 2
return Success(json)
}
} catch (ioe: IOException) {
// 3
return Error(FetcherException(ioe.localizedMessage))
}
}
}
The code is similar to before. But this time you:
- Defined
fetch()
withResult<FetcherException, String>
as a return type. - Returned a
Success<String>
that encapsulates the result in case of success. - Returned a
Error<FetcherException>
that encapsulates the exception in case of error.
Besides type safety and purity, what advantages will you receive by doing this? To better understand, you’ll need to implement some useful typeclasses, starting from the functor.