skip to content

You are designing a public API. When should a parameter be a function type (HOF) versus a single-abstract-method interface, and what signature design choices make a HOF parameter pleasant to call?

level: principalimportance: nice to knowfreq 35%

answer

  1. Single op + inline use => function type
  2. Multi-method / named / Java interop => fun interface (SAM)
  3. Put the lambda param last (trailing-lambda)
  4. Receiver type T.()->R for DSL/builders
  5. Avoid two adjacent function params

basics

~20 s

Use a function-type parameter for simple one-shot callbacks so callers pass a quick lambda. Use a named interface when the callback has several methods, needs documentation, or must be implemented/reused. Put the lambda last so trailing-lambda syntax works.

solid answer

~40 s

Prefer a function-type parameter (HOF) when the callback is a single operation that callers usually supply inline; it gives the cleanest call site, supports references, defaults, and trailing-lambda syntax. Prefer a named SAM/functional interface (fun interface) when: the callback has multiple operations, you want a documented/discoverable type, you need named instances, Java interop matters (Java sees a clean interface and SAM conversion), or you want to attach state/identity. Design tips: place the function-type param last for trailing-lambda; consider a receiver type (T.() -> R) for builder/scope ergonomics; give it a sensible default when optional; name arrow parameters for IDE hints; mark the HOF inline if it is small and hot (and to allow non-local return). Avoid multiple adjacent function-type params (ambiguous call sites) and overly generic () -> Unit grab-bags.

code

kotlin · 10 lines
kotlin
// fun interface gives a named, Java-friendly, SAM-convertible type
fun interface Validator { fun isValid(input: String): Boolean }

fun firstInvalid(inputs: List<String>, v: Validator): String? =
    inputs.firstOrNull { !v.isValid(it) }

fun main() {
    // SAM conversion: lambda becomes a Validator
    println(firstInvalid(listOf("ok", ""), Validator { it.isNotEmpty() }))
}

go deeper

for a junior

Knows a lambda param should go last and that simple callbacks use function types.

for a middle

Distinguishes function type vs interface and applies defaults and trailing-lambda placement.

for a senior

Chooses fun interface vs function type deliberately, uses receiver types and SAM conversion appropriately.

for a principal

Sets API-design conventions weighing ergonomics, Java interop, evolvability, and performance across a library surface.

## Function type vs named interface Both model 'pass behavior in.' Choose based on shape and audience. **Use a function type `(In) -> Out` when:** - the callback is a **single operation**, - callers typically pass an **inline lambda or reference**, - you want the **lightest** call site and stdlib-style ergonomics (`map`, `filter`). **Use a named (functional) interface when:** - the abstraction has **multiple methods** or needs to carry **state/identity**, - you want a **documented, discoverable** type (`interface RetryPolicy { ... }`), - **Java interop** matters: a Kotlin `fun interface` (a SAM/Single-Abstract-Method interface) is implementable from Java with a lambda via **SAM conversion**, and reads as a normal interface, - implementations are **reused/named** rather than written inline each time. ```kotlin // HOF: cleanest for one-shot behavior fun retry(times: Int, block: () -> Unit) { /* ... */ } // fun interface: named, documented, Java-friendly, allows SAM conversion fun interface RetryPolicy { fun shouldRetry(attempt: Int, error: Throwable): Boolean } fun retry(policy: RetryPolicy, block: () -> Unit) { /* ... */ } ``` Kotlin can **SAM-convert** a lambda to a `fun interface`, so `RetryPolicy { a, e -> a < 3 }` works — giving you interface benefits with lambda call sites. ## Signature ergonomics for HOF params - **Last position:** put the function-type parameter last so callers use trailing-lambda syntax: `retry(3) { ... }`. - **Avoid multiple trailing function params:** only the last can be a trailing lambda; two adjacent function params create awkward call sites — prefer a config object or a `fun interface`. - **Receiver types for builders:** `T.() -> Unit` gives `this`-based DSLs (`apply`, `buildList`). Use when the lambda should operate *on* a contextual object. - **Defaults:** make optional callbacks omit-able: `onError: (Throwable) -> Unit = {}`. - **Name arrow params** for IDE/readability: `(index: Int, value: T) -> Unit`. - **Nullability:** model truly-optional hooks as `((T) -> Unit)? = null` only if absence is semantically different from a no-op default. - **inline** small/hot HOFs to remove allocation and permit non-local return; use `noinline`/`crossinline` as needed. ## Tradeoffs to weigh - Function types keep call sites terse but are **anonymous** — harder to document or evolve (adding a parameter is a breaking change either way, but interfaces can add default methods). - Interfaces are **heavier to declare** but **self-documenting**, support **multiple operations**, and interoperate cleanly with Java. - For very hot paths, `inline` HOFs beat interface dispatch; for stable cross-language contracts, `fun interface` is safer. ## Anti-patterns - A god-callback `(Any) -> Any` that erases intent. - Many positional lambda params instead of a builder/receiver DSL. - Returning raw function types from public APIs where a named type would document intent better.

  • What does fun interface add over a plain function type?
    A named, documented type that supports SAM conversion from lambdas, clean Java interop, identity/state, and can later grow default methods — while still callable with a lambda.
  • Why put the function-type parameter last?
    Only the last parameter qualifies for trailing-lambda syntax, which produces the cleanest call site (retry(3) { ... }).
  • When are two function-type params acceptable?
    Rarely; only the last gets trailing-lambda. Prefer a receiver-based DSL, defaults, or a config/fun-interface object to keep call sites readable.

A function type is a sticky note (quick, anonymous); a fun interface is a labeled form (documented, reusable, multi-field).

saying these in an interview costs you the question

  • Always using interfaces even for one-shot lambdas (boilerplate)
  • Placing the lambda param before others, blocking trailing-lambda syntax
  • Two adjacent function-type params with no thought to call-site readability
  • Ignoring Java interop/SAM conversion when designing a cross-language API
  • Exposing anonymous function types where a documented named type is warranted

context