1. Getting to know lambda functions
If you’re still not very confident with programming, lambdas may sound like “some kind of functional-programmer magic”. In reality, the idea is very down-to-earth: sometimes we want to treat an action the same way we treat a number or a string. You can put a number into a variable, pass it to a function, return it from a function. With a lambda, we do the same thing — except we’re not storing data, but a small piece of logic.
Imagine that in our console expense tracker, in one place we want to consider an amount from 1000 as a “big expense”, and in another place — from 500. If the rule is hardcoded in an if, you’ll have to copy if (amount >= 1000) all over the project and then heroically search for places you forgot to update. A lambda lets you write the “rule” once, assign it to a variable, and use it wherever you need it.
2. A lambda as a value: store it in val and call it
The most important thing to get hands-on with: a lambda is not a “special operator”, but a value. That means you can actually store it in a variable. And since it’s a value, you can later “call” it like a normal function.
Let’s start with the simplest possible example: checking whether a number is even.
fun main() {
val isEven: (Int) -> Boolean = { x -> x % 2 == 0 }
println(isEven(4)) // true
println(isEven(5)) // false
}
Several useful things are happening here at once.
First, to the right of = we created a lambda { x -> x % 2 == 0 }.
Second, on the left we explicitly specified the variable’s type: (Int) -> Boolean. Don’t dive too deep yet — treat it as a “function type”. It literally says: “takes Int, returns Boolean”.
Third, below we call the lambda the same way as a function: isEven(4). In other words, the variable isEven behaves like a function.
To reinforce the idea “a lambda is a value”, here’s another example where a lambda returns a string:
fun main() {
val greet: (String) -> String = { name -> "Hello, $name!" }
println(greet("Kotlin")) // Hello, Kotlin!
}
Same idea: we store the “greeting formula” in a variable and can call it as many times as we want.
Create and call are different actions
Even in this first lecture it’s useful to build one habit: distinguish between the place where a lambda is created and the place where it is called.
Compare:
fun main() {
val inc: (Int) -> Int = { it + 1 } // create and store
val a = inc(10) // call
println(a) // 11
}
If you wrote { it + 1 } but never wrote inc(10), nothing will happen. A lambda doesn’t “run by itself”; it’s not a background daemon and not a timer.
3. Lambda syntax and the it parameter
Basic form: { parameters -> body }
It’s easy to get confused with lambdas because there are braces, an arrow, and the feeling that you accidentally opened a portal to another dimension. Let’s tame this as ordinary language grammar: “here are the parameters, here is the body, here is what gets returned”.
The basic lambda form in Kotlin looks like this: always braces, inside them parameters, then the arrow ->, then the body. Parameters (with optional types) go before ->, the body goes after, and the return value is taken from the last expression.
Here’s a mini table for “how to read it”:
| Fragment | What it means | Example |
|---|---|---|
|
“This is a lambda” (always in braces) | |
| x to the left of -> | lambda parameter | |
|
“from parameters to body” | |
| expressions on the right | lambda body | |
And here is an example that’s a bit longer, but still simple:
fun main() {
val addVat: (Int) -> Int = { price ->
price + (price * 20 / 100)
}
println(addVat(100)) // 120
println(addVat(250)) // 300
}
Here the parameter is named price, the body is the expression price + ..., and the lambda’s result will be the value of the last expression.
Notice an important reading habit: when you see { ... -> ... }, mentally replace it with “a small unnamed function”. Not “a magic block”, not “special list syntax”, but a function.
One parameter — you can use it
When a lambda has exactly one parameter, Kotlin often lets you omit declaring it explicitly. Then that parameter is automatically available under the name it. If the signature is clear from context and there is only one parameter, you can omit the name, and the compiler will provide it.
Example: checking “is the number positive?”
fun main() {
val isPositive: (Int) -> Boolean = { it > 0 }
println(isPositive(10)) // true
println(isPositive(-1)) // false
}
Here it is that single Int that comes into the lambda.
It’s important not to turn it into a religion. it is great when the condition is short and obvious. If the logic is more complex, it’s better to name the parameter meaningfully; otherwise you’ll read the code like a riddle: “and what is it… a number? a string? a cat?”.
Compare two variants:
fun main() {
val ok1: (String) -> Boolean = { it.trim().isNotEmpty() }
val ok2: (String) -> Boolean = { text -> text.trim().isNotEmpty() }
println(ok1(" ")) // false
println(ok2("hi")) // true
}
Both variants are correct. The second is sometimes calmer to read, especially if you later add a couple more lines to the lambda body.
4. Return value and multi-line lambdas
What a lambda returns: the result of the last expression
With lambdas in Kotlin there is a very convenient convention: if you don’t explicitly write return@..., then the value of the lambda is considered to be the last expression in its body.
Let’s look at an example. We want to get a string description of a number:
fun main() {
val describe: (Int) -> String = { n ->
val sign = if (n >= 0) "non-negative" else "negative"
"n=$n ($sign)"
}
println(describe(7)) // n=7 (non-negative)
println(describe(-3)) // n=-3 (negative)
}
The last line inside the lambda is the string "n=$n ($sign)". That means it becomes the lambda’s result.
A very practical check: “what does the lambda end with?” If it ends with println(...), then the result will be Unit (because println returns Unit). If it ends with an expression that evaluates to a value, then the lambda returns that value.
Here is an example of a “lambda that performs an action” (and returns Unit):
fun main() {
val log: (String) -> Unit = { msg ->
println("LOG: $msg")
}
log("Starting!") // LOG: Starting!
}
In learning tasks there’s often confusion: “I thought that if the lambda is written, it will already do something”. No — until you call it, it just sits in a variable, like a borscht recipe. The recipe doesn’t cook borscht by itself.
Multi-line lambda: like a small function
When you first see a lambda that’s 5–6 lines long, your brain may start to panic: “why is a nameless function living inside braces, is this even legal?”. It is. And it can even be useful — if you do it carefully.
A multi-line lambda reads just like a normal function: inside you can declare intermediate vals, you can use if as an expression, and at the end you return the final result.
For example, let’s make a lambda that formats an amount of money for output (roughly: mark large amounts with "(!)"):
fun main() {
val formatAmount: (Int) -> String = { amount ->
val badge = if (amount >= 1000) " (!)" else ""
"$amount$badge"
}
println(formatAmount(120)) // 120
println(formatAmount(1500)) // 1500 (!)
}
Notice the style: we don’t try to cram everything into a single line, but we also don’t turn the lambda into a “three-volume novel”. Inside there is one intermediate val, and the last line is the final result.
If you want something a bit more “realistic”, let’s take an element from our practical app. For now, we store an expense as Triple(id, category, amount) (without classes — because we’ll get to OOP later).
fun main() {
val formatExpense: (Triple<Int, String, Int>) -> String = { exp ->
val (id, category, amount) = exp
val mark = if (amount >= 1000) " (!)" else ""
"#$id $category: $amount$mark"
}
println(formatExpense(Triple(1, "food", 450))) // #1 food: 450
println(formatExpense(Triple(2, "rent", 15000))) // #2 rent: 15000 (!)
}
Technically this is still “just a lambda”, but it feels like a small formatting function that you can reuse in different places without copying string templates.
5. Types in lambdas: where Kotlin can figure it out
Up to this point we honestly wrote the variable type on the left: (Int) -> Boolean, (String) -> String, and so on. But Kotlin can often infer the type itself — either from parameter types or from context.
The simplest example: the parameter type is specified directly in the lambda, so the type of the whole lambda is clear:
fun main() {
val square = { x: Int -> x * x }
println(square(5)) // 25
}
Here the type of the variable square is inferred automatically: since the parameter is Int and the result is Int, it’s something like “function Int -> Int”.
And here is an example where Kotlin cannot infer it, because you wrote it too “abstractly”:
fun main() {
// Don't do this: the compiler has no way to know the type of x
// val id = { x -> x }
val idInt: (Int) -> Int = { x -> x }
println(idInt(42)) // 42
}
Why is the first version bad? Because x has no type, there is no context, and the compiler can’t read minds. In such cases you either specify the parameter type directly in the lambda ({ x: Int -> x }), or specify the variable type on the left (val idInt: (Int) -> Int = ...).
6. Mini example: a rule and formatting as lambdas
Let’s take a small but practical step toward “real” code. We won’t build complex abstractions yet — we’ll just show that logic can be stored in variables: separately “how to print”, separately “what counts as big”.
Imagine that in some place in our app we want to print a list of expenses, but mark large amounts. Let’s make two lambdas: isBig and format.
fun main() {
val expenses = listOf(
Triple(1, "food", 450),
Triple(2, "transport", 120),
Triple(3, "rent", 15000)
)
val isBig: (Int) -> Boolean = { it >= 1000 }
val format: (Triple<Int, String, Int>) -> String = { exp ->
val (id, category, amount) = exp
val mark = if (isBig(amount)) " (!) " else " "
"#$id$mark$category: $amount"
}
for (e in expenses) {
println(format(e))
// #1 food: 450
// #2 transport: 120
// #3 (!) rent: 15000
}
}
What’s nice here is that if tomorrow you decide that “big” starts at 5000, you will change one place: val isBig = { it >= 5000 }. And formatting will automatically start using the new rule — because format calls isBig(amount) inside.
This isn’t yet “passing behavior into a function” (we’ll get to that a bit later), but it’s already a good transition from “everything is hardcoded in if” to a cleaner structure.
7. Typical mistakes when working with lambda expressions
Mistake #1: using it when there is more than one parameter.
it exists only for a lambda with one parameter. If there are two parameters, you have to write { a, b -> ... }. If you try to “drag in” it, the code either won’t compile or will become unreadable, because you’ll start inventing weird names and confusing what goes where.
Mistake #2: expecting a lambda to execute by itself just because it’s written.
A lambda is a value. It doesn’t run at the moment of declaration. Until you call it via f(x) (or pass it somewhere that will call it), it just sits in a variable. A very typical symptom: “I wrote { println("hi") }, but it doesn’t print anything”.
Mistake #3: forgetting that the last expression is returned, and accidentally returning Unit.
When the lambda ends with println(...), the lambda effectively returns Unit. Sometimes this is surprising, because you “seem to have computed a value”, but then printed in the last line. The fix is simple: make the computation the last expression (and printing a separate step), or store the computed value in a val and return it as the last line. The rule “the last expression becomes the result” is fundamental.
Mistake #4: writing a lambda that’s too large inline and losing the meaning.
If a lambda is 15 lines long, has three ifs, two counters, and a bit of philosophy — it’s hard on the brain. Even if it works formally, it’s unpleasant to read. In such cases it’s usually better either to give the parameter a meaningful name (instead of constant it), or to extract the logic into a separate named function, keeping the lambda short and clear.
Mistake #5: leaving types without any hints and getting Cannot infer a type for this parameter.
A lambda like { x -> x } looks logical to a human, but the compiler doesn’t have to understand what you meant. If the context doesn’t provide the type, Kotlin will ask you to clarify: either x: Int, or the variable type on the left. This is normal: the compiler isn’t being picky; it just requires unambiguity so it doesn’t have to guess which version of the code to run.
GO TO FULL VERSION