Single-Expression Functions

A single-expression function in Kotlin is a function whose entire body is one expression, written after an = sign instead of inside { } braces with a return statement. It’s Kotlin’s way of saying: if a function just computes and returns a value, don’t make the reader wade through boilerplate to see that. This shows up constantly in idiomatic Kotlin code — predicates, small transformations, extension functions, and getters almost always use this form — and the compiler can even infer the return type for you.

Overview / How it works

Every Kotlin function has a body, and Kotlin gives you two ways to write one. The familiar way is a block body: curly braces containing one or more statements, with an explicit return for the value you want to hand back. The second way is an expression body, informally called a single-expression function: you drop the braces and the return keyword entirely, write an = after the function signature, and follow it with exactly one expression. That expression’s value becomes the function’s return value.

Under the hood these two forms compile to identical JVM bytecode. The compiler treats fun square(x: Int): Int = x * x exactly as if you had written fun square(x: Int): Int { return x * x } — there is no runtime difference, no performance cost, and no difference in how the function is called from Java or Kotlin code. The expression-body form exists purely to remove ceremony when a function’s logic really is “compute one value and give it back”, which turns out to be a huge fraction of the small functions you write day to day.

The other important thing the compiler does for expression-body functions is return type inference. Because the entire body is a single expression, the compiler can statically determine that expression’s type and use it as the function’s return type automatically — you don’t have to write it. fun greet(name: String) = "Hello, $name!" is inferred to return String without : String appearing anywhere. This only works for expression-body functions; a block-bodied function with an omitted return type is assumed to return Unit (Kotlin’s void-like type), because the compiler does not attempt to infer a type by analyzing every return statement buried inside a block.

Inference has one hard limit worth knowing up front: a recursive single-expression function cannot infer its own return type, because the compiler would need to already know that type in order to type-check the recursive call that appears inside the very expression it’s trying to type. For recursive functions you must write the return type explicitly, even when using the expression-body form. This is covered in Common Mistakes below.

Syntax

fun functionName(parameterName: ParameterType): ReturnType = expression

// return type is optional when it can be inferred:
fun functionName(parameterName: ParameterType) = expression
  • fun — keyword that starts every function declaration.
  • functionName — the function’s name, following normal Kotlin naming (lowerCamelCase).
  • (parameterName: ParameterType, ...) — the parameter list, exactly as in any Kotlin function.
  • : ReturnType — optional. Kotlin infers it from the expression’s type when omitted; write it explicitly for public API functions, and it is required for recursive functions.
  • = — replaces the { } block; everything after it is the function body.
  • expression — any single Kotlin expression: arithmetic, a string template, an if expression, a when expression, a function call, a safe-call chain, and so on. It cannot be a sequence of statements — for that you need a block body.

Examples

Example 1: A basic single-expression function

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

fun main() {
    println(square(5))
}

Output:

25

The function signature declares an explicit Int return type, and the body after = is the single expression x * x. There’s no return keyword and no braces — the value of the expression is automatically what square returns.

Example 2: Letting the compiler infer the return type

fun greet(name: String) = "Hello, $name!"

fun main() {
    println(greet("Kotlin"))
}

Output:

Hello, Kotlin!

No return type is written on greet. The string template "Hello, $name!" has static type String, so the compiler infers greet returns String without any help. This is the most common style for small expression-body functions.

Example 3: Single-expression extension function

fun Int.isEven() = this % 2 == 0

fun main() {
    println(4.isEven())
    println(7.isEven())
}

Output:

true
false

isEven is an extension function on Intthis refers to the receiver Int it’s called on. The body is the boolean expression this % 2 == 0, so the return type Boolean is inferred. Extension functions and single-expression syntax combine constantly in idiomatic Kotlin because most extensions are small, focused computations.

Example 4: Combining when, safe calls, and the elvis operator

fun sign(x: Int) = when {
    x < 0 -> "negative"
    x == 0 -> "zero"
    else -> "positive"
}

fun safeSign(x: Int?) = x?.let { sign(it) } ?: "unknown"

fun main() {
    println(sign(-5))
    println(safeSign(null))
    println(safeSign(10))
}

Output:

negative
unknown
positive

sign uses a when expression with an else branch as its single expression, so it’s exhaustive and infers String. safeSign shows that single-expression functions can call other single-expression functions and thread null-safety operators through cleanly: x?.let { sign(it) } is null when x is null, and the elvis operator ?: supplies "unknown" in that case.

How it works step by step

