As we’ve seen, functions are a basic building block of Kotlin code. Throughout this book, we’ve written a lot of them, all using the fun keyword. Kotlin also gives us another way to write functions—lambdas!
To help us learn about function types, function references, and lambdas, we’ll need to pay a visit to Bert’s Snips & Clips.
Bert’s Snips & Clips
Bert’s Snips & Clips is the barber shop to go to for a clean haircut and a nice smooth shave. Although he prides himself on his low prices, sometimes he offers coupons that give the customer $5 off, to lower the price even further.
Once the customer has a fresh new haircut, he needs to calculate the total cost, so that he can charge the customer the right amount. Bert’s pretty good at math, but to make this fast and easy, he created a simple Kotlin function to calculate the total.
His function needed to account for tax and the five-dollars coupon. Here’s what he wrote.
// Tax is 9%, so we'll multiply by 109% to get the total with tax included. val taxMultiplier = 1.09 fun calculateTotalWithFiveDollarDiscount(initialPrice: Double): Double { val priceAfterDiscount = initialPrice - 5.0 val total = priceAfterDiscount * taxMultiplier return total }If the customer has a $20 haircut and presents a $5 off coupon, he simply runs the function like this.
fun main() { val total = calculateTotalWithFiveDollarDiscount(20.0) println("$%.2f".format(total)) }When he runs this code, it prints
$16.35.One day, one of Bert’s customers spent a lot of money on haircuts for the whole family, and she was disappointed that she only had a single coupon for five dollars off.
In order to reward his most loyal, high-paying customers, he decided to introduce a 10% off coupon. That way, the more money they spend, the more dollars would be discounted.
![]()
Alongside the original function, Bert created a second function, which accommodates the new percent-based coupon. To calculate the price after a 10% discount, he just multiplies the initial price by 90%.
fun calculateTotalWithFiveDollarDiscount(initialPrice: Double): Double { val priceAfterDiscount = initialPrice - 5.0 val total = priceAfterDiscount * taxMultiplier return total } fun calculateTotalWithTenPercentDiscount(initialPrice: Double): Double { val priceAfterDiscount = initialPrice * 0.9 val total = priceAfterDiscount * taxMultiplier return total }When he looked at these functions, he realized that they’re almost exactly the same, except for one small part—the part that calculates the discounted price.
If only he could find a way to _pass code as an argument~… then he could consolidate his two functions down to one function that would look something like this.
fun calculateTotal(initialPrice: Double, applyDiscount: ???): Double { val priceAfterDiscount = applyDiscount(initialPrice) val total = priceAfterDiscount * taxMultiplier return total }In other words, he just wants to tell
calculateTotal()to apply a discount in a different way each time he calls it. If he could pass a function as an argument tocalculateTotal(), that would solve his problem. But passing a function as an argument to another function? How could that be possible?With Kotlin’s function types, it’s easy!
Introduction to Function Types
Way back in Chapter 1, we saw how we could create variables that hold values. For example, to create a simple
Stringvariable that holds text, we can do this.Similarly, parameters are variables in a function where the calling code passes in their values when invoking the function.
So far, every time that we’ve assigned a variable or passed an argument to a function, we’ve been dealing with values of simple types like
String, andInt, or even more complex objects of our own types, likeCircle.In addition to assigning simple values like these, Kotlin also lets us assign functions. To demonstrate this, here’s a simple function to calculate a flat dollar discount, like a $5-off coupon.
fun discountFiveDollars(price: Double): Double = price - 5.0Naturally, we can call this function.
val discountedPrice = discountFiveDollars(20.0) // Result is 15.0In addition to calling this function, we can also assign it to a variable like this.
val applyDiscount = ::discountFiveDollarsNotice the difference between these two lines of code. In Listing 7.6, we’re assigning the result of a function call, but in Listing 7.7, we’re assigning the function itself.
In the code above,
::discountFiveDollarsis a function reference. We call it that because it refers to a function. By assigning this function to a variable, it’s kind of like givingdiscountFiveDollars()another name. Now we can callapplyDiscount()the same way that we calleddiscountFiveDollars(), and it does the same thing.1val discountedPrice = applyDiscount(20.0) // Result is 15.0Whether we call
discountFiveDollars()in Listing 7.6 above, or callapplyDiscount()here, the result is the same:15.0(that is, $15.00).As you might recall, every time that we declare a variable, that variable has a type, even if it’s not explicitly written out in the code.
val name = "Bert" // name's type is String val hasCoupon = true // hasCoupon's type is Boolean val price = 12.50 // price's type is DoubleSo, you might be wondering about the type of the
applyDiscountvariable.When a variable holds a function, its type is a combination of all of its parameter types and its return type. In the case of the
discountFiveDollars()function, these include a single parameter of typeDoubleand a return type ofDouble.We can assemble a function’s type by:
- Keeping its parentheses.
- Keeping its types.
- Converting the colon
:to an arrow->.Let’s do that with
discountFiveDollars().So, the type of
discountFiveDollars()is(Double) -> Double. Let’s write out the type ofapplyDiscountexplicitly.val applyDiscount: (Double) -> Double = ::discountFiveDollarsAll of the types that we’ve seen up until now, such as
String,Int, andDouble, have had no spaces or punctuation characters in them. However, function types like(Double) -> Doubleinclude more punctuation, and they’re a bit long. But, they’re easy to figure out! The parameter types go inside the parentheses, and the result type goes to the right of the arrow.For functions that have multiple parameters, separate the parameter types with a comma. For example, below is a function that has two parameters—a
Stringand aDouble—and it returns aString. When assigning this function to a variable, that variable’s type will be(String, Double) -> Stringas seen here.fun menuItemDescription(name: String, price: Double) = "A $name costs $price before discounts and tax." val describeMenuItem: (String, Double) -> String = ::menuItemDescriptionTwo functions of the same type
As you might recall, when we have two values of the same type, such as two
Stringvalues, then we can assign (and reassign) those two values to the same variable.var couponCode = "FIVE_BUCKS" couponCode = "TAKE_10"Similarly, if we have two functions of the same type—that is, functions that have the same parameter types and return type—then we can assign (and reassign) those two functions to the same variable.
To demonstrate this, let’s introduce a function to calculate the discount for a 10%-off coupon. Since it has the same parameter and result types as
discountFiveDollars(), we can assign either of these functions to the same variable.fun discountFiveDollars(price: Double): Double = price - 5.0 fun discountTenPercent(price: Double): Double = price * 0.9 var applyDiscount = ::discountFiveDollars applyDiscount = ::discountTenPercentNote that the parameter names do not have to match. Only the types have to match. The code below works just the same as the code above.
fun discountFiveDollars(initialPrice: Double): Double = initialPrice - 5.0 fun discountTenPercent(originalPrice: Double): Double = originalPrice * 0.9 var applyDiscount = ::discountFiveDollars applyDiscount = ::discountTenPercentFor functions that have multiple parameters, keep in mind that the parameters must match in the same order.
For example, here are two functions. Both return a
String. Both accept two parameters—oneStringand oneDouble.However, since the order of those parameters doesn’t match, the types of those functions don’t match, so we can’t assign them to the same variable, or else we’ll get an error.
fun menuItemDescription(name: String, price: Double) = "A $name costs $price before discounts and tax." fun sillyMenuItemDescription(price: Double, name: String) = "You want a $name? It's gonna run you $price, not counting coupons, tax, and more!" var describeMenuItem = ::menuItemDescription describeMenuItem = ::sillyMenuItemDescriptionErrorNow that we’ve seen how we can assign a function to a variable, let’s move on to a scenario that’s even more interesting—assigning a function to a parameter. In other words, we’ll pass a function as an argument to another function. This is exactly what Bert needed way back in Listing 7.4.
Passing Functions to Functions
Let’s update Bert’s
calculateTotal()function to include the correct function type. We’ll take the code from Listing 7.4 above and just give theapplyDiscountparameter a function type of(Double) -> Double.fun calculateTotal(initialPrice: Double, applyDiscount: (Double) -> Double): Double { val priceAfterDiscount = applyDiscount(initialPrice) val total = priceAfterDiscount * taxMultiplier return total }With this code in place, Bert can call
calculateTotal()with a function reference! Let’s define a few more functions that match the type(Double) -> Double, and then callcalculateTotal()with each of them.fun discountFiveDollars(price: Double): Double = price - 5.0 fun discountTenPercent(price: Double): Double = price * 0.9 fun noDiscount(price: Double): Double = price val withFiveDollarsOff = calculateTotal(20.0, ::discountFiveDollars) // $16.35 val withTenPercentOff = calculateTotal(20.0, ::discountTenPercent) // $19.62 val fullPrice = calculateTotal(20.0, ::noDiscount) // $21.80Great! Bert is now able to calculate the total for different types of coupons, without needing to create multiple versions of
calculateTotal()for each one!Returning Functions from Functions
Instead of typing in the name of the function each time he calls
calculateTotal(), Bert would like to just enter the coupon code from the bottom of the coupon that he receives from the customer.To do this, he just needs a function that accepts the coupon code and returns the right discount function. In other words, it’ll have one parameter that’s a
String, and its return type will be(Double) -> Double.A when expression makes this function easy!
fun discount(couponCode: String): (Double) -> Double = when (couponCode) { "FIVE_BUCKS" -> ::discountFiveDollars "TAKE_10" -> ::discountTenPercent else -> ::noDiscount }And, of course, we can update Listing 7.17 to use the new
discount()function.val withFiveDollarsOff = calculateTotal(20.0, discount("FIVE_BUCKS")) // $16.35 val withTenPercentOff = calculateTotal(20.0, discount("TAKE_10")) // $19.62 val fullPrice = calculateTotal(20.0, discount("NONE")) // $21.80Functions like
calculateTotal()anddiscount()above, which accept functions as arguments or return them as results, are called higher-order functions.So far, we’ve been able to achieve a lot with function references! They can be quite helpful when you’ve already written the function that you want to reference. But Kotlin also gives us another, more concise way to assign functions to variables and parameters—~lambdas!_
Introduction to Lambdas
As you might recall from Chapter 1, when we write out a value directly in the code, it’s called a literal. For example, in Kotlin, we can write literals for basic types such as
String,Int, andBoolean. The highlighted parts of the code below are values that are written as literals.val string: String = "This is a string" val integer: Int = 49 val boolean: Boolean = trueJust as we can write literals of
String,Int, andBoolean, we can also write a literal of a function!“Wait,” I can hear you say, “we’ve already been writing functions! How is this any different?”
Yes, we’ve been writing named functions with the
funkeyword, but we’ve never defined a function directly in an expression, such as on the right-hand side of an assignment, or directly inside a function call.Let’s take another look at the
discountFiveDollars()function from earlier in this chapter. We defined that function and then assigned it to a variable by using a function reference. Here’s what it looked like.fun discountFiveDollars(price: Double) = price - 5.0 val applyDiscount: (Double) -> Double = ::discountFiveDollarsInstead of defining the
discountFiveDollars()function with thefunkeyword, we can rewrite it as a function literal like this.val applyDiscount: (Double) -> Double = { price: Double -> price - 5.0 }The highlighted part of the code above is a function literal. In Kotlin, a function literal written like this is called a lambda.
Lambdas are functions, just like all the other functions we’ve written so far. They’re simply expressed differently.
To write a lambda:
Use an opening brace
{and a closing brace}.Write the parameters before the arrow
->and the body after the arrow.Once we’ve assigned a lambda to a variable, we can call it using the variable’s name. Listing 7.21 and Listing 7.22 accomplish the same thing, but the latter does it more concisely.
Once we’ve assigned a lambda to a variable, we can call it using the variable’s name. Listing 7.21 and Listing 7.22 accomplish the same thing, but the latter does it more concisely.
Traditional Functions vs Lambdas
Both traditional functions and lambdas have parameters and a body, and evaluate to some kind of result. However, unlike traditional functions, the lambda itself does not have a name. Sure, you can choose to assign it to a variable that has a name, but the lambda itself is nameless.
The lambda in Listing 7.22 indicates that the
priceparameter has a type ofDouble. Most of the time, however, Kotlin can use its type inference to figure it out. For example, we can rewrite that listing and omit the parameter’s type in the lambda.val applyDiscount: (Double) -> Double = { price -> price - 5.0 }Kotlin knows that
pricemust be aDouble, because that’s what the type ofapplyDiscountsays it must be. Similarly, the result type of the lambda has to match.So, lambdas are a concise way to create a function right in the middle of an expression. Our lambda above is pretty small already, but we can make it even more concise!
The Implicit
itparameterIn cases where there’s only a single parameter for a lambda, we can omit the parameter name and the arrow. When we do this, Kotlin will automatically make the name of the parameter
it. Let’s rewrite our lambda to take advantage of this.val applyDiscount: (Double) -> Double = { it - 5.0 }The code here is incredibly more concise than the original
discountFiveDollars()function in Listing 7.5.The implicit
itparameter is used often in Kotlin, especially when the lambda is small, like this one. In cases when the lambda is longer, as we’ll see in a moment, it can be a good idea to explicitly name the parameter. Similarly, when one lambda is nested inside another, an explicit parameter name is helpful to avoid confusion.In many cases, though, the implicit
itparameter can make the code easier to read.Assigning a lambda to a variable can be helpful, but things get even more interesting when we start using lambdas with higher-order functions!
Lambdas and Higher-Order Functions
Passing Lambdas as Arguments
As we learned above, higher-order functions are those that have a function as an input (i.e., parameter) or an output (i.e., the result). Here’s the code from Listing 7.16 and Listing 7.17 above, where we used function references to pass functions as arguments to the
calculateTotal()function.fun calculateTotal(initialPrice: Double, applyDiscount: (Double) -> Double): Double { val priceAfterDiscount = applyDiscount(initialPrice) val total = priceAfterDiscount * taxMultiplier return total } fun discountFiveDollars(price: Double): Double = price - 5.0 fun discountTenPercent(price: Double): Double = price * 0.9 fun noDiscount(price: Double): Double = price val withFiveDollarsOff = calculateTotal(20.0, ::discountFiveDollars) // $16.35 val withTenPercentOff = calculateTotal(20.0, ::discountTenPercent) // $19.62 val fullPrice = calculateTotal(20.0, ::noDiscount) // $21.80It’s easy to call
calculateTotal()with a lambda instead of a function reference. Let’s rewrite the last few lines of the code above to use lambdas. We’ll just take the body from each corresponding function and write it as a lambda instead.val withFiveDollarsOff = calculateTotal(20.0, { price -> price - 5.0 }) // $16.35 val withTenPercentOff = calculateTotal(20.0, { price -> price * 0.9 }) // $19.62 val fullPrice = calculateTotal(20.0, { price -> price }) // $21.80In cases where the function’s last parameter is a function type, we can move the lambda argument outside of the parentheses to the right, like this.
val withFiveDollarsOff = calculateTotal(20.0) { price -> price - 5.0 } // $16.35 val withTenPercentOff = calculateTotal(20.0) { price -> price * 0.9 } // $19.62 val fullPrice = calculateTotal(20.0) { price -> price } // $21.80Note that we’re still sending two arguments to
calculateTotal()here. The first is inside the parentheses, and the second is outside to the right.In Kotlin, writing a lambda outside of the parentheses like this is called trailing lambda syntax. Regardless of whether we put that last lambda argument inside the parentheses or outside, it works exactly the same. Kotlin developers usually prefer trailing lambdas, though.
Trailing lambda syntax is even more fun when the lambda is the only argument that we’re passing to the function, because then we can omit the parentheses completely!
For example, here’s a higher-order function with a single parameter, which has a function type.
fun printSubtotal(applyDiscount: (Double) -> Double) { val result = applyDiscount(20.0) val formatted = "$%.2f".format(result) println("A $20.00 haircut will cost you $formatted before tax.") }When calling
printSubtotal(), no parentheses are needed!printSubtotal { price -> price - 5.0 } printSubtotal { price -> price * 0.9 }Returning Lambdas as Function Results
In addition to using lambdas as arguments, we can also use them as function results. Here’s the code from Listing 7.18 above, where we returned function references.
fun discount(couponCode: String): (Double) -> Double = when (couponCode) { "FIVE_BUCKS" -> ::discountFiveDollars "TAKE_10" -> ::discountTenPercent else -> ::noDiscount }We can very easily replace these function references with lambdas, just as we did for the function arguments in Listing 7.27.
fun discount(couponCode: String): (Double) -> Double = when (couponCode) { "FIVE_BUCKS" -> { price -> price - 5.0 } "TAKE_10" -> { price -> price * 0.9 } else -> { price -> price } }Lambdas with Multiple Statements
So far, the lambdas that we’ve created have contained only one simple expression each. But sometimes we need a lambda that has multiple statements in it.
To do this, simply put each statement on a separate line, as we would do in any other function. Unlike in a regular function, though, we don’t use the
returnkeyword to return the result. Instead, the very last line of the lambda will be the result of the call.For example, we might want to print some pricing details inside our lambda that calculates the five-dollars-off coupon. Here’s how we can do that.
val withFiveDollarsOff = calculateTotal(20.0) { price -> val result = price - 5.0 println("Initial price: $price") println("Discounted price: $result") result }When we’ve got a lambda that spans multiple lines like this, it’s conventional to put the parameters and arrow on the same line as the opening brace, as seen above.
Here’s the same code, with some notes indicating each part.
Before we wrap up this chapter, we’ve got one more concept to cover—closures!
Closures
Bert’s barber shop is doing great now!
Let’s take a look at his code, including
calculateTotal(),discount(), and how he ends up calling them to get the total.fun calculateTotal(initialPrice: Double, applyDiscount: (Double) -> Double): Double { val priceAfterDiscount = applyDiscount(initialPrice) val total = priceAfterDiscount * taxMultiplier return total } fun discount(couponCode: String): (Double) -> Double = when (couponCode) { "FIVE_BUCKS" -> { price -> price - 5.0 } "TAKE_10" -> { price -> price * 0.9 } else -> { price -> price } } val initialPrice = 20.0 val couponDiscount = discount("FIVE_BUCKS") val total = calculateTotal(initialPrice, couponDiscount)Bert noticed that whenever he introduces a new coupon, he needs to write another lambda.
For example, if he were to add a new coupon for nine dollars off, and another one for fifteen percent off, he would need to write a few more lambdas.
fun discount(couponCode: String): (Double) -> Double = when (couponCode) { "FIVE_BUCKS" -> { price -> price - 5.0 } "NINE_BUCKS" -> { price -> price - 9.0 } "TAKE_10" -> { price -> price * 0.9 } "TAKE_15" -> { price -> price * 0.85 } else -> { price -> price } }That’s not too bad, but he decided he could make one last small improvement. There are really two main categories of coupons—dollar amount and percentages.
He wrote the two functions in Listing 7.35 below—
amountDiscount()andpercentageDiscount()—to match the two categories of coupons that he identified.It’s important to note that these two functions do not calculate the discount themselves. Instead, they create functions that calculate the discount. This is a little easier to see in
percentageDiscount(), where he’s using an explicitreturnkeyword rather than an expression body.fun amountDiscount(amountOff: Double): (Double) -> Double = { price -> price - amountOff } fun percentageDiscount(percentageOff: Double): (Double) -> Double { val multiplier = 1.0 - percentageOff return { price -> price * multiplier } }Another neat thing here is that these lambdas use variables that are defined outside of the lambda body. The first one uses the
amountOffparameter of the wrapping function, and the second uses themultipliervariable.When a lambda uses a variable that’s defined outside of its body like this, it’s sometimes referred to as a closure. In this code,
multiplieris a read-only variable declared withval. But if it were declared withvar, it would also be possible for the code in the lambda to change the value of that variable.fun discount(couponCode: String): (Double) -> Double = when (couponCode) { "FIVE_BUCKS" -> amountDiscount(5.0) "NINE_BUCKS" -> amountDiscount(9.0) "TAKE_10" -> percentageDiscount(0.10) "TAKE_15" -> percentageDiscount(0.15) else -> { price -> price } }Summary
Thanks to Kotlin’s lambdas and function references, Bert can now create coupons for a wide range of amounts and percentages, while shearing away duplicate functions!
Congratulations on completing this chapter! Lambdas can be a tough concept for many programmers who haven’t used them before. If you still feel a little unsure about them, it’s completely fine. We’ll use them a lot in upcoming chapters, and you’ll get more familiar with them as you go.
In this chapter, you learned about:
- Function types, like
(Int, Int) -> Int.- Function references, which let you assign existing functions to variables and parameters.
- Lambdas, which are literals for functions.
- Higher-order functions, which are functions that accept a function as an argument, or return one as a result.
- The implicit
itparameter, which can be used when a lambda has a single parameter.- Multiple-line lambdas, which can be useful when your lambda needs more than just a single expression.
- Closures, which allow us to read and change the value of variables declared outside of the lambda.
Throughout this chapter, we’ve seen some simple use cases for lambdas, but they really shine when used with collections. In the next chapter, we’ll introduce collections, and we’ll see how we can use lambdas to do all sorts of fun things with them!
Thanks to James Lorenzen for reviewing this chapter!
Note, however, that we cannot use named arguments when calling a function using the variable’s name. ↩︎