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.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 5 of this article. Click here to view the first page.

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 is null otherwise.
  • If there are any errors, you receive a value of type Error, which is also null 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:

  1. Define some types to use as placeholders. You don’t really care about what’s inside those classes here.
  2. Create runAsync with a parameter of Callback<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:

  1. Have a Response without any Info or ErrorInfo.
  2. Return both Info and ErrorInfo.

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:

  1. A Response and an Info object, in case of success.
  2. In case of error, the same Response but with an ErrorInfo.

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.