JavaRush /Курси /Kotlin SELF /Expression body і локальні функції

Expression body і локальні функції

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

1. Функція як вираз

Коли ви починаєте активно виносити код у функції, трапляється типова історія: файл стає приємнішим, main() — коротшим, але… функцій зʼявляється багато. І серед них є такі, які за змістом цілком доречні, а за розміром виглядають як «пʼять рядків заради однієї думки». Надто багато дрібних функцій може бути незручним.

З іншого боку, якщо залишити все в одному main, отримаємо «простирадло». Тож потрібен баланс: маленькі функції — це добре, але «код заради коду» інколи лише заважає. У Kotlin є два інструменти, які допомагають утримувати цей баланс: expression body (коротке тіло функції через =) і локальні функції (функції всередині інших функцій).

Expression body

Expression body — це спосіб сказати Kotlin: «тіло цієї функції складається з одного виразу, тож запишімо її акуратно — без фігурних дужок і return». Припустімо, нам потрібна функція для піднесення до квадрата. Ось її класичний запис:

fun square(x: Int): Int {
    return x * x
}

Але ми можемо записати її й коротше:

fun square(x: Int): Int = x * x

Зміст той самий, але запис ближчий до математичної формули. Для простих обчислень і перевірок це часто читається краще: функція виглядає як визначення, а не як міні‑сценарій.

Міні‑приклад 1: обчислення x у кубі

fun cube(x: Int): Int = x * x * x

fun main() {
    println(cube(3)) // 27
}

Міні‑приклад 2: перевірка на парність

fun isEven(x: Int): Boolean = x % 2 == 0

fun main() {
    println(isEven(10)) // true
    println(isEven(7))  // false
}

Тут важливо відчути суть: isEven читається як запитання, а праворуч — «відповідь‑формула». Для початківців це, чесно кажучи, один із найприємніших способів читати код.

2. Expression body: виведення типу і коли краще блочне тіло

Expression body має приємний бонус: Kotlin часто сам визначає тип, який повертається, за виразом праворуч. Тобто можна писати ще коротше:

fun square(x: Int) = x * x

І компілятор виведе тип як Int.

Але варто бути уважними: інколи явний тип — це не «зайва писанина», а турбота про того, хто читатиме код. Тож домовмося про просте правило: якщо функція цілком очевидна — тип можна опустити; якщо є бодай тінь сумніву — краще залишити.

Порівняймо в таблиці:

Варіант Приклад Коли використовувати
Expression body + явний тип
fun isEven(x: Int): Boolean = x % 2 == 0
Коли хочете, щоб контракт читався одразу
Expression body + виведення типу
fun isEven(x: Int) = x % 2 == 0
Коли вираз максимально очевидний
Блочне тіло
fun f(...) { ... }
Коли є кілька кроків, if, цикл або змінні

Міні‑приклад: рядок як результат

fun formatMoney(amount: Int): String = "$amount €"

fun main() {
    println(formatMoney(120)) // 120 €
}

Коли expression body краще не використовувати

Expression body має пастку: раз можна коротше — значить «треба» коротше. У результаті код перетворюється на однорядкових монстрів, де половина логіки ховається в дужках. Читати таке хочеться хіба що під загрозою дедлайну.

Якщо всередині функції зʼявляється кілька кроків, проміжні змінні, цикл або розгалуження, то блочне тіло зазвичай логічніше: воно показує, що тут не формула, а процес.

Порівняйте відчуття. Так — нормально й читабельно:

fun absInt(x: Int): Int {
    if (x < 0) return -x
    return x
}

Так — уже спірно для новачка (хоча технічно коректно):

fun absInt(x: Int): Int = if (x < 0) -x else x

3. Міні‑застосунок: «Cкарбничка витрат»

Щоб не обговорювати компактність у вакуумі, продовжимо розвивати консольний застосунок. Нехай це буде проста «cкарбничка витрат»: користувач вводить кілька витрат (сум), а програма рахує підсумок, максимум і середнє.

Спочатку накидаємо простий каркас (ніби він уже був у попередніх лекціях), а потім почнемо його «причісувати».

