skip to content

Function Types

The vocabulary for typing callables: plain function types, receiver types that power DSLs, nullable and suspend variants, and aliases that make them readable. Interviewers use this when discussing API design more than day-to-day code.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

explore

questions

20

You have a property `val onClick: (() -> Unit)?`. How do you call it safely when it might be null, and what is the difference between `onClick?.invoke()` and `onClick()`?

level: juniorimportance: must knowfreq 70%

answer

  1. `?.invoke()` is the safe call form
  2. `f()` desugars to `f.invoke()` — needs non-null receiver
  3. Parens: `(() -> Unit)?` vs `() -> Unit?`
  4. var member can't smart-cast → must use ?.invoke()
  5. result type is `Unit?`

basics

~10 s

Use onClick?.invoke(). It runs the function only if it is not null, otherwise it does nothing. Calling onClick() directly would not compile because the type may be null.

solid answer

~40 s

A nullable function type like `(() -> Unit)?` cannot be invoked with normal call syntax `onClick()` because the call operator is not defined on a nullable type — the compiler rejects it. You must first deal with the null. The idiomatic way is the safe-call form `onClick?.invoke()`, which only calls `invoke` when the reference is non-null and otherwise evaluates to `null` (or `Unit?`). The parentheses-call sugar `onClick()` desugars to `onClick.invoke()`, and `.invoke` requires a non-null receiver. Alternatives: a null check (`if (onClick != null) onClick()` with a smart cast, only for `val`/locals), or `onClick?.let { it() }`. For a default, `(onClick ?: {})()` substitutes an empty lambda.

code

kotlin · 11 lines
kotlin
class Button(var onClick: (() -> Unit)? = null) {
    fun click() {
        // onClick() would NOT compile (nullable, and var can't smart-cast)
        onClick?.invoke()
    }
}

val b = Button { println("clicked") }
b.click() // prints "clicked"
b.onClick = null
b.click() // does nothing, no NPE

go deeper

for a junior

Knows to use onClick?.invoke() and that direct onClick() won't compile on a nullable type.

for a middle

Explains f() desugars to .invoke(), the Unit? result type, and offers let/elvis alternatives.

for a senior

Articulates the smart-cast limitation on mutable member vars and the thread-safety reasoning behind it.

for a principal

Discusses API design — when to model an absent callback as nullable vs a no-op default lambda, and the ergonomics/perf trade-offs.

## What a nullable function type is A **function type** describes a value you can call, e.g. `() -> Unit`, `(Int) -> Int`, `(String, Int) -> Boolean`. Adding `?` makes it **nullable**: `(() -> Unit)?` means "either a function taking no args returning Unit, OR null". The parentheses around the function type are required so the `?` applies to the whole function type and not just the return type — `() -> Unit?` is a function returning a nullable `Unit`, which is different. ## Why `onClick()` does not compile In Kotlin, the call syntax `f(args)` is sugar for `f.invoke(args)`. `invoke` is an **operator function** defined on (non-null) function types. On a nullable receiver you cannot call a member/operator directly, so `onClick()` is a compile error: *"Reference has a nullable type '(() -> Unit)?', use explicit '?.invoke()' to make a function-like call instead."* ## Safe ways to call it ```kotlin val onClick: (() -> Unit)? = null // 1. Safe call on invoke — most idiomatic onClick?.invoke() // 2. let onClick?.let { it() } // 3. Null check + smart cast (only val / local, not var properties) if (onClick != null) onClick() // 4. Elvis with a no-op default, then call (onClick ?: {})() ``` - `onClick?.invoke()` evaluates to `Unit?`: `Unit` if called, `null` if skipped. - The smart-cast form `if (onClick != null) onClick()` works only when the compiler can prove the value will not change between the check and the call — true for a local `val` or an immutable property, but **not** for a mutable `var` member property (it could be changed by another thread), where you must use `?.invoke()` or copy into a local first. ## Passing arguments For `val transform: ((Int) -> Int)?`, you pass args inside invoke: `transform?.invoke(5)`.

  • Why does `if (onClick != null) onClick()` fail to compile when `onClick` is a `var` member property?
    Smart casts require the value can't change after the check; a mutable member `var` could be reassigned (e.g. by another thread) between check and call, so the compiler refuses to smart-cast it to non-null. Copy to a local val or use `?.invoke()`.
  • What is the type of the expression `onClick?.invoke()`?
    `Unit?` — it is `Unit` when the function ran and `null` when the receiver was null, because `?.` makes the whole expression nullable.