Take fun safeSign(x: Int?) = x?.let { sign(it) } ?: "unknown" and trace what the compiler and the runtime each do:

  1. Parsing: the compiler sees the = after the signature and parses everything that follows as one expression rather than a block of statements.
  2. Type inference: the compiler determines the static type of that expression bottom-up. x?.let { sign(it) } has type String? — a safe-call chain is always nullable, because the receiver might be null — and String? ?: "unknown" resolves to non-null String. That becomes safeSign‘s inferred return type.
  3. Compilation to bytecode: the expression is compiled as if it were { return x?.let { sign(it) } ?: "unknown" } — identical instructions to what a block body with a single return would produce.
  4. At call time with safeSign(null), the runtime evaluates x?.let { ... } first. Since x is null, the safe call short-circuits and the lambda never runs, producing null without a NullPointerException.
  5. The elvis operator ?: then sees a null left-hand side and evaluates its right-hand side, yielding "unknown".
  6. That value becomes what safeSign returns to its caller, which println writes to standard output.

Common Mistakes

Mistake 1: Adding braces after = creates a lambda, not a block body

It’s tempting to think { } after = works like a block body. It doesn’t — Kotlin sees a brace-delimited expression as a lambda literal, i.e. a function value, not a set of statements to execute.

// WRONG — this does not do what it looks like it does
fun square(x: Int) = { x * x }

fun main() {
    println(square(5))
}

Here square‘s inferred return type is () -> Int — a zero-argument function that computes x * x when called — not Int. Calling square(5) returns that lambda object itself, not 25, so println(square(5)) prints something like a function-object reference, not the number you expected. The fix is simply to remove the braces so the expression body is x * x directly:

fun square(x: Int) = x * x

fun main() {
    println(square(5))
}

Output:

25

Mistake 2: Forgetting the explicit return type on a recursive function

Return-type inference needs to already know a function’s type to type-check a call to that same function inside its own body. For a non-recursive function that’s never a problem, but a recursive single-expression function creates a circular dependency the compiler refuses to resolve.

// WRONG — fails to compile: the recursive call to factorial()
// needs factorial's return type before that type is known
fun factorial(n: Int) = if (n <= 1) 1 else n * factorial(n - 1)

This is rejected at compile time because factorial calls itself inside the very expression the compiler is trying to type. The fix is to declare the return type explicitly, breaking the circularity:

fun factorial(n: Int): Int = if (n <= 1) 1 else n * factorial(n - 1)

fun main() {
    println(factorial(5))
}

Output:

120

Best Practices

  • Use expression-body syntax for functions whose entire job is “compute and return one value” — predicates, small transformations, getters, and short extension functions.
  • Switch to a block body with { } and an explicit return as soon as the logic needs more than one statement, intermediate vals for readability, or side effects before returning.
  • Write the return type explicitly on public or exported single-expression functions even though it’s optional — it documents the API and prevents callers from being surprised if the expression’s inferred type quietly changes later.
  • Always write the return type explicitly on recursive single-expression functions — the compiler requires it, and it also makes the function’s contract obvious to readers.
  • Prefer if and when as the expression inside a single-expression function over chained boolean tricks; both are genuine expressions in Kotlin and read cleanly right after =.
  • Don’t chain let, also, or run excessively just to cram multi-step logic into one expression — if the single line becomes hard to read, a block body is the more idiomatic choice, not a compromise.

Practice Exercises

  1. Write a single-expression function fun isPositive(x: Int) that returns true if x is greater than zero and false otherwise. Call it with -3 and 7 and print both results.
  2. Write a single-expression extension function fun String.shout() that returns the string in uppercase with an exclamation mark appended, so "hi".shout() produces "HI!". Let Kotlin infer the return type.
  3. Write a recursive single-expression function fun fibonacci(n: Int): Int that returns the nth Fibonacci number, with fibonacci(0) == 0 and fibonacci(1) == 1. Remember that recursive expression-body functions require an explicit return type. Test it with n = 10; the expected output is 55.

Summary

  • A single-expression function replaces { return ... } with = expression — both forms compile to identical bytecode.
  • The return type can usually be omitted; the compiler infers it from the expression’s static type.
  • Recursive single-expression functions are the one case where the return type must be written explicitly, or the code fails to compile.
  • Putting { } after = creates a lambda — a function value — not a block body; this is a common source of confusing bugs.
  • Use expression-body syntax for short, purely computational functions, and switch to a block body once a function needs multiple statements.