Functional Programming with Kotlin and Arrow – Generate Typeclasses With Arrow
In this Kotlin tutorial, you’ll take the functional programming concepts learned in previous tutorials and apply them with the use of the Arrow framework. 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 – Generate Typeclasses With Arrow
25 mins
- Getting Started
- Implementing the Result<E, A> Datatype With Arrow
- Seeing How Arrow Can Help?
- Generating Arrow Code
- Looking at the Generated Code
- Using the Generated Code
- Implementing a Bifunctor With Arrow
- Examining the Generated Code for Bifunctor
- Using an Alternative Option
- Implementing a Result Applicative With Arrow
- Validating With Applicative
- Where to Go From Here?
Using the Generated Code
Now you have everything you need for a better implementation of FunctionalFetcher
.
Create a file named FunctionalFetcherResult.kt in the same package of the FunctionalFetcher.kt file in the main module. Add 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))
}
}
}
In this code, you:
- Replace the previous return type
String
withResult<FetcherException, String>
. - Return the result encapsulated in a
Success<String>
, if successful. - Return the exception into an object of type
Error<FetcherException>
, if you encounter an error.
If you want to test the previous code, you need something different from the main()
method in the previous implementation. You need a different behavior if the result is a Success
than what you’d want if the result is an Error
. Specifically, you need a Bifunctor.
Implementing a Bifunctor With Arrow
In the Functional Programming with Kotlin and Arrow – More on Typeclasses tutorial you learned what a Bifunctor is. It is a way to apply different functions to Kind2
depending on its actual type. For the Result<E,T>
data type, it’s a way to apply a different function for Success<T>
versus an Error<E>
.
Implementing a Bifunctor with Arrow is relatively simple; you just need to create an implementation of the existing arrow.typeclasses.Bifunctor
interface which requires the following operation:
fun <A, B, C, D> Kind2<F, A, B>.bimap(fl: (A) -> C, fr: (B) -> D): Kind2<F, C, D>
The fl
is the function you’ll apply for an Error
and fr
for Success
.
To implement this, create a new file named ResultBifunctor.kt in the arrow/typeclasses module and add the following code:
// 1
@extension
// 2
interface ResultBifunctor : Bifunctor<ForResult> {
// 3
override fun <A, B, C, D> Kind2<ForResult, A, B>.bimap(fl: (A) -> C, fr: (B) -> D): Kind2<ForResult, C, D> {
val fixed = fix()
return when (fixed) {
// 4
is Error<A> -> Error(fl(fixed.e))
// 5
is Success<B> -> Success(fr(fixed.a))
}
}
companion object
}
In this code, you define an interface that:
- Uses the
@extension
to enable Arrow code generation for typeclasses. - Extends the
Bifunctor
interface for theForResult
type which is the one you use in theKind2
abstraction. - Provides implementation for the
bimap()
function. - Uses the
fixed
version of theResult
object checking if it’s anError
or aSuccess
. If the former, it appliesfl
and returns the result into anotherError
object. - Applies
fr
, if you have a success and returns the result into a newSuccess
object.
You can now run this command from the terminal:
./gradlew :arrow:typeclasses:build
…or use the same option in the Gradle tab. Either method will trigger the Arrow code generation for typeclasses.
Examining the Generated Code for Bifunctor
After the execution of the previous command, you’ll see a new file in the build/generated/source/kaptKotlin/main folder of the typeclasses module:
If you have a look at the content of the ResultBifunctor.kt file, you’ll see the code that Arrow generated. You can find the bimap()
function implementation with the following signature:
fun <A, B, C, D> Kind<Kind<ForResult, A>, B>.bimap(arg1: Function1<A, C>, arg2: Function1<B, D>):
Result<C, D>
You can also find utility functions like:
fun <A, B, C> Kind<Kind<ForResult, A>, B>.mapLeft(arg1: Function1<A, C>): Result<C, B>
…which allow you to apply a single function to the E
part of Result<E, T>
.
Note the functions with the following signatures:
fun <X> rightFunctor(): Functor<Kind<ForResult, X>>
fun <X> leftFunctor(): Functor<Conested<ForResult, X>>
These allow you to use the E
and T
parts as different Functors.
For a better understanding of this, go back to the FunctionalFetcherResult.kt file in the main module and add the following code after the FunctionalFetcherResult
implementation:
fun main() {
// 1
val ok_url = URL("https://jsonplaceholder.typicode.com/todos")
val error_url = URL("https://error_url.txt")
// 2
val errorFunction = { error: FetcherException -> println("Exception $error") }
// 3
val successFunction = { json: String -> println("Json $json") }
FunctionalFetcherResult
.fetch(ok_url)
.bimap(errorFunction,successFunction) // 4
}
In this code, you:
- Define the
ok_url
anderror_url
variable in order to test your code. - Create
errorFunction()
to use for Error. This prints the encapsulated exception. - Create
successFunction
, for success. This prints the result. - Invoke
bimap()
with the previous functions as parameters.
Note that bimap()
is the one Arrow generates for you for the Kind2
implementation of ForResult
You can now run the code and see the different behavior for an error versus success.
Using an Alternative Option
You can also use the generated code in a different, more complex way. Replace main()
in the previous block of code with the following:
fun main() {
// 1
val ok_url = URL("https://jsonplaceholder.typicode.com/todos")
val error_url = URL("https://error_url.txt")
// 2
val result = FunctionalFetcherResult.fetch(error_url)
// 3
when (result) {
is Success<String> -> manageSuccess(result)
is Error<FetcherException> -> manageError(result)
}
}
Here you simply:
- Define
ok_url
anderror_url
to test your code. - Invoke
fetch()
to returnResult<E, T>
. - Invoke
manageSuccess()
ormanageError()
depending on the resulting type.
To compile, you’ll need to add the following as well:
fun manageSuccess(result: Success<String>) {
// 1
val successFunction = { json: String -> println("Json $json") }
val rightFunctor = Result
.bifunctor() // 2
.rightFunctor<String>() // 3
.lift(successFunction) // 4
rightFunctor(result) // 5
}
fun manageError(result: Error<FetcherException>) {
// 6
val errorFunction = { error: FetcherException -> println("Exception $error") }
val leftFunctor = Result
.bifunctor()
.leftFunctor<FetcherException>()
.lift(errorFunction)
// 7
leftFunctor(result.conest())
}
This code defines the manageSuccess()
and manageError()
functions. Here you:
- Define the
successFunction
function which simply prints the content of the response. - Invoke
bifunctor()
onResult
, after code generation makes it available. This will get the reference to theBifunctor
implementation. - Invoke
rightFunctor<String>()
to get the reference to the Functor for the right part ofResult
, which isString
.
Note: A Functor is a high order function which maps a function from a type(A) -> B
to a function of typeF(A) -> F(B)
. In your case, F is the right part of theResult<E, T>
. - Pass the reference to
successFunction()
as a parameter oflift()
. If successful, you’ll get another function you can apply to the result. - Apply
rightFunctor()
to the result. - Do the same in
manageError()
but for the left part of theResult<E, T>
. - Apply
leftFunctor
to the conest version of the result. Note: You can get more information regarding this on the Arrow website. In general, a Conest> is a way to represent a function from a typeA
to a typeKind<F,A,C>
.
Run the new main()
implementation and verify that the behavior is the same for success or an error. You’ll find that the previous approach is better. But this last approach demonstrates a possible use of the Arrow generated code for Bifunctor
.