Chapters

Hide chapters

Kotlin Apprentice

Second Edition · Android 10 · Kotlin 1.3 · IDEA

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Section III: Building Your Own Types

Section 3: 8 chapters
Show chapters Hide chapters

Section IV: Intermediate Topics

Section 4: 9 chapters
Show chapters Hide chapters

10. Lambdas
Written by Matt Galloway & Joe Howard

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

A previous chapter taught you about functions. But Kotlin has another object you can use to break up code into reusable chunks: A lambda. These have many uses, and become particularly useful when dealing with collections such as an array or map.

A lambda expression is simply a function with no name; you can assign it to a variable and pass it around like any other value. This chapter shows you how convenient and useful lambdas can be.

Lambdas basics

Lambdas are also known as anonymous functions, and derive their name from the lambda calculus of Alonzo Church, in which all functions are anonymous. Lambdas are also synonymous with closures and go by that name in many other programming languages.

Closures are so named because they have the ability to “close over” the variables and constants within the closure’s own scope. This simply means that a lambda can access, store and manipulate the value of any variable or constant from the surrounding context, acting as a nested function. Variables and constants used within the body of a lambda are said to have been captured by the lambda.

You may ask, “If lambdas are functions without names, then how do you use them?” To use a lambda, you first have to assign it to a variable or constant, including as an argument to another function.

Here’s a declaration of a variable that can hold a lambda:

var multiplyLambda: (Int, Int) -> Int

multiplyLambda takes two Int values and returns an Int. Notice that this is exactly the same as a variable declaration for a function. As was said, a lambda is simply a function without a name. The type of a lambda is a function type.

You assign a lambda to a variable like so:

multiplyLambda = { a: Int, b: Int -> Int
  a * b
}

This looks similar to a function declaration, but there are subtle differences. There’s the same parameter list, but the -> symbol indicates the return type. The body of the lambda begins after the return type. The lambda expression returns the value of the last expression in the body.

With your lambda variable defined, you can use it just as if it were a function, like so:

val lambdaResult = multiplyLambda(4, 2) // 8

As you’d expect, result equals 8. Again, though, there’s a subtle difference. A lambda does not allow the use of names for arguments; for instance, you can’t write multiplyLambda(a = 4, b = 2). Unlike functions, you can’t use the parameter names for labeling the arguments.

Shorthand syntax

Compared to functions, lambdas are designed to be lightweight. There are many ways to shorten their syntax. First, you can use Kotlin’s type inference to shorten the syntax by removing the type information:

multiplyLambda = { a, b ->
  a * b
}

it keyword

For a lambda that has only one parameter, you can shorten it even further using the it keyword:

var doubleLambda = { a: Int ->
  2 * a
}
doubleLambda = { 2 * it }
val square: (Int) -> Int = { it * it }

Lambdas as arguments

Consider the following code:

fun operateOnNumbers(a: Int, b: Int, operation: (Int, Int) -> Int): Int {
  val result = operation(a, b)
  println(result)
  return result
}
val addLambda = { a: Int, b: Int ->
  a + b
}
operateOnNumbers(4, 2, operation = addLambda) // 6
fun addFunction(a: Int, b:Int) = a + b

operateOnNumbers(4, 2, operation = ::addFunction) // 6
operateOnNumbers(4, 2, operation = { a: Int, b: Int ->
  a + b
})
operateOnNumbers(4, 2, { a, b ->
  a + b
})
operateOnNumbers(4, 2, operation = Int::plus)
operateOnNumbers(4, 2) { a, b ->
  a + b
}

Lambdas with no meaningful return value

Until now, all the lambdas you’ve seen have taken one or more parameters and have returned values. But just like functions, lambdas aren’t required to do these things. A lambda will always return the value of its last expression, so here is how you define a lambda that takes no parameters and returns only the Unit object:

var unitLambda: () -> Unit = {
  println("Kotlin Apprentice is awesome!")
}
unitLambda()
var nothingLambda: () -> Nothing = { throw NullPointerException() }

Capturing from the enclosing scope

Let’s return to an important characteristic of lambdas, as they act as closures: they can access the variables and constants from within their own scope.

var counter = 0
val incrementCounter = {
  counter += 1
}
incrementCounter()
incrementCounter()
incrementCounter()
incrementCounter()
incrementCounter()
fun countingLambda(): () -> Int {
  var counter = 0
  val incrementCounter: () -> Int = {
    counter += 1
    counter
  }
  return incrementCounter
}
val counter1 = countingLambda()
val counter2 = countingLambda()

