Kotlin Lambda Functions Overview

Lambdas are bits of code that you can pass around to other functions, and because this can greatly simplify code verbosity, the Kotlin library uses lambdas heavily. Lambdas are most often used to iterate through collections to modify or filter certain elements in some way. I wanted to show some examples of how lambdas can make life easier and outline a brief overview of what lambdas with Kotlin looks like along with the most useful APIs Kotlin has to offer.

Searching through a collection

// define a list of 2 books, each book has the property of its name // and the number of pages
val books = listOf(Book("Harry Potter", 561), Book("Feynman", 230))
// regular way of searching through collection for the max pages
fun getLongest(books : List<Book>) {
var maxPages = 0;
var longestBook: Book? = null
for
(book in books) {
if (book.numberOfPages > maxPages) {
maxPages = book.numberOfPages
longestBook = book;
}
}
println(book)
}
getLongest(books)
// lambda way of searching through same collection   
// the 'it' is an autogenerated parameter name when we don't specify // the argument name
println(books.maxBy {it.numberOfPages})
// this line of code does the exact same thing except it's searching 
// using a member reference

println(books.maxBy(Book::numberOfPages)

filter() and map()

  • filter() and map() are the two of the most useful lambda functions. filter() returns the elements that satisfy the condition you specify, and map() applies a function to all elements in a collection
// the filter function iterates through a collection and returns the // elements to which the given function returns true
val nums = listOf(1, 2, 3, 4, 5)
println(num.filter {it > 3})
>> [4. 5]
// the map function applies the given function to each element in 
// the collection
println(num.map{it * 2})
>> [2, 4, 6, 8, 10]
// you can also chain together calls
println(num.filter{it > 3}.map{it * 2})
>> [8, 10]

all(), any(), count(), find()

// all() can be used to see if all elements in a collection satisfy // a condition
val books = listOf(Book("Harry Potter", 561), Book("East of Eden", 630), Book("Feynman", 230))
val isLongerThan300Pages = { b : Book -> p.numberOfPages > 300}
println(books.all(isLongerThan300Pages))
>> false
// any() returns true if just one element satisfies the condition
println(books.any(isLongerThan300Pages))
>> true
// count() returns the number of elements that satisfy the condition
println(books.count(isLongerThan300Pages))
>> 2
// find() returns the first element in the collection that satisfies 
// the condition
println(books.find(isLongerThan300Pages))
Book(title=Harry Potter, numberOfPages=561)

groupBy()

  • groupBy() can group a collection of elements together based on a object you specify.
val books = listOf(Book("Harry Potter", novel), Book("East of Eden", novel), Book("Feynman", biography));
// Let's get all the books that are considered novels
println(books.groupBy(it.type))

{novel=[Book(title=Harry Potter, type=novel),
Book(title=East of Eden, type=novel)],
biography=[Book(title=Feynman, type=biography)]}

flatMap()

val letters = listOf("a", "bc", "def", "g")
println(letters.flatMap {it.toList()})
[a, b, c, d, e, f, g]

sequences

  • In one of the examples above where we chained together two functions: map() and filter()
println(num.filter{it > 3}.map{it * 2})

When we do something like the above, we create temporary lists at each step. That means, that we will first filter the list numbers, put that in a new temporary list, and then perform the map function on that temporary list. This isn’t a big deal when we have just a few elements in our collection, but if we had thousands of elements in the collection, this could end up taking a hit on the performance with each iteration.

Sequences in Kotlin avoids the creation of these temporary lists by lazily evaluating the elements in the collection. When evaluations are done eagerly, every operation runs on every element in the collection. However, when we do this lazily, the elements are processed one by one.

println(num.asSequence()
.filter{it > 3}
.map{it * 2}
.toList())
// since we converted it to a sequence to start, we have to convert
// it back at the end

with() and apply()

The Builder pattern is something that is very common in Java, and the with() and apply() functions make Builders a bit easier to use

// when we don't use with() or apply() we end up repeating calls to // 'result' multiple times
fun formattedTitleString() : String {
val result = StringBuilder()
result.append("Large Title String")
result.append("\n")
result.append("Subtitle String")
return result.toString()
}
// using with(). This returns an expression
fun formattedTitleString() = with(StringBuilder()) {
append("Large Title String")
append("\n")
append("Subtitle String")
toString()
}
// using apply(). This returns the object 
fun formattedTitleString() = StringBuilder().apply {
append("Large Title String")
append("\n")
append("Subtitle String")
}.toString()

Most of this is what I learned from reading Kotlin in Action!