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?
Presenting Monads
In the previous section, you implemented Result<E,A>
as a functor and applicative that you can use for the FunctionalFetcher. The result is a JSON you store as a type String. In practice, you need to parse the JSON and return an object of a different type.
Create a file named FunctionalFetcherApp.kt into the main module. Then copy the following code:
object FunctionalFetcherResult {
// 1
fun fetch(url: URL): Result<FetcherException, String> {
try {
with(url.openConnection() as HttpURLConnection) {
requestMethod = "GET"
val reader = inputStream.bufferedReader()
val result = reader.lines()
.asSequence().fold(StringBuilder()) { builder, line ->
builder.append(line)
}.toString()
// 2
return Success(result)
}
} catch (ioe: IOException) {
// 3
return Error(FetcherException(ioe.localizedMessage))
}
}
}
This should be nothing new. You’ve simply:
- Defined
fetch()
using theResult<FetcherException,String>
as a return type. - Returned a
Success<String>
if you successfully receive some data. - Returned an
Error<FetcherException>
in case of an error.
In case of success, you’ll get a text containing the JSON. It will look like this:
[
{
"userId":1,
"id":1,
"title":"delectus aut autem",
"completed":false
},
- - -
]
It contains the JSON for an array of items that you can represent with the following data class, which you can copy in the same Kotlin file. It also represents a Task in an application for your TODOs.
@Serializable
data class Task(val userId: Int, val id: Int, val title: String, val completed: Boolean)
You now need to parse JSON into a List<Task>
. You can do this with the following function copied into the same file:
fun parseTasks(jsonData: String): Result<JsonDecodingException, List<Task>> {
val json = Json(JsonConfiguration.Stable)
try {
return Success(json.parse(Task.serializer().list, jsonData))
} catch (ex: JsonDecodingException) {
return Error(ex)
}
}
This code tries to parse String
in input. It returns a Success<List<Task>>
in case of success or an Error<JsonDecodingException>
in case of error.
Suppose you want to use this function to parse the result of your FunctionalFetcherResult
object. You can then copy the following code into the same FunctionalFetcherApp.kt:
fun main() {
val ok_url = URL("https://jsonplaceholder.typicode.com/todos")
val error_url = URL("https://error_url.txt")
// 1
FunctionalFetcherResult.fetch(ok_url).mapRight {
// 2
parseTasks(it)
}.mapLeft {
// 3
println("Error $it")
}.mapRight {
// 4
println("Output $it")
}
}
In this code you’ve:
- Invoked
fetch()
with theok_url
. - Used
parseTasks()
as a parameter tomap()
. - Printed the error message in case of an error.
- Printed the result in case of success.
Build and run. In case of success, you’ll get an output like the following:
Output com.raywenderlich.fp.Success@131276c2
This is because of the parseTasks()
in mapRight
. It returns a Result<JsonDecodingException, List<Task>>
, which is the one you print in the end. In case of success, it would be nice to have List<Task>
directly as an implicit parameter for the last mapRight
. This is where the monad comes in.
Creating the Result<E, T> Monad
Crete a new ResultMonad.kt in the typeclass sub-module. Copy the following code:
// 1
fun <E, T, R> Result<E, T>.flatMap(fn: (T) -> Result<E, R>): Result<E, R> = when (this) {
// 2
is Success<T> -> fn(this.a)
// 3
is Error<E> -> this
}
Here you define a monad typeclass with flatMap()
. This function:
- Has a parameter of type
(T) -> Result<E, R>
. - Returns the result of the function you get as parameter on the current value. In this case, the current object is a
Success<T>
. - Returns the same object in case of an
Error<E>
.
Now you can replace main()
in FunctionalFetcherApp.kt with the following:
fun main() {
val ok_url = URL("https://jsonplaceholder.typicode.com/todos")
val error_url = URL("https://error_url.txt")
FunctionalFetcherResult.fetch(ok_url)
.flatMap(::parseTasks) // HERE
.mapLeft {
println("Error $it")
}.mapRight {
println("Output $it")
}
}
You just replaced the first mapRight()
invocation with the flatMap()
you just define. Build and run.
If successful, you’ll now get an output like this:
Output [Task(userId=1, id=1, title=delectus aut autem, completed=false), Task(userId=1, id=2, title=quis ut nam facilis et officia qui, completed=false),
- - -
Task(userId=10, id=200, title=ipsam aperiam voluptates qui, completed=false)]
Now, if successful, the last mapRight()
receives the result of parseTasks()
, a function that can fail. In that case, you’d still get the JsonDecodingException
as the type of the implicit parameter for mapLeft()
.
Where to Go From Here?
Congratulations! You just wrote a bunch of code. You implemented a Result<E, T>
data type, and you handled the superpower of a bifunctor, an applicative and a monad. You also had the chance to use the semigroup typeclass in case of error management.
Your journey through the world of functional programming doesn’t end here, though. In the next tutorial, you’ll learn how Arrow can help generate most of the code you just wrote from scratch. You’ll have the chance to work on this same project and see how Arrow can help with the implementation of the FunctionalFetcher use case.
You can download a complete project using the Download Materials button at the top or bottom of this tutorial.
If you have any comments or questions, feel free to join in the forum below.