JavaRush /Курси /Kotlin SELF /Лямбда‑вирази в Kotlin: синтаксис, it, значення, що повер...

Лямбда‑вирази в Kotlin: синтаксис, it, значення, що повертається

Kotlin SELF
Рівень 21 , Лекція 0
Відкрита

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 -> x + 1 }
x ліворуч від -> параметр лямбди
{ x -> ... }
->
«від параметрів до тіла»
{ x -> x + 1 }
вирази праворуч тіло лямбди
{ x -> x + 1 }

І ось приклад трохи довший, але все ще простий:

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, або тип змінної ліворуч. Це нормально: компілятор не прискіпується — він просто вимагає однозначності, щоб потім не «вгадувати», яку версію коду запускати.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