1. Знайомство з лямбда‑функціями
Якщо ви поки що не дуже впевнено почуваєтеся в програмуванні, лямбди можуть звучати як «якась магія функціонального підходу». Насправді ідея дуже життєва: інколи нам хочеться поводитися з дією так само, як із числом або рядком. Число можна покласти в змінну, передати у функцію, повернути з функції. Так само й з лямбдою: ми робимо те саме — тільки зберігаємо не дані, а невелику логіку.
Уявіть, що в нашому консольному обліку витрат в одному місці ми хочемо вважати «великими витратами» суму від 1000, а в іншому — від 500. Якщо правило «зашито» в if, доведеться копіювати if (amount >= 1000) по всьому проєкту, а потім героїчно шукати, де ви забули все оновити. Лямбда дає змогу написати «правило» один раз, присвоїти його змінній і використовувати там, де потрібно.
2. Лямбда як значення: зберігаємо в val і викликаємо
Найважливіше, що варто відчути на практиці: лямбда — не «особливий оператор», а значення. Тобто її цілком реально зберегти в змінній. А раз це значення, то його можна викликати — як звичайну функцію.
Почнемо з максимально простого прикладу: перевірки парності числа.
fun main() {
val isEven: (Int) -> Boolean = { x -> x % 2 == 0 }
println(isEven(4)) // true
println(isEven(5)) // false
}
Тут одразу відбувається кілька корисних речей.
По‑перше, праворуч від = ми створили лямбду { x -> x % 2 == 0 }.
По‑друге, ліворуч ми чесно вказали тип змінної: (Int) -> Boolean. Поки не заглиблюйтеся — сприймайте це як «тип функції». Він буквально означає: «приймає Int, повертає Boolean».
По‑третє, нижче ми викликаємо лямбду так само, як функцію: isEven(4). Тобто змінна isEven поводиться як функція.
Щоб закріпити ідею «лямбда — це значення», ось ще один приклад, у якому лямбда повертає рядок:
fun main() {
val greet: (String) -> String = { name -> "Привіт, $name!" }
println(greet("Котлін")) // Привіт, Котлін!
}
Сенс той самий: ми зберігаємо «формулу» привітання в змінній і можемо викликати її скільки завгодно разів.
Створити й викликати — різні дії
Навіть у цій першій лекції корисно виробити одну звичку: відрізняти місце, де лямбду створюють, від місця, де її викликають.
Порівняйте:
fun main() {
val inc: (Int) -> Int = { it + 1 } // створюємо й зберігаємо
val a = inc(10) // викликаємо
println(a) // 11
}
Якщо ви написали { it + 1 }, але ніде не написали inc(10), то нічого не станеться. Лямбда не «працює сама по собі»: це не фоновий демон і не таймер.
3. Синтаксис лямбди та параметр it
Базова форма: { параметри -> тіло }
У лямбдах легко заплутатися: там і фігурні дужки, і стрілка, і відчуття, ніби ви випадково відкрили портал в інший вимір. Давайте «приручимо» це як звичайну граматику: ось де параметри, ось де тіло, ось що повертається.
Базова форма лямбди в Kotlin виглядає так: завжди фігурні дужки, усередині — параметри, потім стрілка ->, а після неї — тіло. Параметри (з необовʼязковими типами) ідуть до ->, тіло — після, а значення, що повертається, береться з останнього виразу.
Ось мінітабличка «як читати»:
| Фрагмент | Що це означає | Приклад |
|---|---|---|
|
«Це лямбда» (завжди у фігурних дужках) | |
| x ліворуч від -> | параметр лямбди | |
|
«від параметрів до тіла» | |
| вирази праворуч | тіло лямбди | |
І ось приклад трохи довший, але все ще простий:
fun main() {
val addVat: (Int) -> Int = { price ->
price + (price * 20 / 100)
}
println(addVat(100)) // 120
println(addVat(250)) // 300
}
Тут параметр називається price, тіло — це вираз price + ..., а результатом лямбди буде значення останнього виразу.
Зверніть увагу на корисну звичку читання: коли бачите { ... -> ... }, подумки замінюйте це на «маленька функція без імені». Не на «магічний блок» і не на «особливий синтаксис списків», а саме на функцію.
Один параметр — можна використовувати it
Коли в лямбди рівно один параметр, Kotlin часто дозволяє його не оголошувати явно. Тоді цей параметр автоматично доступний під іменем it. Якщо сигнатура зрозуміла з контексту й параметр один, імʼя можна опустити — і компілятор підставить it.
Приклад: перевірка «чи додатне число»:
fun main() {
val isPositive: (Int) -> Boolean = { it > 0 }
println(isPositive(10)) // true
println(isPositive(-1)) // false
}
Тут it — це той самий єдиний Int, який потрапляє в лямбду.
Важливо не перетворювати it на релігію. it доречний, коли умова коротка й очевидна. Якщо логіка складніша, краще назвати параметр за змістом — інакше ви читатимете код як загадку: «а it — це що… число? рядок? кіт?».
Порівняйте два варіанти:
fun main() {
val ok1: (String) -> Boolean = { it.trim().isNotEmpty() }
val ok2: (String) -> Boolean = { text -> text.trim().isNotEmpty() }
println(ok1(" ")) // false
println(ok2("hi")) // true
}
Обидва варіанти коректні. Другий інколи читається спокійніше — особливо якщо ви потім додасте ще кілька рядків у тіло лямбди.
4. Значення, що повертається, і багаторядкові лямбди
Що повертає лямбда: результат останнього виразу
У Kotlin є дуже зручна домовленість: якщо ви явно не пишете return@..., то значенням лямбди вважається останній вираз у її тілі.
Подивімося на прикладі. Ми хочемо отримати рядковий опис числа:
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)
}
Останній рядок усередині лямбди — це рядок "n=$n ($sign)". Отже, саме він і стане результатом лямбди.
Дуже корисна «побутова» перевірка: «чим закінчується лямбда?» Якщо вона закінчується println(...), то результатом буде Unit (бо println повертає Unit). Якщо ж закінчується виразом, який обчислюється в значення, то й лямбда поверне саме це значення.
Ось приклад «лямбда, яка виконує дію» (і повертає Unit):
fun main() {
val log: (String) -> Unit = { msg ->
println("LOG: $msg")
}
log("Стартуємо!") // LOG: Стартуємо!
}
У навчальних задачах часто виникає плутанина: «я думав, що якщо лямбда написана, то вона вже щось зробить». Ні — доки ви її не викликали, вона просто лежить у змінній, як рецепт борщу. Рецепт сам борщ не зварить.
Багаторядкова лямбда: як маленька функція
Коли ви вперше бачите лямбду на 5–6 рядків, мозок може почати панікувати: «чому функція без імені живе у фігурних дужках, це взагалі законно?». Законно. І навіть корисно — якщо робити це акуратно.
Багаторядкова лямбда читається так само, як звичайна функція: усередині можна оголошувати проміжні val, можна використовувати if як вираз, а наприкінці — повернути підсумок.
Наприклад, зробімо лямбду, яка форматує суму грошей для виводу (умовно: великі суми позначаємо "(!)"):
fun main() {
val formatAmount: (Int) -> String = { amount ->
val badge = if (amount >= 1000) " (!)" else ""
"$amount$badge"
}
println(formatAmount(120)) // 120
println(formatAmount(1500)) // 1500 (!)
}
Зверніть увагу на стиль: ми не намагаємося втиснути все в один рядок, але й не перетворюємо лямбду на «роман у трьох томах». Усередині є одна проміжна змінна val, а останній рядок — фінальний результат.
Якщо хочеться ще трохи «наблизити до реальності», візьмемо елемент нашого практичного застосунку. Поки що ми зберігаємо витрату як Triple(id, category, amount) (без класів — до ООП ми дістанемося пізніше).
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 (!)
}
Технічно це все ще «просто лямбда», але за відчуттям — невелика функція форматування. Її можна повторно використовувати в різних місцях, не копіюючи рядкові шаблони.
5. Типи в лямбдах: де Kotlin здогадається сам
До цього моменту ми чесно писали тип змінної ліворуч: (Int) -> Boolean, (String) -> String і так далі. Але Kotlin часто вміє виводити тип сам — або з типів параметрів, або з контексту.
Найпростіший приклад: тип параметра вказано прямо в лямбді — отже тип усієї лямбди зрозумілий:
fun main() {
val square = { x: Int -> x * x }
println(square(5)) // 25
}
Тут тип змінної square виводиться автоматично: раз параметр Int і результат Int, то це щось на кшталт «функція Int -> Int».
А ось приклад, де Kotlin не може здогадатися, бо ви написали надто «абстрактно»:
fun main() {
// Так робити не варто: компілятору нізвідки дізнатися тип x
// val id = { x -> x }
val idInt: (Int) -> Int = { x -> x }
println(idInt(42)) // 42
}
Чому перша версія погана? Бо x без типу, контексту немає, а компілятор не вміє читати думки. У таких випадках ви або вказуєте тип параметра прямо в лямбді ({ x: Int -> x }), або задаєте тип змінної ліворуч (val idInt: (Int) -> Int = ...).
6. Мініприклад: правило й форматування як лямбди
Зробімо невеликий, але практичний крок у бік «живого» коду. Поки що не будуватимемо складних абстракцій — просто покажемо, що логіку можна покласти в змінні: окремо «як друкувати», окремо «що вважати великим».
Уявімо, що в якомусь місці нашого застосунку ми хочемо вивести список витрат і позначати великі суми. Зробимо дві лямбди: isBig і 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
}
}
Тут зручно те, що якщо завтра ви вирішите, що «великий» — це від 5000, ви зміните одне місце: val isBig = { it >= 5000 }. І форматування автоматично почне використовувати нове правило — бо format усередині викликає isBig(amount).
Це ще не «передавання поведінки у функцію» (до цього ми дістанемося трохи пізніше), але вже хороший перехід від «усе зашито в if» до акуратнішої структури.
7. Типові помилки під час роботи з лямбда‑виразами
Помилка №1: використовувати it, коли параметрів більше ніж один.
it існує лише для лямбди з одним параметром. Якщо параметрів два, доведеться писати { a, b -> ... }. Якщо спробувати «протягнути» it, код або не скомпілюється, або стане нечитабельним: ви почнете вигадувати дивні імена й плутатиметеся, що і куди передається.
Помилка №2: чекати, що лямбда виконається сама, раз вона написана.
Лямбда — це значення. Вона не запускається в момент оголошення. Доки ви не викликали її через f(x) (або не передали туди, де її викличуть), вона просто лежить у змінній. Дуже типовий симптом: «я написав { println("hi") }, а воно нічого не друкує».
Помилка №3: забути, що повертається останній вираз, і випадково повернути Unit.
Коли наприкінці лямбди стоїть println(...), лямбда фактично повертає Unit. Іноді це несподівано, бо ви нібито «порахували значення», але останнім рядком зробили друк. Виправляється просто: робіть обчислення останнім виразом (а друк — окремим кроком) або зберігайте обчислення в val і повертайте його останнім рядком. Правило «останній вираз стає результатом» — базове.
Помилка №4: писати надто велику лямбду прямо на місці й втрачати сенс.
Якщо лямбда розрослася до 15 рядків, у ній три if, два лічильники й трохи філософії — мозку важко. Навіть якщо формально все працює, читати такий код неприємно. У таких ситуаціях зазвичай краще або дати параметру змістовне імʼя (замість вічного it), або винести логіку в окрему іменовану функцію, а лямбду залишити короткою та зрозумілою.
Помилка №5: залишити типи повністю без підказок і отримати Cannot infer a type for this parameter.
Лямбда на кшталт { x -> x } виглядає логічно по‑людськи, але компілятор не зобовʼязаний вгадувати, що ви мали на увазі. Якщо контекст не підказує тип, Kotlin попросить вас уточнити: або x: Int, або тип змінної ліворуч. Це нормально: компілятор не прискіпується — він просто вимагає однозначності, щоб потім не «вгадувати», яку версію коду запускати.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