skip to content

When calling a higher-order function, what are the different ways to pass the function argument (lambda, reference, stored value), and how do nullable and receiver function types appear in a HOF signature?

level: middleimportance: should knowfreq 60%

answer

  1. Forms: lambda, ::reference, stored value, anonymous fun
  2. Nullable: ((Int)->Int)? then f?.invoke(x)
  3. (Int)->Int? returns nullable, not nullable function
  4. Receiver type: Receiver.()->R gives implicit this
  5. Function params can have defaults

basics

~10 s

You can pass a lambda, a function reference like ::name or obj::method, or a stored function value. A function-type parameter can be nullable, like ((Int)->Int)?, and can declare a receiver, like Int.()->String.

solid answer

~40 s

Three calling forms feed a HOF: a lambda ({ it + 1 } or { a, b -> a + b }), a callable reference (::topLevel, String::length, instance::method, or even Class::constructor via ::ClassName), and a stored value of the function type. Function-type parameters can be nullable: f: ((Int) -> Int)? = null, invoked safely with f?.invoke(x). They can carry a receiver: transform: Int.() -> String, where inside the lambda the receiver is this and at the call site you write number.transform() style — this is what makes scope functions and DSLs (apply, with, buildString) work. The arrow type can also name parameters for documentation: (index: Int, value: T) -> Unit. Default values for function-type params let callers omit the lambda entirely.

code

kotlin · 10 lines
kotlin
fun process(items: List<String>, transform: (String) -> Int): List<Int> =
    items.map(transform)

fun main() {
    val xs = listOf("a", "bb", "ccc")
    println(process(xs) { it.length })   // lambda
    println(process(xs, String::length)) // unbound member reference
    val len: (String) -> Int = { it.length }
    println(process(xs, len))            // stored value
}

go deeper

for a junior

Passes a lambda and knows it/named-param syntax.

for a middle

Uses references and stored values, handles nullable function params correctly.

for a senior

Designs receiver-typed parameters for DSLs and chooses the clearest passing form per API.

for a principal

Sets API conventions (defaults, receiver vs plain, reference-friendly signatures) for library ergonomics.

## Three ways to supply the function argument Given `fun apply(x: Int, f: (Int) -> Int): Int = f(x)`: 1. **Lambda** — an anonymous function literal. Single param is implicitly `it`; multiple params are named: `{ a, b -> a + b }`. Trailing-lambda syntax applies when it is the last param: `apply(5) { it + 1 }`. 2. **Callable reference** — `::name` for a top-level/local function, `Type::member` for an unbound member (`String::length` has type `(String) -> Int`), `instance::member` for a bound one, and `::ClassName` for a constructor reference. 3. **Stored value** — any `val`/`var` whose type matches: `val g: (Int) -> Int = { it * 2 }; apply(5, g)`. There is also the **anonymous function** form `fun(x: Int): Int { return x + 1 }`, useful when you need an explicit return type or a non-local-free `return`. ## Nullable function types A parameter can itself be nullable; wrap the type in parentheses before `?`: ```kotlin fun run(x: Int, f: ((Int) -> Int)? = null): Int = f?.invoke(x) ?: x // safe call via invoke ``` Note `(Int) -> Int?` is different — that is a non-null function returning a nullable `Int`. Parentheses disambiguate. ## Function types with a receiver A **receiver function type** `Receiver.(Params) -> Return` makes the lambda body have an implicit `this` of type `Receiver`: ```kotlin fun buildGreeting(block: StringBuilder.() -> Unit): String { val sb = StringBuilder() sb.block() // call with sb as receiver return sb.toString() } buildGreeting { append("Hi"); append("!") } // 'this' is the StringBuilder ``` This is exactly how `apply`, `with`, `run`, `buildString`, and type-safe DSL builders are declared. ## Named params and defaults Arrow types may name parameters for readability: `onEach: (index: Int, value: T) -> Unit`. A function-type parameter can have a **default value**, letting callers omit it: `fun loop(n: Int, body: (Int) -> Unit = {})`. ## Choosing a form - Reference (`String::length`) is cleanest when an existing function already matches. - Lambda is best for inline, short logic. - Stored value when the same behavior is reused or chosen at runtime.

  • What is the type of String::length?
    An unbound reference of type (String) -> Int; the receiver becomes the first parameter.
  • Why parenthesize a nullable function type?
    ((Int)->Int)? is a nullable function; (Int)->Int? is a non-null function returning Int?. The parentheses pick which part is nullable.

saying these in an interview costs you the question

  • Confusing ((Int)->Int)? with (Int)->Int?
  • Not knowing function references (::name, Type::member) are valid HOF arguments
  • Calling a nullable function param with f(x) instead of f?.invoke(x)
  • Unaware that receiver function types power apply/with/DSLs

context