fun main() {
    val amounts = intArrayOf(120, 80, 200, 50)

    val total = sumAmounts(amounts)
    val max = maxAmount(amounts)
    val avg = averageAmount(amounts)

    println("Разом: $total")     // Разом: 450
    println("Максимум: $max")    // Максимум: 200
    println("Середнє: $avg")     // Середнє: 112.5
}

Функції поки зробимо звичайними, щоб було з чим працювати:

fun sumAmounts(xs: IntArray): Int {
    var total = 0
    for (x in xs) {
        total += x
    }
    return total
}

fun maxAmount(xs: IntArray): Int {
    var m = xs[0]
    for (x in xs) {
        if (x > m) m = x
    }
    return m
}

fun averageAmount(xs: IntArray): Double {
    val total = sumAmounts(xs)
    return total.toDouble() / xs.size
}

Тепер подивімося очима «редактора коду»: які функції тут — кандидати на expression body? averageAmount — так, бо в ній логіка майже формульна (хоча є проміжна змінна). Можна зробити трохи компактніше, але без фанатизму.

Охайний варіант, який усе ще легко читати:

fun averageAmount(xs: IntArray): Double = sumAmounts(xs).toDouble() / xs.size

І тут добре видно головну перевагу: ми не «стиснули код заради стискання», а зробили функцію схожою на визначення середнього.

Оновімо приклад повністю (зверніть увагу: ми залишили sumAmounts і maxAmount блочними, бо там є цикл, а це вже не «один вираз»):

fun averageAmount(xs: IntArray): Double = sumAmounts(xs).toDouble() / xs.size

fun main() {
    val amounts = intArrayOf(120, 80, 200, 50)
    println(averageAmount(amounts)) // 112.5
}

4. Локальні функції в застосунку

Іноді функція потрібна лише як маленький помічник усередині одного сценарію. Виносити її на рівень файла — означає «засмічувати простір імен»: зʼявляється ще одне імʼя, ще одна річ, яку можна випадково використати не там, де треба. Локальна функція розвʼязує це: ви оголошуєте fun helper(...) прямо всередині іншої функції, і вона існує лише там.

Це схоже на те, як ви заводите локальну змінну: ви ж не оголошуєте кожну тимчасову змінну глобально на весь файл. Так само й із локальними функціями: вони «живуть поруч із місцем використання» й не заважають іншим.

Важливий момент: локальна функція може звертатися до змінних зовнішньої функції й навіть змінювати їх (тобто використовувати «зовнішнє оточення»). Це працює так само, як і в інших вкладених конструкціях: вони можуть захоплювати змінні із зовнішньої області видимості (scope).

Введення з парсингом і валідацією

Зробімо наступний крок у «копілці витрат»: замість жорстко заданого масиву читатимемо витрати з консолі. Але тут одразу зʼявляється типовий сценарій введення: прочитати рядок, прибрати зайві пробіли, розпарсити число, перевірити, що воно додатне. І такий сценарій часто або дублюється, або стає надто довгим усередині main.

Зробімо функцію readPositiveInt(prompt: String): Int, а всередині неї сховаємо локальних помічників.

fun readPositiveInt(prompt: String): Int {
    fun parseIntOrNull(text: String): Int? {
        val cleaned = text.trim()
        return cleaned.toIntOrNull()
    }

    while (true) {
        print(prompt)
        val raw = readln()
        val value = parseIntOrNull(raw)

        if (value != null && value > 0) return value
        println("Введіть ціле число, більше за 0.")
    }
}

Тут локальна функція parseIntOrNull не потрібна всій програмі — вона потрібна лише для цього введення. Тому тримати її всередині логічно: ви читаєте readPositiveInt і одразу бачите, як саме ми парсимо рядок.

Невелика блок‑схема процесу введення

flowchart TD
    A[Показати prompt] --> B[Прочитати рядок]
    B --> C[trim]
    C --> D[toIntOrNull]
    D --> E{value != null і value > 0?}
    E -- так --> F[return value]
    E -- ні --> G[Повідомлення про помилку]
    G --> A

Тепер використаємо цю функцію в main і зберемо масив витрат фіксованого розміру. Розмір теж запитаємо в користувача — так буде ближче до реального застосунку.

