JavaRush/Courses/Kotlin SELF/Expression body and local functions

Expression body and local functions

Open

1. A function as an expression

When you start actively extracting code into functions, a typical story happens: the file becomes nicer, main() becomes shorter, but… you end up with many functions. And among them there are ones that make perfect sense, yet look like “5 lines for a single idea”. Too many tiny functions can be inconvenient.

On the other hand, if you leave everything inside one main, you get a “wall of code”. So we need balance: small functions are good, but code for the sake of code can also get in the way. In Kotlin we have two tools to maintain this balance: expression body (a short function body via =) and local functions (functions inside other functions).

Expression body

Expression body is a way to tell Kotlin: “this function’s body consists of a single expression, let’s write it nicely, without braces and without return.” Suppose we need a function to square a number. Here is the classic form:

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

But we can write it shorter:

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

The meaning is the same, but the notation is closer to a math formula. For simple calculations and checks, this often reads better: the function looks like a definition, not a mini script.

Mini example 1: computing x cubed

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

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

Mini example 2: checking for evenness

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

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

What matters here is the idea: isEven reads like a question, and on the right you have the “answer formula”. For beginners, honestly, this is one of the most pleasant ways to read code.

2. Expression body: type inference and when a block body is better

Expression body has a nice bonus: Kotlin can often infer the return type from the expression on the right. That means you can write even shorter:

fun square(x: Int) = x * x

And the compiler will infer the type as Int.

But you need to be careful: sometimes an explicit type is not “extra typing”, but a courtesy to the reader. So let’s agree on a simple rule: if the function is completely obvious, you can omit the type; if there is even a shadow of doubt, it is better to keep it.

Let’s compare in a table:

Option Example When to use
Expression body + explicit type
fun isEven(x: Int): Boolean = x % 2 == 0
When you want the contract to be readable immediately
Expression body + type inference
fun isEven(x: Int) = x % 2 == 0
When the expression is as obvious as possible
Block body
fun f(...) { ... }
When there are multiple steps, if, a loop, variables

Mini example: a string as a result

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

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

When it is better not to use expression body

Expression body has a trap: if you can write it shorter, you must write it shorter. As a result, code turns into one-line monsters where half the logic is hidden in parentheses, and you only want to read it under deadline pressure.

If the function contains multiple steps, intermediate variables, a loop, or branching, then a block body is usually more logical: it shows that this is not a formula, but a process.

Compare the feel. This is fine and readable:

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

This is already questionable for a beginner (though technically correct):

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

4. Mini app: “Expense jar”

To avoid discussing compactness in a vacuum, let’s keep developing one console app. Let it be a simple “expense jar”: the user enters several expenses (amounts), and the program computes the total, the maximum, and the average.

First, we will sketch a simple skeleton (as if it already existed in previous lectures), and then we will start “polishing” it.

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

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

    println("Total: $total")     // Total: 450
    println("Maximum: $max")    // Maximum: 200
    println("Average: $avg")     // Average: 112.5
}

For now, let’s keep the functions as regular ones, so we have something to work with:

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
}

Now let’s look with “a code editor’s eyes”: which functions here are candidates for expression body? averageAmount — yes, because the logic is almost formula-like (even though there is an intermediate variable). We can make it a bit more compact, but without fanaticism.

A neat option that is still readable:

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

And here you can clearly see the main advantage: we did not “compress code just to compress it”, we made the function look like the definition of an average.

Let’s update the example as a whole (note: we kept sumAmounts and maxAmount as block bodies, because there is a loop — that is no longer “one expression”):

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

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

5. Local functions in the app

Sometimes a function is needed only as a small helper inside one scenario. Pulling it up to the file level means “polluting the namespace”: you get one more name, one more thing that can be used by accident where it should not be. A local function solves this: you declare fun helper(...) right inside another function, and it exists only there.

This is similar to how you create a local variable: you do not declare every temporary variable globally for the whole file. Local functions follow the same idea: “it lives next to the usage site and does not interfere with the rest”.

An important point: a local function can access variables of the outer function and even modify them (that is, use the “outer environment”). This behavior is the same as with other nested constructs that can capture variables from an outer scope.

Input with parsing and validation

Let’s take the next step in the “expense jar”: instead of a hard-coded array, we will read expenses from the console. But we immediately get a typical input scenario: read a string, trim whitespace, parse a number, verify that it is positive. And this scenario often ends up either duplicated or too long inside main.