println(counter1()) // > 1
println(counter2()) // > 1
println(counter1()) // > 2
println(counter1()) // > 3
println(counter2()) // > 2

Custom sorting with lambdas

Lambdas come in handy when you start looking deeper at collections. In Chapter 8, you used array’s sort method to sort an array. By specifying a lambda, you can customize how things are sorted.

val names = arrayOf("ZZZZZZ", "BB", "A", "CCCC", "EEEEE")
names.sorted() // A, BB, CCCC, EEEEE, ZZZZZZ
val namesByLength = names.sortedWith(compareBy {
  -it.length
})
println(namesByLength) // > [ZZZZZZ, EEEEE, CCCC, BB, A]

Iterating over collections with lambdas

In Kotlin, collections implement some very handy features often associated with functional programming. These features come in the shape of functions that you can apply to a collection to perform an operation on it.

val values = listOf(1, 2, 3, 4, 5, 6)
values.forEach {
  println("$it: ${it * it}")
}
// > 1: 1
// > 2: 4
// > 3: 9
// > 4: 16
// > 5: 25
// > 6: 36
var prices = listOf(1.5, 10.0, 4.99, 2.30, 8.19)

val largePrices = prices.filter {
  it > 5.0
}
public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T>
[10.0, 8.19]
val salePrices = prices.map {
  it * 0.9
}
[1.35, 9.0, 4.4910000000000005, 2.07, 7.3709999999999996]
val userInput = listOf("0", "11", "haha", "42")
val numbers = userInput.map {
  it.toIntOrNull()
}
println(numbers) // > [0, 11, null, 42]
val numbers2 = userInput.mapNotNull {
  it.toIntOrNull()
}
println(numbers2) // > [0, 11, 42]
var sum = prices.fold(0.0) { a, b ->
  a + b
}
println(sum) // > 26.980000000000004
sum = prices.reduce { a, b ->
  a + b
}
println(sum) // > 26.980000000000004
val stock = mapOf(1.5 to 5, 10.0 to 2, 4.99 to 20, 2.30 to 5, 8.19 to 30)
var stockSum = 0.0
stock.forEach {
  stockSum += it.key * it.value
}
println(stockSum) // > 384.5

Mini-exercises

  1. Create a constant list called nameList which contains some names as strings. Any names will do — make sure there’s more than three. Now use fold to create a string which is the concatenation of each name in the list.
  2. Using the same nameList list, first filter the list to contain only names which have more than four characters in them, and then create the same concatenation of names as in the above exercise. (Hint: you can chain these operations together.)
  3. Create a constant map called namesAndAges which contains some names as strings mapped to ages as integers. Now use filter to create a map containing only people under the age of 18.
  4. Using the same namesAndAges map, filter out the adults (those 18 or older) and then use map to convert to a list containing just the names (i.e., drop the ages).

Challenges

Check out the challenges below to test your knowledge of Kotlin lambdas.

Challenge 1: Repeating yourself

Your first challenge is to write a function that will run a given lambda a given number of times.

fun repeatTask(times: Int, task: () -> Unit)

Challenge 2: Lambda sums

In this challenge, you’re going to write a function that you can reuse to create different mathematical sums.

fun mathSum(length: Int, series: (Int) -> Int) -> Int

Challenge 3: Functional ratings

In this final challenge, you will have a list of app names with associated ratings they’ve been given. Note — these are all fictional apps!

val appRatings = mapOf(
  "Calendar Pro" to arrayOf(1, 5, 5, 4, 2, 1, 5, 4),
  "The Messenger" to arrayOf(5, 4, 2, 5, 4, 1, 1, 2),
  "Socialise" to arrayOf(2, 1, 2, 2, 1, 2, 4, 2)
)

Key points

  • Lambdas are functions without names. They can be assigned to variables and passed as arguments to functions.
  • Lambdas have shorthand syntax that makes them a lot easier to use than other functions.
  • A lambda can capture the variables and constants from its surrounding context.
  • A lambda can be used to direct how a collection is sorted.
  • There exists a handy set of functions on collections which can be used to iterate over the collection and transform the collection. Transforms include mapping each element to a new value, filtering out certain values, and folding or reducing the collection down to a single value.

Where to go from here?

Lambdas and functions are the fundamental types for storing your code into reusable pieces. Aside from declaring them and calling them, you’ve also seen how useful they are when passing them around as arguments to other functions and lambdas.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now