fun main() {
    val n = readPositiveInt("Скільки витрат увести? ")

    val amounts = IntArray(n)
    for (i in 0 until n) {
        amounts[i] = readPositiveInt("Витрата #${i + 1}: ")
    }

    println("Разом: ${sumAmounts(amounts)}") // наприклад: Разом: 450
}

Комбінуємо підходи: helpers і expression body

Найприємніше починається тоді, коли ви поєднуєте інструменти. Локальні функції допомагають сховати одноразові деталі, а expression body робить «формульні» функції короткими й читабельними.

Наприклад, у нашому застосунку можна зробити маленьку функцію форматування суми — і вона ідеально підходить для expression body. Причому це корисно навіть зараз, коли здається: «та я й так можу написати $amount € прямо в println». Можете. Але потім ви захочете змінити формат (наприклад, додати слово «євро») і почнете шукати по файлу всі місця, де друкували суму.

fun formatMoneyEuro(amount: Int): String = "$amount €"

А всередині main:

fun main() {
    val amounts = intArrayOf(120, 80, 200, 50)
    val total = sumAmounts(amounts)

    println("Разом: ${formatMoneyEuro(total)}") // Разом: 450 €
}

Ще один хороший кандидат — «середнє». Ми вже це робили:

fun averageAmount(xs: IntArray): Double = sumAmounts(xs).toDouble() / xs.size

Тепер трохи поліпшимо підсумковий вивід, але обережно: без громіздкого форматування й без нових тем.

fun printReport(amounts: IntArray) {
    val total = sumAmounts(amounts)
    val max = maxAmount(amounts)
    val avg = averageAmount(amounts)

    println("Разом: ${formatMoneyEuro(total)}")     // Разом: 450 €
    println("Максимум: ${formatMoneyEuro(max)}")    // Максимум: 200 €
    println("Середнє: $avg")                       // Середнє: 112.5
}

І main стає «сценарієм», а не «мішком логіки»:

fun main() {
    val n = readPositiveInt("Скільки витрат увести? ")

    val amounts = IntArray(n)
    for (i in 0 until n) {
        amounts[i] = readPositiveInt("Витрата #${i + 1}: ")
    }

    printReport(amounts)
}

Ось тут якраз видно, навіщо все це: main читається як послідовність кроків, а деталі розкладені по функціях. Водночас ми не плодимо зайвих «одноразових» функцій на рівні файла: те, що потрібно лише всередині введення, живе локально.

5. Типові помилки при expression body і локальних функціях

Помилка № 1: перетворювати expression body на «компресор мозку».
Іноді хочеться запакувати в = усе підряд — включно зі складними умовами та багатоповерховими обчисленнями. Формально компілятор це перетравить, але людина — не завжди. Якщо рядок перестає читатися «з першого погляду», краще повернутися до блочного тіла й чесно розписати кроки.

Помилка № 2: намагатися зробити expression body там, де потрібен цикл або кілька дій.
Expression body — це про один вираз. Якщо у вас зʼявився for, накопичення суми, оновлення змінної, друк проміжних значень — це вже сценарій, і йому пасує звичайне { ... }. Не сприймайте блочне тіло як «гірше»: воно просто для іншого типу задач.

Помилка № 3: виносити назовні функції, які потрібні рівно в одному місці.
Часто новачок робить десяток маленьких функцій‑помічників на рівні файла, а потім сам же плутається, що й де використовується. Локальна функція — хороший компроміс: вона дає імʼя кроку, але не роздуває зовнішній API файла. Якщо помічник використовується лише всередині однієї функції введення — нехай він і живе там.

Помилка № 4: надмірно ускладнювати локальні функції й ховати важливу логіку надто глибоко.
Локальна функція має бути маленькою й «до діла». Якщо ви всередині readPositiveInt оголосили 5 локальних функцій, і кожна — по 20 рядків, це вже не «сховали деталі», а «зробили квест». У такому разі краще винести частину на рівень файла й дати їй нормальне імʼя.

Помилка № 5: випадково покладатися на зовнішні змінні й отримувати несподівані ефекти.
Локальні функції можуть звертатися до змінних зовнішньої функції й змінювати їх — це зручно, але може стати пасткою, якщо ви не помітили, що помічник змінює стан. Зробіть такі зміни очевидними: якщо помічник щось змінює, це має читатися з коду, а не бути сюрпризом.

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