skip to content

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%

answer

  1. Arity = param count = which FunctionN
  2. () -> Unit is zero args; (Unit) -> Unit is one
  3. (Unit) -> Unit must be called with f(Unit)
  4. Unit-return lambdas discard the final value
  5. Different arity = different, incompatible type

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.

solid answer

~40 s

Arity — the parameter count — is baked into the function type and selects which FunctionN interface backs it. () -> Unit is Function0<Unit>: zero parameters. (Unit) -> Unit is Function1<Unit, Unit>: one parameter that must be passed the Unit value explicitly, e.g. f(Unit). (Int) -> Unit is Function1<Int, Unit>: one Int parameter. The classic trap is accidentally writing (Unit) -> Unit when you meant () -> Unit; the compiler then expects you to supply Unit as an argument. A related subtlety: a lambda assigned to a Unit-returning type may end on any expression — its value is discarded (Unit coercion), so { x -> compute(x) } satisfies (Int) -> Unit even if compute returns Int. These distinctions matter for callback APIs and for SAM conversion targets.

code

kotlin · 9 lines
kotlin
fun runZero(f: () -> Unit) = f()
fun runOne(f: (Unit) -> Unit) = f(Unit)

runZero { println("no args") }
runOne  { u -> println("got $u") }   // prints: got kotlin.Unit

// Unit-return coercion: final Int is discarded, still type-checks
val cb: (Int) -> Unit = { n -> n + 1 }
cb(10)

go deeper

for a junior

Recognizes () -> Unit as a no-arg callback but may not catch the (Unit) -> Unit trap.

for a middle

Explains arity selects FunctionN and that (Unit) -> Unit needs f(Unit); distinguishes the three types.

for a senior

Articulates Unit-return coercion, overload resolution by arity, and SAM-interop consequences.

for a principal

Considers API ergonomics and footgun-avoidance in public callback signatures and how arity affects overload sets and source compatibility.

## Arity is part of the type The **arity** of a function type is how many parameters it declares. It selects the backing interface (`Function0`, `Function1`, …), so two function types with different arities are **never** the same type and are not assignment-compatible. ### The three cases ```kotlin val a: () -> Unit = { println("hi") } // Function0<Unit>: zero params val b: (Unit) -> Unit = { u -> println(u) } // Function1<Unit, Unit>: ONE param of type Unit val c: (Int) -> Unit = { n -> println(n) } // Function1<Int, Unit>: one Int param ``` Calling them: ```kotlin a() // ok b(Unit) // must pass the Unit value! c(42) // pass an Int ``` Writing `b()` is a compile error because `b` expects one argument. ## Why (Unit) -> Unit is a footgun `Unit` is a real type with a single value, also named `Unit`. `(Unit) -> Unit` therefore declares a parameter that must receive that value. People type the extra parentheses by accident — e.g. intending a no-arg callback `() -> Unit` — and then get confusing errors at the call site ("too few arguments") or when passing it to an API that calls it with no args. ## Unit coercion in the body For a function type whose **return** type is `Unit`, the lambda body's final expression value is simply **discarded** — the compiler coerces it to `Unit`. So: ```kotlin val handler: (Int) -> Unit = { n -> n * 2 } // final Int is discarded; type is fine ``` This is convenient for callbacks but means a non-Unit final expression won't cause a type error here. ## Practical implications - **Callback design**: prefer `() -> Unit` for no-arg side effects; reserve a real parameter type when you actually pass data. - **Higher-order overloads**: `(T) -> Unit` vs `() -> Unit` are distinct overloads; the compiler picks by arity and argument types. - **SAM interop**: a Java `Runnable` SAM-converts from `() -> Unit`, while a `Consumer<Int>` matches `(Int) -> Unit`. ## Summary Arity is structural and non-negotiable; `Unit` as a parameter is legal but almost never what you want; and a Unit return discards the body's value.

  • Can you pass a () -> Unit where a (Unit) -> Unit is expected?
    No. They have different arities (Function0 vs Function1), so they are unrelated types and the assignment fails to compile.
  • Why does { n -> n * 2 } satisfy (Int) -> Unit even though n * 2 is an Int?
    When the expected return type is Unit, the lambda's last-expression value is discarded and coerced to Unit, so the Int result is simply ignored.

saying these in an interview costs you the question

  • Treating () -> Unit and (Unit) -> Unit as the same type
  • Thinking Unit is null or has no value
  • Claiming arity can be widened/narrowed implicitly
  • Believing a Unit-returning lambda must end in a Unit expression

context