article banner

Collection processing in Kotlin: drop and take

This is a chapter from the book Functional Kotlin. You can find it on LeanPub or Amazon. It is also available as a course.

When you need to take or get rid of a certain number of elements, the take, takeLast, drop and dropLast functions are at your service:

  • take(n) - returns a collection with only the first n elements (or returns the unchanged collection if it has less than n elements).
  • takeLast(n) - returns a collection with only the last n elements (or returns the unchanged collection if it has less than n elements).
  • drop(n) - returns a collection without the first n elements.
  • dropLast(n) - returns a collection without the last n elements.
fun main() { val chars = ('a'..'z').toList() println(chars.take(10)) // [a, b, c, d, e, f, g, h, i, j] println(chars.takeLast(10)) // [q, r, s, t, u, v, w, x, y, z] println(chars.drop(10)) // [k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z] println(chars.dropLast(10)) // [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p] }

Kotlin by design doesn't have aliases for head (to take the first element) or tail (to drop the first element) methods, that are well known in other functional languages. Instead, we use first() and drop(1).

Most collection processing functions, including take and drop, are extension functions on the Iterable interface, but takeLast and dropLast are extension functions on List. Such design is needed for efficiency.

If we know the size of our collection, these methods can be used interchangeably:

  • l.take(n) gives the same result as l.dropLast(l.size - n),
  • l.takeLast(n) gives the same result as l.drop(l.size - n),
  • l.drop(n) gives the same result as l.takeLast(l.size - n),
  • l.dropLast(n) gives the same result as l.take(l.size - n),
fun main() { val c = ('a'..'z').toList() println(c.take(10) == c.dropLast(c.size - 10)) // true println(c.takeLast(10) == c.drop(c.size - 10)) // true println(c.drop(10) == c.takeLast(c.size - 10)) // true println(c.dropLast(10) == c.take(c.size - 10)) // true }

If we are operating on a List, all these methods can be replaced with the more universal subList, which expects as arguments the start index (inclusive) and the end index (exclusive), so:

  • l.take(n) gives the same result as l.subList(0, n),
  • l.takeLast(n) gives the same result as l.subList(l.size - n, l.size),
  • l.drop(n) gives the same result as l.subList(n, l.size),
  • l.dropLast(n) gives the same result as l.subList(0, l.size - n),
fun main() { val c = ('a'..'z').toList() val n = 10 val s = c.size println(c.take(n) == c.subList(0, n)) // true println(c.takeLast(n) == c.subList(s - n, s)) // true println(c.drop(n) == c.subList(n, s)) // true println(c.dropLast(n) == c.subList(0, s - n)) // true }

I find take, takeLast, drop and dropLast much more readable than subList, which requires unintuitive operations on indexes. They are also safer - when we ask to drop more elements than there are in the collection, the result is an empty collection, when we try to take more than there is the result is the collection with as many elements as possible, when we call subList with an incorrect value, it throws an exception.

fun main() { val letters = listOf("a", "b", "c") println(letters.take(100)) // [a, b, c] println(letters.takeLast(100)) // [a, b, c] println(letters.drop(100)) // [] println(letters.dropLast(100)) // [] letters.subList(0, 4) // throws IndexOutOfBoundsException }