skip to content

What are the exact matching rules between an `expect fun` and its `actual fun`? Where do default parameter values and type parameters go?

level: middleimportance: must knowfreq 55%

answer

  1. Match: name + param types + return type
  2. Defaults live ONLY on expect
  3. Type params declared on expect, mirrored on actual
  4. Actual visibility must be same or wider
  5. Param names may differ; positions/types bind

basics

~20 s

The actual function must have the same name, the same parameters and return type, and at least the same visibility. Default values and type parameters are written only on the expect side, not repeated on actual.

solid answer

~40 s

An `actual fun` must structurally match its `expect fun`: identical simple name, the same parameter types in the same order, the same return type, and compatible modifiers. Visibility on the actual must be the **same or more permissive**. Crucially, **default parameter values are declared only on the `expect`** — repeating `= ...` on the actual is a compile error, because the default is part of the common contract. Likewise, **type parameters and their bounds are declared on the expect**, and the actual mirrors them positionally without re-specifying variance/bounds redundantly. Parameter names can differ (they aren't part of the binding) but matching them aids readability and named-argument calls from common code. The compiler does this check per target compilation, so a mismatch in any one target breaks only that target's build.

code

kotlin · 7 lines
kotlin
// commonMain
expect fun retry(times: Int = 3, block: () -> Unit)

// jvmMain — default NOT repeated; compile error if you write times: Int = 3
actual fun retry(times: Int, block: () -> Unit) {
    repeat(times) { block() }
}

go deeper

for a junior

Knows name and types must match but may be fuzzy on defaults/visibility nuances.

for a middle

States the full matching rules including defaults-on-expect and visibility widening.

for a senior

Explains why defaults live on the expect (call-site resolution in common) and per-target checking.

for a principal

Considers API-evolution implications of these rules across many targets and intermediate source sets.

## What "matching" means When the compiler links an `actual fun` to its `expect fun`, it requires a **structural match** so that any call written against the expect signature is valid for the actual implementation. Must match: - **Name** — same simple function name. - **Parameters** — same number, same types, same order. Parameter *names* need not match (the compiler binds by position/type), but keeping them identical lets common code use named arguments consistently. - **Return type** — identical. - **Receiver** — an extension `expect fun T.foo()` needs an `actual fun T.foo()`. ## Defaults belong to the expect only Default argument values are part of the **common contract** because common code calls the function and supplies (or omits) arguments. Therefore the default is written **once, on the expect**: ```kotlin // commonMain expect fun log(message: String, level: Int = 0) // jvmMain — NO default here actual fun log(message: String, level: Int) { println("[$level] $message") } ``` Writing `level: Int = 0` again on the `actual` is a compile error: *"An expected function with default parameter values can only have them on the expect declaration."* ## Type parameters Generic `expect` functions declare their type parameters and bounds; the `actual` repeats the type-parameter list positionally: ```kotlin // commonMain expect fun <T : Comparable<T>> maxOf(a: T, b: T): T // jvmMain actual fun <T : Comparable<T>> maxOf(a: T, b: T): T = if (a >= b) a else b ``` ## Visibility and modifiers The `actual`'s visibility must be **equal to or wider** than the expect's; you cannot narrow `public` to `internal`. Other modifiers (e.g., `inline`, `suspend`, `infix`) must be consistent with the expect. ## Why per-target The expect/actual check runs **inside each target's compilation**. So if `androidMain` matches but `iosMain` has a typo in the parameter type, only the iOS build fails. This is why a green common build doesn't guarantee all platforms compile. ## Recall - Same name, params (by type/position), return type. - Defaults: **expect only**. - Type params: declared on expect, mirrored on actual. - Visibility: actual ≥ expect.

  • Why are default values disallowed on the actual side?
    Defaults are part of the common-facing contract resolved at the call site in common code, so they must be defined once on the expect to avoid ambiguity.
  • Can the actual be `public` when the expect is `internal`?
    Yes — actual visibility may be the same or more permissive, so widening internal to public is allowed; narrowing is not.

saying these in an interview costs you the question

  • Repeating default values on the actual function
  • Saying parameter names must match exactly
  • Claiming the actual can narrow visibility
  • Forgetting that type parameters/bounds are declared on the expect

context