Distinguish these function types: () -> Unit, (Unit) -> Unit, and (Int) -> Unit. Why does arity matter and what trips people up with Unit?
answer
- Arity = param count = which FunctionN
- () -> Unit is zero args; (Unit) -> Unit is one
- (Unit) -> Unit must be called with f(Unit)
- Unit-return lambdas discard the final value
- 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 sArity — 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 linesfun 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
Recognizes () -> Unit as a no-arg callback but may not catch the (Unit) -> Unit trap.
Explains arity selects FunctionN and that (Unit) -> Unit needs f(Unit); distinguishes the three types.
Articulates Unit-return coercion, overload resolution by arity, and SAM-interop consequences.
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