skip to content

Given `class Box(val n: Int) { fun scaled(f: Int) = n * f }`, what are the function types of `Box::scaled` and `box::scaled`, and how do you invoke each?

level: middleimportance: must knowfreq 45%

answer

  1. Unbound arity = declared params + 1 (receiver)
  2. Box::scaled -> (Box, Int) -> Int
  3. box::scaled -> (Int) -> Int
  4. Bound fits a single-object callback slot
  5. Mismatched form -> Type mismatch on the receiver param

basics

~10 s

Box::scaled needs both the box and the factor, so its type is (Box, Int) -> Int. box::scaled already has the box, so it only needs the factor: (Int) -> Int.

solid answer

~40 s

`Box::scaled` is unbound: the receiver `Box` becomes a leading parameter, giving `(Box, Int) -> Int`. You invoke it as `Box::scaled(box, 3)` (or pass it where a two-arg function is expected). `box::scaled` is bound to that specific instance, so the receiver drops out and the type is `(Int) -> Int`; you invoke it as `box::scaled(3)`. The general rule: unbound arity equals the method's declared parameter count plus one (for the receiver); bound arity equals exactly the declared parameter count. This is why `box::scaled` plugs into a `(Int) -> Int` slot while `Box::scaled` plugs into `(Box, Int) -> Int`. The bound form is the natural choice for a callback tied to one object; the unbound form is what you hand to a higher-order function iterating over `Box` instances.

code

kotlin · 6 lines
kotlin
class Box(val n: Int) { fun scaled(f: Int) = n * f }
val box = Box(10)
val unbound: (Box, Int) -> Int = Box::scaled
val bound: (Int) -> Int = box::scaled
println(unbound(box, 3))  // 30
println(bound(3))         // 30

go deeper

for a junior

Identifies that the bound form drops the box and the unbound form keeps it.

for a middle

Writes both exact function types and invokes each correctly, including in a higher-order call.

for a senior

Predicts the precise type-mismatch error and chooses the right form for a given callback signature, including extension receivers.

for a principal

Generalizes the +1 receiver rule across dispatch and extension receivers and reasons about how it interacts with SAM conversion and overload sets.

## The mechanism A member function has a hidden receiver plus its declared parameters. A callable reference can either *capture* that receiver (bound) or *expose* it as a parameter (unbound). `scaled` declares one parameter (`f: Int`) and returns `Int`. ## Unbound: `Box::scaled` No instance is captured, so the receiver `Box` is promoted to the first parameter: ```kotlin val unbound: (Box, Int) -> Int = Box::scaled val box = Box(10) println(unbound(box, 3)) // 30 -> 10 * 3 ``` Type: `(Box, Int) -> Int`. Arity = declared params (1) + receiver (1) = 2. ## Bound: `box::scaled` The instance `box` is captured at reference-creation time; only the declared parameter remains: ```kotlin val bound: (Int) -> Int = box::scaled println(bound(3)) // 30 ``` Type: `(Int) -> Int`. Arity = declared params (1) = 1. ## Putting them in higher-order functions ```kotlin val boxes = listOf(Box(1), Box(2), Box(3)) // unbound fits a function that receives each Box as receiver boxes.map { b -> b.scaled(2) } // or boxes.map(...) with a lambda // bound is great for a single object's behavior as a callback fun apply(op: (Int) -> Int) = op(5) apply(box::scaled) // 50 ``` ## Receiver of an extension function For an extension `fun Box.doubled() = scaled(2)`, the unbound reference `Box::doubled` is still `(Box) -> Int` — the extension receiver behaves like the dispatch receiver for reference arity purposes. ## Why it matters Matching the expected function type is purely an arity/receiver question. If a callback slot is `(Int) -> Int`, only the **bound** `box::scaled` fits; if it is `(Box, Int) -> Int`, only the **unbound** `Box::scaled` fits. Mixing them up is a common compile error: `Type mismatch: expected (Int) -> Int, found (Box, Int) -> Int`.

  • If a higher-order function expects `(Int) -> Int`, which reference compiles, and what error does the other produce?
    `box::scaled` compiles. `Box::scaled` is `(Box, Int) -> Int` and fails with a type mismatch because of the extra receiver parameter.
  • Does adding a parameter to `scaled` change both arities the same way?
    Yes. Both forms grow by one; bound becomes `(Int, X) -> Int` and unbound becomes `(Box, Int, X) -> Int` — the +1 receiver offset is preserved.

saying these in an interview costs you the question

  • Saying `Box::scaled` is `(Int) -> Int`
  • Saying `box::scaled` is `(Box, Int) -> Int`
  • Forgetting the receiver counts toward unbound arity
  • Claiming you must still pass `box` to a bound reference
  • Assuming the return type changes between forms

context