Functional Programming With Kotlin and Arrow — Algebraic Data Types
Learn how to use algebraic operations to better understand functional programming concepts like class constructs, typeclasses and lists in Kotlin & Arrow. 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 — Algebraic Data Types
35 mins
- Getting Started
- What Is Algebra?
- Data Types and Multiplication
- Multiplying the Unit Type
- Multiplying the Nothing Type
- Multiplying Classes
- Data Types and Addition
- Understanding the Unit and Nothing Types
- Putting Algebra to Work
- Using Algebra for Type Safety
- Other Algebraic Properties
- Using Algebra With the Optional Type
- More Fun With Exponents
- Understanding Currying
- Carrying and the Flip Function
- Using Algebra With the List Type
- Functional Lists and Algebra
- Where to Go From Here?
Data Types and Addition
The next question is about addition, which is another fundamental algebraic operation. Open Either.kt and copy the following code, which you might remember from the previous tutorials of this series:
sealed class Either<out A, out B>
class Left<A>(val left: A) : Either<A, Nothing>()
class Right<B>(val right: B) : Either<Nothing, B>()
This is the Either<E, A> data type, which represents a value of type A
or a value of type B
. For your next step, you’ll repeat the same exercise, trying to understand how many values the Either<E, A>
type has in relation to the number of values of A
and B
.
Start by adding the following definition to Either.kt:
typealias EitherBooleanOrBoolean = Either<Boolean, Boolean>
Then add the following code:
val either1 = Left(true)
val either2 = Left(false)
val either3 = Right(true)
val either4 = Right(false)
This is the list of all possible values of the EitherBooleanOrBoolean
type, which you can think of as:
Boolean + Boolean = 2 + 2 = 4
This is, perhaps, not the best example because, as you saw earlier, 4
is 2 + 2
but also 2 * 2
. However, you already learned how to solve this problem.
In this case, just add the following definition to Either.kt:
typealias EitherBooleanOrTriage = Either<Boolean, Triage>
Now, add the following values:
val eitherTriage1: Either<Boolean, Triage> = Left(true)
val eitherTriage2: Either<Boolean, Triage> = Left(false)
val eitherTriage3: Either<Boolean, Triage> = Right(Triage.RED)
val eitherTriage4: Either<Boolean, Triage> = Right(Triage.YELLOW)
val eitherTriage5: Either<Boolean, Triage> = Right(Triage.GREEN)
This proves that:
Boolean + Triage = 2 + 3 = 5
The Boolean
type has 2
values and the Triage
type has 3
values, so the EitherBooleanOrTriage
type has 2 + 3 = 5
values.
Understanding the Unit and Nothing Types
It’s now easy to see what the role of the Unit
and Nothing
types are in the case of Either<E, A>. You already know how to understand this. Enter the following code in Either.kt:
typealias EitherBooleanOrNothing = Either<Boolean, Nothing>
val boolNothing1: Either<Boolean, Nothing> = Left(true)
val boolNothing2: Either<Boolean, Nothing> = Left(false)
Now, it’s simple to understand that:
Boolean + Nothing = 2 + 0 = 2
The Nothing
type, as you saw earlier for multiplication, translates to 0.
And now for the Unit case, enter:
typealias EitherBooleanOrUnit = Either<Boolean, Unit>
val boolUnit1: Either<Boolean, Unit> = Left(true)
val boolUnit2: Either<Boolean, Unit> = Left(false)
val boolUnit3: Either<Boolean, Unit> = Right(Unit)
Which translates to:
Boolean + Unit = 2 + 1 = 3
Just as when you multiplied it earlier, the Unit
type counts as 1.
Putting Algebra to Work
After some simple calculations, you now understand that you can see a class as a way to represent values that are, in number, the product of multiplying the possible values of the aggregated types. You also learned that the Either<E, A> has as many values as the sum of the values of type A
and B
.
But how is this knowledge useful?
As a simple example, open TypeSafeCallback.kt and enter the following definition:
typealias Callback<Data, Result, Error> = (Data, Result?, Error?) -> Unit
This is the definition of a Callback<Data, Result, Error>
type. This could, for example, represent the operation you invoke to notify something of the result of an asynchronous task.
It’s important to note that you define the Result
and Error
types as optional.
With this type, you want to consider that:
- You always receive some data back from the asynchronous function.
- If the result is successful, you receive the content in a
Result
object, which isnull
otherwise. - If there are any errors, you receive a value of type
Error
, which is alsonull
otherwise.
You can simulate a typical use case of the previous type by enering the following code into TypeSafeCallback.kt:
// 1
class Response
class Info
class ErrorInfo
// 2
fun runAsync(callback: Callback<Response, Info, ErrorInfo>) {
// TODO
}
In this code you:
- Define some types to use as placeholders. You don’t really care about what’s inside those classes here.
- Create
runAsync
with a parameter ofCallback<Data, Result, Error>
.
An example of when to implement runAsync()
is when you’re performing an asynchronous operation and you invoke the callback function, then pass the corresponding parameter. For instance, in case of success, runAsync()
might result in the following, where you return some Response
and the Info
into it:
fun runAsync(callback: Callback<Response, Info, ErrorInfo>) {
// In case of success
callback(Response(), Info(), null)
}
If there’s an error, you could use the following code to return the Response
along with ErrorInfo
, which encapsulates information about the problem.
fun runAsync(callback: Callback<Response, Info, ErrorInfo>) {
// In case of error
callback(Response(), null, ErrorInfo())
}
But there’s a problem with this: The type you define using the Callback<Data, Result, Error>
typealias is not type-safe. It describes values that make no sense in runAsync()
‘s case. That type doesn’t prevent you from having code like the following:
fun runAsync(callback: Callback<Response, Info, ErrorInfo>) {
// 1
callback(Response(), null, null)
// 2
callback(Response(), Info(), ErrorInfo())
}
Here you you might:
- Have a
Response
without anyInfo
orErrorInfo
. - Return both
Info
andErrorInfo
.
This is because the return type allows those values. You need a way to implement type safety.
Using Algebra for Type Safety
Algebraic data types can help with type safety. You just need to translate the semantic of Callback<Data, Result, Error>
into an algebraic expression, then apply some simple mathematic rules.
What you’re expecting from the callback is:
A Result AND an Info OR a Result AND an ErrorInfo
You can represent the previous sentence as:
Result * Info + Result * ErrorInfo
Now, apply the associative property and get:
Result * (Info + ErrorInfo)
This is similar to what you saw in the previous paragraphs.
Next, translate this to the following and add it to TypeSafeCallback.kt:
typealias SafeCallback<Data, Result, Error> = (Pair<Data, Either<Error, Result>>) -> Unit
The safe version of runAsync
now looks like the following code, which you can also add to TypeSafeCallback.kt:
fun runAsyncSafe(callback: SafeCallback<Response, Info, ErrorInfo>) {
// 1
callback(Response() to Right(Info()))
// 2
callback(Response() to Left(ErrorInfo()))
}
The only values you can return using the safe callback are:
- A
Response
and anInfo
object, in case of success. - In case of error, the same
Response
but with anErrorInfo
.
More important than what you can do is what you cannot do. You cannot return both Info
and ErrorInfo
, but you must return at least one of them.