Like dialing a phone number that might be empty — ?.invoke() checks there's a number before dialing.

saying these in an interview costs you the question

  • Claiming `onClick()` works directly on a nullable function type
  • Saying `?.invoke()` throws if null (it does not — it skips)
  • Forgetting the parentheses distinction `(() -> Unit)?` vs `() -> Unit?`
  • Insisting smart cast works on a mutable `var` member property
  • Calling `onClick!!.invoke()` as the normal/safe approach

context

open as a page

How do you read and write a plain Kotlin function type such as (Int, Int) -> String, and what does () -> Unit mean?

level: juniorimportance: must knowfreq 80%

basics

~20 s

It describes a value that is itself a function. The part in parentheses lists the input types, the arrow points to the result type. () -> Unit means: takes no input and returns nothing useful.

open as a page

What is a receiver function type like String.() -> Unit in Kotlin, and how does it differ from a plain function type (String) -> Unit?

level: juniorimportance: must knowfreq 70%

basics

~20 s

A receiver function type is a lambda that runs 'inside' an object, so you can use this and call its members directly without naming it. A plain function type just takes the object as a normal parameter you must name.

open as a page

What is a function-type typealias in Kotlin, and why would you declare something like `typealias Handler = (Event) -> Unit`?

level: juniorimportance: must knowfreq 55%

basics

~10 s

It gives a short, meaningful name to a function type. Instead of writing (Event) -> Unit everywhere, you write Handler. It is just an alias, so the two names mean exactly the same type.

open as a page

What is a suspend function type such as `suspend () -> T`, and where can a value of that type be invoked?

level: middleimportance: must knowfreq 60%

basics

~20 s

It is the type of a function that can suspend, like a lambda you can pause and resume. You can only call it from inside a coroutine or another suspend function, not from ordinary code.

open as a page

Compare the signatures of apply, run, with, also and let. Which use a receiver function type, which use a plain function type, and how does that change the lambda body?

level: middleimportance: must knowfreq 75%

basics

~10 s

apply, run and with give you this (receiver lambdas); let and also give you it (plain lambdas). With this you call members directly; with it you must name the argument.

open as a page

How do you declare and safely invoke a property that is both nullable and a suspend function, e.g. an optional `suspend () -> Unit` callback?

level: middleimportance: should knowfreq 40%

basics

~10 s

Declare it as (suspend () -> Unit)?. Call it from inside a coroutine with callback?.invoke(), which runs it only if it is not null.

open as a page

What do plain function types like (Int, Int) -> Int compile to on the JVM, and how does that relate to FunctionN interfaces and invoke?

level: middleimportance: should knowfreq 55%

basics

~10 s

A function type is really a regular interface in disguise. (Int, Int) -> Int becomes Function2<Int, Int, Int>, an interface with one method called invoke. Calling the function calls invoke.

open as a page

Why can you both call a function value with f() and write f.invoke()? Explain the role of the invoke operator and where it can show up.

level: middleimportance: should knowfreq 45%

basics

~10 s

Calling something with parentheses is shorthand for calling its invoke method. Function values have invoke, so f() and f.invoke() do the same thing. Any object that defines an invoke operator becomes callable too.

open as a page

Implement a small type-safe builder DSL using a receiver function type (e.g. an html { } or buildString-style builder). Show the function signature and how the lambda gives the body an implicit receiver.

level: middleimportance: should knowfreq 60%

basics

~20 s

Write a builder function that takes a lambda with the builder as its receiver (Builder.() -> Unit), create the builder, run the lambda on it, and return the result. Inside the block users call builder methods directly.

