Functional Programming with Kotlin and Arrow Part 2: Categories and Functors
In this functional programming tutorial, you’ll learn what category theory is, see how to apply it to programming, and learn how to make use of Functors with 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 Part 2: Categories and Functors
25 mins
- Getting Started
- What Is a Category?
- Composition
- The Category of Types and Functions
- The Associativity of Function Composition
- Morphisms and Function Types
- Mapping Between Categories: Functors
- The Maybe Functor
- The List Functor
- Arrow Typeclasses
- Type Constructors and Higher Kinds
- Arrow Data Types
- Functors in Arrow
- Where to Go From Here?
The Associativity of Function Composition
To complete the proof that types and functions are a category, you need to prove that function composition has associativity, which is the following:
h compose (g compose f) = (h compose g) compose f
given that these functions have the following types:
f: (A) -> B
g: (B) -> C
h: (C) -> D
To prove this, keep the previous code and apply the referential transparency you’ve learned about in the previous tutorial to the left and right sides of the equation above. From the left side, you get this:
h compose (g compose f) = h compose ({ a: A -> g(f(a)) }) = { a: A -> h(g(f(a))) }
On the right you get:
(h compose g) compose f = ({ b: B -> h(g(b)) }) compose f = { a: A -> h(g(f(a))) }
These two sides are then identical. You just proved that types and functions create a category!
Morphisms and Function Types
In the category of types and functions, each object is a type, and each morphism is a function. Given two types, you can have infinite morphisms. The set of all morphisms between two types, which can even be equal in case of identity, has a name: hom-set. A hom-set is a representation of the concept of a function type.
If types and functions create a category, anything you discover to be true in category theory will also be true for types and functions. Identity, composition and associativity are just the tip of the iceberg, and in the next sections, your efforts to prove that types and functions create a category will pay off.
Mapping Between Categories: Functors
Suppose you have a category C which contains two objects, a and b, and a morphism, f, between them.
You want to define a mapping, which you can call F between a and b and another pair of objects in a different category D, which you’ll call a’ and b’.
You also want F to map morphisms, so it will map f into a morphism f’ in the D category.
Take a look at all of this in this next image, visually:
a' = F a
b' = F b
f' = F f
For F to be a correct mapping, then if f is a morphism from a to b, f : (a) -> b
, then the same must be true for their mapped counterparts, so Ff must be a morphism from Fa to Fb:
F f : (F a) -> F b
The F mapping that you’ve defined here is a functor and, thanks to the property above, it maintains the structure of a category. But what is a functor in practice?
The Maybe Functor
Suppose you have the function stringToInt
that converts a String
into the Int
that it contains. If this is not possible, the function throws an exception.
A function like this is simple and you can find it in the StringToInt.kt file in the functions module:
val stringToInt: (String) -> Int = Integer::parseInt
You need to explicitly type the function to remove the ambiguity with another overload of the parseInt
method of the Int
class.
Now, suppose you want to convert an Int
into the related roman numeral.
To do so, you can use the intToRoman
function in the IntToRoman.kt file in the same functions module. It uses the toRoman
function in the Roman.kt file. The type of this function is (Int?) -> String
.
val intToRoman: (Int?) -> String = Int?::toRoman
It would be nice to compose these two functions in a declarative way. A possible way of doing this is by implementing a functor called Maybe<T>
.
Navigate to the custom-maybe module, create a CustomMaybe.kt file in the rw
package and copy this code into the file:
sealed class Maybe<out T>
class Some<T>(val value: T): Maybe<T>()
object None: Maybe<Nothing>()
This is a sealed class with a generic type T
, which has two possible realizations. The type Some<T>
represents the case when you can encapsulate a value of type T
. The second is the case when the result doesn’t exist.
If you think about about the previous stringToInt
function, this is exactly the type the function should return. Depending on whether the input string can be converted to a valid `Int` value, you might have a result or not.
As a first option, you could rewrite the same function like this in a new file called StringToIntMaybe.kt in the rw
package, still in the custom-maybe module:
val stringToIntMaybe: (String) -> Maybe<Int> =
{ s ->
try {
Some(Integer.parseInt(s))
} catch (e: NumberFormatException) {
None
}
}
If the input contains something that can be converted to an Int
, the function will return a Some
with the value. Otherwise it will return a None
.
Chaining Operations on Maybe
But what’s the advantage of this?
Remember that your superpowers are abstraction and composition.
What you tried to do here is apply a function to a value of a generic type T
and get a value as a result or nothing. Then you want to apply other functions like the one which converts the Int
into roman numerals.
To do that, define the map
function like this in the CustomMaybe.kt file in the custom-maybe module:
fun <T, S> Maybe<T>.map(fn: (T) -> S): Maybe<S> = when (this) {
is Some<T> -> Some<S>(fn(this.value))
else -> None
}
This is an extension function on the type Maybe<T>
that has a parameter with a function type, which maps values in T
into values in S
. The return type is Maybe<S>
and it’ll be a Some<S>
in case the function returns a value, or None
if it doesn’t. If the receiver of the function is None
, the result will be None
.
If you want to compose the function for converting a String
into Int
and then get its roman numeral representation, it will be quite easy.
Write and run this code in a file called Main.kt, which you can create in the custom-maybe module:
fun main() {
stringToIntMaybe("Hello")
.map(intToRoman)
.map { print(it) }
}
Run this main function.
You’ll get nothing in output because the input is a String
that cannot be converted into Int
, and so the conversion to a roman numeral won’t happen.
Replace the previous code with this:
fun main() {
stringToIntMaybe("123")
.map(intToRoman)
.map { print(it) }
}
If you run this code, you’ll get the following output, which is the roman numeral representation of the number 123.
CXXIII
What you’ve just built in Maybe<T>
is a functor. It’s a safe way to control side effects.
You’re Maybe<T> is similar to the nullable types that Kotlin gives you for free using the ?
symbol with a type.