skip to content

A teammate writes `fun <T> mid(a: T, b: T) where T : Number, T : Comparable<T>` and calls `mid(1, 2)`. It compiles, but `mid(1.0, 2)` does not. Explain why, and what the multi-bound `where` actually requires of the argument types.

level: seniorimportance: should knowfreq 30%

answer

  1. `T : Comparable<T>` = comparable to its OWN type (F-bound)
  2. Multi-bound `where` needs ONE T meeting ALL constraints
  3. `Number` is NOT `Comparable<Number>`
  4. Mixed numeric args -> no self-comparable common supertype
  5. Fix: same-typed args or drop self-comparability

basics

~20 s

T must be one single type that is both a Number and Comparable to itself. mid(1, 2) infers T = Int, which qualifies. mid(1.0, 2) mixes Double and Int, so no single T satisfies both arguments and the bounds, and inference fails.

solid answer

~40 s

The `where` clause forces the compiler to find **one** type `T` that simultaneously satisfies `T : Number` and `T : Comparable<T>` **and** is a common type for all arguments. `Int` is `Comparable<Int>` and a `Number`, so `mid(1, 2)` infers `T = Int`. For `mid(1.0, 2)`, the natural common supertype of `Double` and `Int` is `Number` (or `Comparable<*>`), but `Number` is **not** `Comparable<Number>`, so the `Comparable<T>` bound cannot be satisfied — there is no single concrete `T` that is both arguments' type and self-comparable. Kotlin's numeric types are each `Comparable` only to themselves, not across types, so mixed numeric arguments break self-referential `Comparable<T>` bounds. The fix is to make both arguments the same type, e.g. `mid(1.0, 2.0)`, or to redesign without the self-comparable constraint.

code

kotlin · 8 lines
kotlin
fun <T> mid(a: T, b: T): T where T : Number, T : Comparable<T> =
    if (a <= b) a else b

fun main() {
    println(mid(1, 2))       // Int  -> 1
    println(mid(1.0, 2.0))   // Double -> 1.0
    // println(mid(1.0, 2))  // won't compile: Number is not Comparable<Number>
}

go deeper

for a junior

Recognizes both arguments must end up the same type.

for a middle

Explains inference picks one T and that mixed types lack a common qualifying type.

for a senior

Identifies the F-bounded Comparable<T> self-reference and that Number is not self-comparable, with a correct fix.

for a principal

Generalizes about F-bounded polymorphism, why cross-type ordering is intentionally hard, and API alternatives (Comparator, explicit conversion).

## What the bounds demand `where T : Number, T : Comparable<T>` says the chosen `T` must (1) be a `Number` and (2) implement `Comparable<T>` — i.e. be comparable **to itself**, the same `T`. This self-reference (`Comparable<T>` where the type argument is `T` again) is the crux. ## Why `mid(1, 2)` works Both literals are `Int`. The compiler infers `T = Int`. Check the bounds: - `Int : Number` ✔ - `Int : Comparable<Int>` ✔ (each numeric type is comparable to its own type) So `T = Int` satisfies everything. ## Why `mid(1.0, 2)` fails Arguments are `Double` and `Int`. There is no single concrete `T` equal to both, so the compiler looks for a common supertype. Candidates: - `Number` — but `Number` does **not** implement `Comparable<Number>`. Fails bound (2). - `Comparable<*>` — too loose; cannot satisfy `Comparable<T>` with a concrete self-type. Because Kotlin's `Int`, `Double`, etc. are each `Comparable` **only to their own type** (`Int : Comparable<Int>`, not `Comparable<Number>`), there is no `T` that is both a supertype of `{Double, Int}` and self-comparable. Inference fails with no suitable type for `T`. ```kotlin fun <T> mid(a: T, b: T): T where T : Number, T : Comparable<T> = if (a <= b) a else b mid(1, 2) // T = Int -> OK mid(1.0, 2.0) // T = Double -> OK // mid(1.0, 2) // ERROR: no single T is both arg types AND Comparable<T> ``` ## The general lesson A **self-referential** bound `T : Comparable<T>` (the recursive/F-bounded pattern) means "comparable to exactly its own type." Combined with `Number`, it excludes mixing numeric types, because the only common supertype `Number` is not self-comparable. This is a deliberate consequence of multi-bound `where` requiring **one** type to satisfy **all** constraints at once. ## How to fix - Pass arguments of the **same** type: `mid(1.0, 2.0)`. - Or drop self-comparability and accept a `Comparator<T>`/`Comparable<*>` if cross-type comparison is genuinely needed (rare and lossy). - Or convert explicitly before calling: `mid(1.0, 2.toDouble())`.

  • Why isn't `Number` an acceptable `T` here?
    `Number` satisfies the first bound but not `Comparable<Number>` — `Number` has no `compareTo`. The self-referential `Comparable<T>` bound rejects it.
  • What is the name for the `T : Comparable<T>` pattern?
    A recursive or F-bounded constraint — the bound mentions the parameter itself, pinning comparison to the parameter's own type.

It's like requiring a key that fits both locks at once; two different keys (Double, Int) won't merge into one master key that's also self-comparable.

saying these in an interview costs you the question

  • Saying mixed numeric args work because both are `Number`
  • Not recognizing `Comparable<T>` is self-referential
  • Claiming `Number` implements `Comparable`
  • Suggesting the compiler will auto-widen Int to Double to satisfy the bound

context