7.
Functional Data Structures
Written by Massimo Carli
In Chapter 6, “Immutability & Recursion”, you learned all about immutability and how to use it in Kotlin. You learned:
- How using immutable objects helps solve concurrency problems.
- The cost you have to pay in terms of code simplicity.
- How to implement recursive functions and use the
tailreckeyword to improve performance.
Immutability and recursion are fundamental skills you need to understand and implement immutable data structures and, in particular, persistence collections. In this chapter, you’ll learn:
- What an immutable data structure is.
- What it means for a data structure to be persistent.
- How to implement an immutable and persistent list as a classic example of immutable and persistent data structures.
- What pattern matching is and what you can actually achieve in Kotlin.
- What the main functions for a collection are and how to implement them in Kotlin.
As always, you’ll learn all this by solving some interesting exercises and challenges.
Immutable data structure
In the “Immutability and Kotlin collections” section in Chapter 6, “Immutability & Recursion”, you saw that the List<T> implementation you usually get in Kotlin isn’t an actual immutable collection, but something called a read-only collection. This means builder functions like listOf<T>() return objects you see through the List<T> interface, but they aren’t actually immutable. They still have mutators, but they just implement them by throwing exceptions when invoked.
As the name says, an immutable data structure is a data structure that can’t change after it’s created. However, you can still add or remove values in a sense. What you get from an immutable data structure after adding or removing an element is another data structure with the element added or removed. This might look like a waste of memory on the Java Virtual Machine with performance consequences because of the garbage collector. This isn’t always true.
Persistent singly linked list
The persistent singly linked list is a classic example of a functional data structure. You’ll find it often in functional programming tutorials because of its simplicity and because its implementation is a very good exercise of the typical functions a collection provides. Before diving into the code, it’s useful to have a visual representation that explains how to handle immutable data structures.
Imagine you have to build a singly linked list which is, of course, initially empty like in Figure 7.1:
You usually represent an empty list with the Nil value, but you could call it Zero, Empty or even Null. It’s important to note how the empty list Nil doesn’t depend on the type of values the list should contain. All the empty lists are the same.
You can then try to add an element, for instance, an Int, getting what’s shown in Figure 7.2:
This new representation of a list is very interesting because it’s somehow different from what you would’ve normally implemented using a classic object-oriented approach.
To prove this, add the following code in ObjectOrientedList.kt in the material for this chapter:
data class Node<T>( // 1
val value: T,
val next: Node<T>? = null
)
fun main() {
val emptyList: Node<*>? = null // 2
val singleValueList = Node(1) // 3
}
In this code, you define:
-
Node<T>as an immutable class with a property forvalueand one for the optionalnextelement in the list of typeNode<T>?. -
emptyListas a constant of typeNode<*>initialized withnull. -
singleValueListas a simpleNode<Int>.
This is different from what you have in Figure 7.2 because there’s no explicit relation between what you have in singleValueList and emptyList. Also, emptyList is just a null value that doesn’t give meaning to the empty list object.
Of course, you can make the relation with emptyList explicit, modifying the previous code like this:
fun main() {
val emptyList: Node<*>? = null
val singleValueList = Node(1, emptyList as Node<Int>) // HERE
}
Here, you pass emptyList as a second parameter of the Node<T> primary constructor, and this requires you to do an explicit cast with as. IntelliJ isn’t super happy, as you see in Figure 7.3:
The following change would fix this warning, but again, it would just be another way of creating a simple Node<T>, and you’d lose the relation with emptyList:
fun main() {
val singleValueList = Node(1, null)
}
To better understand how to implement the persistent singly linked list, look at Figure 7.4, illustrating the list you get by adding a second element:
Again, you might still have the references to the previous emptyList and singleValueList, but now you can find a pattern in how you build the list. In this case, the object-oriented code gives you a hint. Just add the following definition to the bottom of main:
val twoValuesList = Node(2, Node(1, null))
This is quite normal code, but it gives you an idea; it helps you see every list as a collection with the following characteristics:
- It can be empty.
- It can contain a value in the head with an optional list as the tail. These are represented as
valueandnextrespectively withinNode. - In the last case, the tail can be empty.
This leads you to the following definition of FList<T>. Write it in FList.kt:
sealed class FList<out T> // 1
object Nil : FList<Nothing>() // 2
internal data class FCons<T>(
val head: T,
val tail: FList<T> = Nil
) : FList<T>() // 3
With this code, you define:
- The sealed class
FList<T>, which allows you to define a limited set of implementations that Kotlin forces you to define in the same file or package. -
Nilas an object that represents the empty list. Because the empty list is the same for every typeT, you can represent it asFList<Nothing>. This works becauseNothingis a subtype of every other type and becauseFList<T>is covariant. You define the covariance ofFList<T>using the out keyword. If you need a reminder on covariance, take a peek at Chapter 4, “Expression Evaluation, Laziness & More About Functions”. -
FCons<T>as the second way to representFList<T>: aheadwith anotherFList<T>astail. Note howNilis the defaulttail.
Note: The name
Conscomes from the word “Constructor”. For this reason, one of the names forFList<T>isConsList<T>.
FList<T> builders
In Kotlin, you can create different collection implementations using some builder methods. For instance, you create a read-only list of Int with:
val readOnlyList = listOf(1,2,3)
You create a mutable map with:
val mutableMap = mutableMapOf(1 to "One", 2 to "Two")
What builder function would you create for FList<T>? Open Builders.kt and write the following code:
fun <T> fListOf(vararg items: T): FList<T> { // 1
val tail = items.sliceArray(1 until items.size) // 2
return if (items.isEmpty()) Nil else FCons(items[0], fListOf(*tail)) // 3
}
This code allows you to create FList<T> using the following syntax:
fun main() {
// ...
val flist = fListOf(1, 2, 3)
}
In the previous code:
- You define
fListOfas a builder function using avarargparameter for values of typeT. It’s important to note how the return type isFList<T>. - The type for the
varargparameter is actually anArray, so in this case,itemshas the typeArray<T>. You then usesliceArrayfor getting another array containing everything but the first element. If the initial array is empty or contains just one element,tailwill also be the emptyArray<T>. - If
itemsis empty, you returnNil. Otherwise, you returnFCons<T>whereheadis the first element, andtailis theFList<T>you get, invokingfListOfrecursively on the sliced array.
Note: Note the use of the spread operator
*, which allows you to use the values in an array as if they were a list of multiplevararginput parameters.
Safer FList<T> builders
Now, add the following code to main in the same FList.kt file:
fun main() {
// ...
val emptyList = fListOf() // ERROR
}
In this case, you have an error because you’re not providing the specific value for the type parameter T. An easy fix would be to provide what’s missing, like this:
fun main() {
// ...
val emptyList = fListOf<Int>()
}
But you know the empty list Nil is the same for every type, so the Int information should be obsolete. In this case, you have two different options:
- Use
Nildirectly. - Use
fListOf()as a parameter of another function, taking advantage of the type inference the Kotlin compiler provides.
Add this code to main as an example:
fun main() {
val emptyList = Nil // 1
val singleElementFList = FCons(2, fListOf()) // 2
}
In this code:
- You use
Nildirectly. - You use
fListOfwhen Kotlin is already expectingFList<Int>because of theFCons<T>you use forsingleElementFList.
In the second point, there’s a problem, though. singleElementFList’s type is FCons<T> and not FList<T>. How can you prevent the direct use of Nil and FCons<T>, forcing all the clients to use them through a reference of type FList<T>?
You already solved a similar problem in Exercise 6.1 in Chapter 6, “Immutability & Recursion”. Comment out all the code in Builders.kt, open FList.kt and replace the FList<T> definition with the following:
sealed class FList<out T> {
companion object { // 1
@JvmStatic
fun <T> of(vararg items: T): FList<T> { // 2
val tail = items.sliceArray(1 until items.size)
return if (items.isEmpty()) {
empty()
} else {
FCons(items[0], of(*tail))
}
}
@JvmStatic
fun <T> empty(): FList<T> = Nil // 3
}
}
internal object Nil : FList<Nothing>() // 4
internal data class FCons<T>(
val head: T,
val tail: FList<T> = Nil
) : FList<T>() // 5
In this code, you:
- Use a companion object to define
ofandempty. - Implement
ofas the replacement for the previousfListOf. This allows you to useFList.of()syntax. The body is very similar to thefListOfyou saw earlier. You replacedfListOfwithofandNilwith the invocation ofempty. - Define
emptyas a builder for the empty listNil. It’s important to see how the return type isFList<T>. This simplifies the use ofempty()in the following examples. - Create
Nilas aninternal object. - Define
FCons<T>as aninternal data class.
To try this code, open Main.kt and add:
fun main() {
val emptyList = FList.empty<Int>() // 1
val singleElementList = FList.of(1) // 2
val singleElementList2 = FCons(1, emptyList) // 3
val twoElementsList = FList.of(1, 2) // 4
}
In this code, you:
- Create
emptyListusingFList.empty<Int>(), which still needs a type to help the compiler with type inference. - Use
FList.ofto createsingleElementListwith one element. - Create another
FList<Int>with a single element usingFCons<T>passingemptyListas a second parameter. - Use
FList.ofwith twoIntvalues to properly create anFList<Int>with two elements.
Declaring Nil and FCons<T> as internal has the advantage of hiding the actual implementations in code in different modules and, as you’ll see very soon, this might cause some problems. To understand what, it’s very useful to introduce the concept of pattern matching.
Pattern matching
A simple exercise can help you understand what pattern matching is and how it can be helpful. Suppose you want to implement size as a function that returns the number of elements in a given FList<T>. Open Accessor.kt and write the following code:
// DOESN'T COMPILE IN ANOTHER MODULE
fun <T> FList<T>.size(): Int = when (this) { // 1
is Nil -> 0 // 2
is FCons<T> -> 1 + tail.size() // 3
}
Because Nil and FCons<T> are internal, the previous code wouldn’t compile if implemented in a different module. However, you should note a few interesting things. Here, you:
- Define the
sizeextension function, which should return the number of elements inFList<T>. The result value is the evaluation of awhenexpression onthis. - Return
0if the currentFList<T>isNil, which is the emptyFList<T>. - If the current
FList<T>isn’tNil, it means it has aheadandtail.sizeis then thesizeof thetail+1.
As said, this code wouldn’t compile if written in a different module because Nil and FCons<T> are internal classes. This doesn’t allow the use of the is keyword to test if a reference of type FList<T> is actually Nil or FCons<T>. In the latter case, you’d also need a way to get the reference to head and tail. You need something very similar to what, in languages like Swift or Scala, is called pattern matching. Something that would make this pseudo-code compile is:
when(list){
Nil -> {}
(head, tail) -> {}
}
Unfortunately, that syntax doesn’t work yet with Kotlin, and it probably never will.
Note: Kotlin provides very limited pattern matching. For instance, if you release the constraint to have
NilandFCons<T>internal, you can make the previous code forsizecompile and, forFCons<T>, thetailproperty would be available as a consequence of the smart casting.
However, you can still do something to achieve a similar result. Open FList.kt and add the following code:
fun <T, S> FList<T>.match( // 1
whenNil: () -> S, // 2
whenCons: (head: T, tail: FList<T>) -> S // 3
) = when (this) {
is Nil -> whenNil() // 4
is FCons<T> -> whenCons(head, tail) // 5
}
In this code, you:
-
Define the
matchhigher-order function as an extension ofFList<T>. This function has two type parameters:TandS.Tis the type forFList<T>.Sis the type of result of the expression you want to evaluate ifFlist<T>isNilorFCons<T>. -
Declare the first parameter
whenNilas the lambda you want to evaluate if theFList<T>receiver isNil. The lambdawhenNilevaluates in a value of typeS. -
Define the second parameter,
whenCons, as the lambda you want to evaluate if theFList<T>receiver isFCons<T>. Again, the lambdawhenConsevaluates to a value of typeS. Here, it’s important to note howwhenConsacceptsheadandtailas input parameters. -
Check if the receiver
FList<T>isNil, retuning the evaluation ofwhenNil. -
Use the smart casting Kotlin provides to extract
headandtailif the receiver value isFCons<T>and use them as input parameters forwhenCons.
Because you define match in FList.kt, is Nil and is FCons<T> are available. Now, return to Accessor.kt, and replace the previous implementation of size with the following:
fun <T> FList<T>.size(): Int = match(
whenNil = { 0 }, // 1
whenCons = { head, tail -> 1 + tail.size() } // 2
)
Here, you implement size, returning the result of the match function evaluating:
-
{ 0 }ifFList<T>isNil. -
{1 + tail.size()}if the receiver isFCons<T>.
To test the size function, just add the following code to the same file and run:
fun main() {
println(FList.empty<Int>().size())
println(FList.of(1).size())
println(FList.of(1, 2, 3).size())
}
You’ll get the following output:
0
1
3
Exercise 7.1: Implement the extension function
isEmpty(), which returnstrueifFList<T>is empty andfalseotherwise.Try to answer these questions without the support of IntelliJ and check your solutions in Appendix G or the challenge project.
Note: The
matchfunction allows you to make the selection of the different states more explicit.FList<T>can beNilorFCons<T>. You’ll use it many times in the rest of the chapter, but you could do the same directly usingNilandFCons<T>and leveraging Kotlin’s smart cast. Remember, you can useNilandFCons<T>only in this module because of theirinternalvisibility.
Other FList<T> accessors
You can use the match function you created earlier in the implementation of most of the functions you’ll see in the following paragraphs. Another simple function is the one returning Flist<T>’s head. Open Accessor.kt and add the following code:
fun <T> FList<T>.head(): T? = match(
whenNil = { null }, // 1
whenCons = { head, _ -> head } // 2
)
In this case, you use match, returning:
-
nullif the receiver isNil. -
headif the receiver isFCons<T>.
Again, you can easily test this by adding the following code to main in the same file:
fun main() {
// ...
println(FList.empty<Int>().head())
println(FList.of(1).head())
println(FList.of(1, 2, 3).head())
}
Run it, and check that you get the following output:
null
1
1
Exercise 7.2: Implement the extension function
tail(), which returns thetailof a givenFList<T>.
Iteration
Iterating over a collection is one of the most important features a data structure provides. How would you allow clients to iterate over the elements in FList<T>? The List<T> interface provides the forEach higher-order function. Open Iteration.kt and add the following code:
fun main() {
listOf(1, 2, 3).forEach {
println(it)
}
}
Of course, running this code, you’ll get:
1
2
3
To implement the same forEach for your FList<T>, add the following code to the same file:
fun <T> FList<T>.forEach(fn: (T) -> Unit): Unit = match( // 1
whenNil = {}, // 2
whenCons = { head, tail -> // 3
fn(head)
tail.forEach(fn)
}
)
Here, you define forEach:
- With the lambda function
fnas an input parameter. The lambdafnreceives the current element of typeTas input. - If the receiver is
Nil, you do nothing. - If the receiver isn’t
Nil, you invokefn(head)and then recursively invokeforEachon thetail.
Run the following code:
fun main() {
// ...
FList.of(1, 2, 3).forEach {
println(it)
}
}
And you’ll get:
1
2
3
Exercise 7.3: Kotlin provides
forEachIndexedfor theIterable<T>interface, which accepts as input a lambda of type(Int, T) -> Unit. The firstIntparameter is the index of the itemTin the collection. To testforEachIndexed, run the code:listOf("a", "b", "c").forEachIndexed { index, item -> println("$index $item") }Getting the following output:
0 a 1 b 2 cCan you implement the same for
FList<T>?
Exercise 7.4: Another option to implement
forEachIndexedis to makeFList<T>anIterable<T>. How would you do that? To make all the code coexist in the same codebase, call theIterable<T>versionIFList<T>withINilandICons<T>.
Mutators
You just implemented some interesting functions to access elements in FList<T> or iterate over them. Now, it’s time to do something even more interesting that will allow you to actually add or remove elements and update the immutable singly linked list.
Inserting
In this chapter’s introduction, you saw, with some illustrations, how to add elements at the head of FList<T>. Later, in Exercise 7.5, you’ll implement addHead. Implementing append to add an element at the end of FList<T> is a little more challenging because it implies copying the initial list to a new one. Open Mutator.kt and add the following code:
fun <T> FList<T>.append(newItem: T): FList<T> = match( // 1
whenNil = { FList.of(newItem) }, // 2
whenCons = { head, tail ->
FCons(head, tail.append(newItem)) // 3
}
)
In this code, you:
- Define
appendas an extension function ofFList<T>with a single input parameternewItemof typeT. You still usematch. - Create a new
FList<T>if the current value isNil, with the value to append as the only element. This will be the tail of the newFList<T>you’re building. - Create a new
FCons<T>when the current reference isFCons<T>, usingheadas the initial value and the list you get by appendingnewItemtotail.
To test the previous code, run:
fun main() {
val initialList = FList.of(1, 2)
val addedList = initialList.append(3)
initialList.forEach {
print("$it ")
}
println()
addedList.forEach {
print("$it ")
}
}
You’ll get:
1 2
1 2 3
To help visualize what’s happening, think of it like this:
(1, (2, ())).append(3) // 1
(1, (2, ()).append(3)) // 2
(1, (2, ().append(3))) // 3
(1, (2, (3, ()))) // 4
Here:
- You start invoking
append(3)on anFList<Int>of2elements. Note how the last tail isNil, represented by()above. - The first element is still
1, and thetailis the one you get, invokingappend(3)on the previoustail. - Again, you invoke
append(3)on thetail, which isNil. This creates anFList<Int>with the only element3. - The result is a new
FList<Int>of3elements.
Exercise 7.5: Implement
addHead, which adds a new element at the head of an existingFList<T>.
Filtering
In the previous chapters, you met the filter function that lets you select elements using some predicate. How would you implement the filter function for FList<T>? In Filter.kt, add the following code:
typealias Predicate<T> = (T) -> Boolean // 1
fun <T> FList<T>.filter(predicate: Predicate<T>): FList<T> = match(
whenNil = { FList.empty() }, // 2
whenCons = { head, tail ->
if (predicate(head)) {
FCons(head, tail.filter(predicate)) // 3
} else {
tail.filter(predicate) // 4
}
}
)
Here, you:
- Define
Predicate<T>, which you met in previous chapters. - Implement
filterusing thematchfunction. When the receiver isNil, you return the empty list usingFList.empty(). You could also returnNildirectly here. - Evaluate the predicate on
headwhen the receiver isn’t empty. If it evaluates totrue, you returnFList<T>using the sameheadand what you get invokingfilteron thetail. - Return what you get invoking
filteron thetailif the predicate doesn’t evaluate totrueon thehead.
To test the previous code, run:
fun main() {
FList.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
.filter { it % 3 == 0 }
.forEach { println(it) }
}
This filters the values that are multiples of 3 in FList<Int>. In this case, the output is:
3
6
9
Exercise 7.6: Kotlin defines the
takefunction onIterable<T>that allows you to keep a given number of elements. For instance, running the following code:fun main() { listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) .take(3) .forEach { print("$it ") }You’d get:
1 2 3Can you implement the same
takefunction forFList<T>?
Exercise 7.7: Kotlin defines the
takeLastfunction onIterable<T>that allows you to keep a given number of elements at the end of the collection. For instance, running the following code:fun main() { listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) .takeLast(3) .forEach { print("$it ") }You’d get:
8 9 10Can you implement the same
takeLastfunction forFList<T>?
Why FList<T> is a persistent data structure
So far, you’ve met the descriptors immutable, functional and persistent for data structures, and it’s important to quickly emphasize what they are:
- Immutable data structure: This data structure can’t change after it’s been created. This means you can’t replace a value in a specific position with another or remove another element. When performing this kind of operation, you need to get another immutable data structure, as you’ve seen for other immutable objects in Chapter 6, “Immutability & Recursion”.
-
Functional data structure: This is a data structure you can interact with using only pure functions. For instance, you get a new
FList<T>filtering the data of another one using a predicate you represent using a pure function. As you learned in Chapter 3, “Functional Programming Concepts”, a pure function doesn’t have any side effects and is represented using a referentially transparent expression. In the following chapters, you’ll see many other functions likemap,flatMapand others. -
Persistent data structure: This data structure always preserves the previous version of itself when it’s modified. They can be considered immutable, as updates aren’t in place. The
FList<T>you implemented in this chapter is persistent. You see this when you add a new value. The existing object is still there, and it just becomes thetailof the new one.
Challenges
In this chapter, you had a lot of fun implementing some of the classic functions you find in collections for the singly linked list FList<T>. You also had the chance to use the recursion skills you learned in Chapter 6, “Immutability & Recursion”. Why not implement some more functions?
Challenge 7.1: First and last
Kotlin provides the functions first and last as extension functions of List<T>, providing, if available, the first and last elements. Can you implement the same for FList<T>?
Challenge 7.2: First and last with predicate
Kotlin provides an overload of first for Iterable<T> that provides the first element that evaluates a given Predicate<T> as true. It also provides an overload of last for List<T> that provides the last element that evaluates a given Predicate<T> as true. Can you implement firstWhen and lastWhen for FList<T> with the same behavior?
Challenge 7.3: Get at index
Implement the function get that returns the element at a given position i in FList<T>. For instance, with this code:
fun main() {
println(FList.of(1,2,3,4,5).get(2))
}
You’d get:
3
Because 3 is the element at index 2. Consider 0 the index of the first element in FList<T>.
Key points
- An immutable data structure is a data structure that can’t change after it’s been created.
- A functional data structure is a data structure you can interact with using only pure functions.
- A persistent data structure is a data structure that always preserves the previous version of itself when it’s modified.
- Kotlin doesn’t have pattern matching, but you can achieve something similar using the smart cast feature.
-
FList<T>is the implementation of a singly linked list and is a very common example of a functional, immutable and persistent data structure.
Where to go from here?
Congratulations! In this chapter, you had a lot of fun implementing the FList<T> functional data structure. You had the chance to apply what you learned in Chapter 6, “Immutability & Recursion”, for implementation of the most common higher-order functions like filter, forEach, take and many others. It’s crucial to say that these are just the first, and many others will come in the following chapters. In Chapter 9, “Data Types”, you’ll get to add more functions for FList<T>. For now, it’s time to dive deep into the concept of composition. See you there!