open as a page

How do you write generic and receiver-based function-type typealiases, e.g. `Validator<T>` or an alias for `A.() -> B`? Show the syntax and how generics flow through.

level: middleimportance: should knowfreq 40%

basics

~20 s

Add type parameters after the alias name: typealias Validator<T> = (T) -> Boolean. The T is filled in when you use it, e.g. Validator<String>. You can also alias receiver function types like typealias Block<T> = T.() -> Unit.

open as a page

When would you choose a `typealias` for a function type versus a `fun interface`? What are the practical differences?

level: middleimportance: should knowfreq 50%

basics

~20 s

A typealias just renames a function type — any matching lambda fits. A fun interface is a real, distinct type: lambdas convert to it, but it can have a name, members, and is not interchangeable with other function types. Use the interface when you want a real type or members.

open as a page

Explain why a `suspend () -> T` value cannot be assigned to a `() -> T` variable. What does the compiler actually generate?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Because they are different types under the hood. The compiler turns a suspend function into one that takes an extra hidden parameter to track where to resume, so its shape doesn't match a plain function.

open as a page

Distinguish these function types: () -> Unit, (Unit) -> Unit, and (Int) -> Unit. Why does arity matter and what trips people up with Unit?

level: seniorimportance: should knowfreq 35%

basics

~20 s

() -> Unit takes no arguments. (Unit) -> Unit takes one argument whose type happens to be Unit. (Int) -> Unit takes one Int. The number of parameters (the arity) is part of the type, so these are all different.

open as a page

In a nested receiver-lambda DSL, how does Kotlin resolve a call when multiple implicit receivers are in scope, and how do @DslMarker and labeled this (this@Outer) control that resolution?

level: seniorimportance: should knowfreq 45%

basics

~10 s

When nested receiver lambdas stack, Kotlin tries the innermost receiver first, then outer ones. @DslMarker blocks reaching outer receivers implicitly, and this@Label lets you target a specific one explicitly.

open as a page

A teammate uses `typealias UserId = (String) -> Unit` and `typealias OrderId = (String) -> Unit` expecting the compiler to keep them apart. What actually happens, and how should they get the safety they want?

level: seniorimportance: should knowfreq 35%

basics

~20 s

The compiler treats both as the same type, so it won't keep them apart — you can freely swap them. To get real distinct types, use separate classes, value classes, or fun interfaces instead of aliases.

open as a page

When designing a library API, would you model an optional asynchronous hook as `(suspend () -> Unit)?` or as a non-null `suspend () -> Unit = {}` default? Discuss the trade-offs.

level: principalimportance: nice to knowfreq 20%

basics

~20 s

A default empty lambda is usually cleaner for callers because there is no null to check. Nullable is better when 'no hook' is a meaningful state you must detect or when the lambda allocation matters.

open as a page

Given the FunctionN variance (in parameters, out return), explain when one plain function type is a subtype of another, with an example.

level: principalimportance: nice to knowfreq 22%

basics

~20 s

A function can stand in for another if it accepts at least as wide a range of inputs and returns at least as specific an output. So you can use a function that takes a broader parameter type and returns a narrower result type.

open as a page

At the JVM level, how is a receiver function type A.() -> B represented, and what are the API-design tradeoffs of choosing a receiver lambda over a plain parameter lambda?

level: principalimportance: nice to knowfreq 30%

basics

~20 s

On the JVM a receiver lambda is the same as a plain one-arg function; the receiver is just the first argument. Choosing a receiver lambda makes APIs read like DSLs but can hide which object you're acting on and pollute scope.

open as a page

How do function-type typealiases behave with respect to expansion, overload resolution, error messages, and Java/JVM interop?

level: principalimportance: nice to knowfreq 18%

basics

~20 s

An alias is expanded to its underlying function type early, so the compiler treats it exactly like that type for overloads and equality. It has no presence at runtime — from the JVM/Java side you only see the underlying FunctionN type.

open as a page