A Kotlin Lambda Is a Value You Can Pass Around

Code as a value: defining versus calling, parameters and results, function types and what they compile to, invoke, nullable function types, type aliases, the implicit it, and multi-line bodies. Part 1 of five.

You’ve passed values around since your first program — numbers, strings, objects go into variables and out of functions all day. What you usually couldn’t pass around easily was behavior: a piece of code to run later. In Java that meant ceremony — an anonymous Comparator, a Runnable, a single-method interface wrapped in new. Java 8’s lambdas softened it, but they’re still tied to those functional interfaces. Kotlin makes a chunk of code a value as ordinary as the number 42, with a type of its own. That value is a lambda, and this five-part arc builds a complete picture of it — the runtime cost, the capture rules, the return gotcha, and the receiver trick behind Kotlin’s DSLs. We start with the foundation.

This is part one of five, following collections. Here we answer one question: what is a lambda?

The smallest lambda

A lambda is code wrapped in curly braces:

{ println("Hello") }

That’s a complete lambda — a value, a little package of “print Hello” that hasn’t happened yet. Like any value, you can put it in a variable:

val greet = { println("Hello") }

Here’s the idea to hold from the start: defining a lambda and running it are two different things. The line above defines greet and prints nothing. To run the code inside, you call it, with parentheses:

greet()        // now it prints: Hello
greet()        // prints again: Hello

Define once, call as often as you like. If that distinction is clear, the rest is detail.

Giving it an input

A lambda can take parameters — you list them at the start, followed by an arrow ->, then the body:

val greet = { name: String -> println("Hello, $name") }
greet("Ada")   // Hello, Ada

Read the braces as two halves split by the arrow: before -> are the inputs, after -> is the body.

Giving it an output

A lambda hands a value back without a return — the last expression in the body is the result:

val square = { x: Int -> x * x }
square(3)   // 9

The same rule holds for any body, and since the result is just “the last expression,” that expression can itself be an if or any value-producing Kotlin:

val shout   = { word: String -> word.uppercase() + "!" }   // "HI!"
val isAdult = { age: Int -> age >= 18 }                    // true / false
val sign    = { n: Int -> if (n >= 0) "positive" else "negative" }

Do note the flip side: because the last expression is the return value, a return keyword inside a lambda does not mean “return from the lambda.” That’s a genuine surprise with its own rules, and part four is where we face it.

What is a lambda’s type?

Every value has a type — 42 is an Int, "Ada" is a String. A lambda’s type describes its inputs and output, written with an arrow:

(Int) -> Int

Read it left to right: “takes an Int, gives back an Int” — exactly the type of square. Write it explicitly on the left and you no longer repeat the parameter’s type inside the braces, since the compiler already knows it:

val square: (Int) -> Int = { x -> x * x }

The shape is always (inputs) -> output:

val shout:   (String) -> String  = { w -> w.uppercase() + "!" }
val isAdult: (Int) -> Boolean     = { age -> age >= 18 }
val log:     (String) -> Unit     = { msg -> println(msg) }
(String, String) -> String        // two Strings in, a String out
() -> Unit                        // nothing in, nothing meaningful out

Under the hood on the JVM, a function type is an interface — (Int) -> Int is Function1<Int, Int>, () -> Unit is Function0<Unit>, and so on up to a fixed arity. You rarely name these, but knowing a lambda is “an object implementing a FunctionN interface” demystifies both the Java-interop story and the allocation cost that part five exists to eliminate.

Calling is really invoke

The parentheses that call a lambda are sugar for its invoke operator. These two lines are identical:

greet("Ada")
greet.invoke("Ada")

You almost never write invoke directly — with one exception that matters. A nullable function type needs it. If a lambda might be absent, its type ends in ?, and you call it with a safe call, which requires the method-style form:

val onClick: ((String) -> Unit)? = null
onClick?.invoke("save")     // calls only if non-null; there's no onClick?.() syntax

Nullable callbacks are everywhere — optional event handlers, defaulted hooks — so ?.invoke(...) is the idiom to keep in your pocket. Note the parentheses in ((String) -> Unit)?: they group the whole function type before the ?, distinguishing “a nullable function” from (String) -> Unit?, which is a non-null function returning a nullable Unit.

it: a shortcut for one parameter

Naming a single parameter often feels like overkill. When a lambda has exactly one parameter, you can skip the name and the arrow and refer to it as it:

val square: (Int) -> Int = { it * it }

it is the implicit name for that lone parameter — a convenience, not a new concept, and it’s everywhere in real Kotlin. With two or more parameters you must name them; there’s no it.

Naming a function type: typealias

A function type repeated across a codebase reads better with a name. typealias gives one — it introduces no new type, just an alias the compiler expands:

typealias Validator = (String) -> Boolean

val notBlank: Validator = { it.isNotBlank() }
fun runAll(input: String, rules: List<Validator>) = rules.all { it(input) }

Validator and (String) -> Boolean are interchangeable, but a signature reading List<Validator> says more than List<(String) -> Boolean>. This is the tidy way to keep function-typed APIs readable.

When one line isn’t enough

A lambda body can span several lines. The rule doesn’t change: the last line is the result, the lines above are setup.

val describe = { score: Int ->
    val grade = if (score >= 50) "pass" else "fail"
    "Result: $grade"          // this last line is what comes back
}
describe(72)   // "Result: pass"

Final thoughts

That’s the foundation, and it’s this small: a lambda is a value that happens to be code — braces, inputs before the ->, a result on its last line, a type like (Int) -> Int that compiles to a FunctionN object. Store it in a variable, call it with () (really invoke), and use ?.invoke(...) when it might be null.

So far we’ve only called our lambdas ourselves, which isn’t much use yet. The power comes from handing a lambda to another function and letting it do the calling. Next: lambdas at work, where this idea transforms how you work with lists — and where lazy sequences make a chain of transforms cost almost nothing.

Practice: reinforce this with the companion workbook — short, click-to-reveal problems.

Comments