Functional Kotlin part 4: collections manipulation

This is a part 4 of the #kotlin-showoff series and it's going to be about the standard functions over the collections(mostly iterables to be precise) allowing developer to express data modification in the clean and functional way.

General convention

Although one might think that kotlin has inherited all the base collection types from the Java it's not quite true. Kotlin transparently maps existing Java collections into the Kotlin by using some tricks such as typealiasing. Collections hierarchy in Kotlin make code even more safer by imposing separation between mutable and immutable data structures. Take a look on the interfaces diagram:

diagram originally posted on the kotlinlang.org

Having dedicated interfaces for immutable collections makes expressions are purely functional - no need to worry if api consumer modifies list on the way or even worse, attempt to insert into the immutable collection(goodbye UnsupportedOperationException!). Indeed, immutability is enforced in compile time by contract.

A note about Iterable vs Sequence

Those are very similar types of the base entities even with the same signatures, let's take a look

public interface Sequence<out T> {
    public operator fun iterator(): Iterator<T>
}

public interface Iterable<out T> {
    public operator fun iterator(): Iterator<T>
}

Those two base classes define the way data will be processed in the chain of the calls:

  • Operations on Iterable produce result immideatelly, so the full intermideate result will be passed between calls in the chain. The result is evaluated eagerly after each step.
  • Operations on the Sequence treat data items comming thorough as it would be an infinite stream, the closest analogy would be java8 Stream or RxObservable. Items passed via the chain of the calls one by one. Result is evaluated lazily.

As for now we focus on the Iterable and it's descendants(Collection, List, Map, etc..) . Luckily, many operations exist for both interfaces with exactly the same signatures

Simple list transformations filter, map, forEach

Those are the probably the most widely used operators and they do exactly after their name. The provided function is applied to the each element of the operation

val adminNames = users
  .filter { it.isAdmin }
  .map { it.name }

pupils.forEach { 
  println("${it.name}: ${it.score}")
}

filter* and map* families

There are way more similar operations provided in the Kotlin stdlib giving extra flexibility when it need:

val userList = users
  .filterNotNul()
  .filterNot { it.isBanned }
  .mapTo(mutableHashSet()) { it.userId }
  .mapIndexed { (idx, userId) -> "#${idx}: {it.userId}" }

In many occasions you'll find the same pattern - verb [not] [indexed] [to]. No need to memorise - the names come out intuitively:

Operations returning single element: first, last, single, elementAt, get

first and last return first and last elements (obviously).

val firstUser = users.first()
val firstAdminUser = users.first { it.isAdmin }
val lastBannedUser = users.last { it.isBanned }

single returns one element and throws exception if more than 1 element in collection matches the predicate

val oneLove = listOf("java", "kotlin",  "javascript").singleOrNull { it == "kotlin" } 

Also those operations can have return alternative value - provided by closure or null:

val oneLove = languages.singleOrNull { it == "kotlin" }
val tenthWinnerName = user.getOrElse(10) { "NO WINNER" }
val secondPerson = user.getOrNull(2)

Aggregation operations count, average, min, max

Again, intuitively those operations perform aggregations:

val avgScore = pupils.average { it.score }
val topStudent = pupils.max { it.score }
val channagingStudent = pupils.min { it.score }

Conditional oprations all, none, count, any

val numberOfTopStudents = pupils.count { it.score > 4.5 }
val allPassed = pupils.all { it.score > 2.0 }
val hasNeedleInHaystack = heap.any { it.object == NEEDLE }
val allGood = results.none { it.error != null }

List to Map transformation associate*, groupBy

Both operations produce a Map and they are different on how keys are collided. While assciate* simply overwrites existing value with associated key, groupBy adds value to the list of values:

val usersById = users.associate { it.id to it } // result type: Map<UserId, User>
val usersById = users.associateBy { it.id } // same output
val pupilsByScore = pupils.groupBy { it.score } // result type Map<Int, List<Pupil>>

Many more

There a way more functional operations over collections are available in Kotlin stdlib such as fold, reduce, minus(-), plus(+), contains(in) etc:

// result - list of the both users
val allUsers = fbUsers + twitterUsers 

// result - elements of allUserIds which are not in bannedUsersIds
val activeUserIds = allUserIds - bannedUsersIds 

// result - the longest length of the name
val longestName = names.reduce { longest, item -> if (longest.length < item.length) item else longest }

// result - same as above, the longest length of the name
val longestLength = names.map(String::length).fold(0, ::max))

// result - if Wally was there
val isWallyLovesKotlin = "Wally" in kotlinLovers

Those extension functions are very intuitive and widely used, essentially can cover most of the everyday tasks.

Conclusion

Kotlin collection functions provide a lot of flexibility to express your ideas and business logic in very concise, clear and functional way

Hopefully you found this article useful for you, please check out other posts by #kotlin-showoff hashtag