Let’s create a function readPositiveInt(prompt: String): Int, and hide local helpers inside it.

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("Enter an integer greater than 0.")
    }
}

Here the local function parseIntOrNull is not needed by the whole program — it is needed only for this input. So keeping it inside is logical: you read readPositiveInt, and immediately see how exactly we parse the string.

A small flowchart of the input process

flowchart TD
    A[Show prompt] --> B[Read string]
    B --> C[trim]
    C --> D[toIntOrNull]
    D --> E{value != null and value > 0?}
    E -- yes --> F[return value]
    E -- no --> G[Error message]
    G --> A

Now let’s use this function in main and build an expense array of a fixed size. We will also ask the user for the size — that will feel more like a real program.

fun main() {
    val n = readPositiveInt("How many expenses to enter? ")

    val amounts = IntArray(n)
    for (i in 0 until n) {
        amounts[i] = readPositiveInt("Expense #${i + 1}: ")
    }

    println("Total: ${sumAmounts(amounts)}") // for example: Total: 450
}

Combining approaches: helpers and expression body

The nicest part begins when you combine the tools. Local functions help hide one-off details, and expression body makes “formula-like” functions short and readable.

For example, in our app we can make a small function to format an amount, and it is a perfect fit for expression body. And this is useful even now, when it feels like “I can just write $amount € directly in println”. You can — but later you will want to change the format (for example, add the word “euro”), and you will have to search through the file for all places where you printed the amount.

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

And inside main:

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

    println("Total: ${formatMoneyEuro(total)}") // Total: 450 €
}

Another good candidate is “average”; we already did it:

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

Now let’s improve the final output a bit, but carefully: without huge formatting and without new topics.

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

    println("Total: ${formatMoneyEuro(total)}")     // Total: 450 €
    println("Maximum: ${formatMoneyEuro(max)}")    // Maximum: 200 €
    println("Average: $avg")                       // Average: 112.5
}

And main becomes a “scenario”, not a “bag of logic”:

fun main() {
    val n = readPositiveInt("How many expenses to enter? ")

    val amounts = IntArray(n)
    for (i in 0 until n) {
        amounts[i] = readPositiveInt("Expense #${i + 1}: ")
    }

    printReport(amounts)
}

This is exactly where you can see why all of this matters: main reads like a sequence of steps, while the details are decomposed into functions. At the same time, we do not create extra “one-off” functions at the file level: what is needed only inside input stays local.

6. Common mistakes with expression body and local functions

Mistake No. 1: turning expression body into a “brain compressor”.
Sometimes you want to pack everything into =, including complex conditions and multi-level computations. Formally, the compiler will digest it, but a human will not always. If a line stops being readable “at a glance”, it is better to return to a block body and write out the steps honestly.

Mistake No. 2: trying to use expression body where a loop or several actions are needed.
Expression body is about a single expression. If you have a for, sum accumulation, variable updates, printing intermediate values — that is already a scenario, and a regular { ... } fits it. Do not treat a block body as “worse”: it is simply for a different type of task.

Mistake No. 3: moving out functions that are needed in exactly one place.
A beginner often creates a dozen tiny helper functions at the file level and then gets confused about which one is used where. A local function is a good compromise: it gives a name to a step, but does not bloat the file’s external API. If a helper is used only inside one input function, let it live there.

Mistake No. 4: overcomplicating local functions and hiding important logic too deep.
A local function should be small and “to the point”. If inside readPositiveInt you declared 5 local functions and each is 20 lines — that is no longer “we hid details”, it is “we made a quest”. In that case, it is better to move part of it to the file level and give it a proper name.

Mistake No. 5: accidentally relying on outer variables and getting unexpected effects.
Local functions can access variables of the outer function and modify them — it is convenient, but it can become a trap if you did not notice that a helper mutates state. Keep such changes obvious: if a helper changes something, it should be readable from the code, not a surprise.

1
Task
Kotlin SELF,  8 level,  4 lecture
Locked
Warm-up timer
Warm-up timer
1
Task
Kotlin SELF,  8 level,  4 lecture
Locked
Name on the badge
Name on the badge
1
Task
Kotlin SELF,  8 level,  4 lecture
Locked
Safe limit
Safe limit
1
Task
Kotlin SELF,  8 level,  4 lecture
Locked
Robust input
Robust input
Комментарии
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны sign in
This page has no comments yet