Inference

A type argument is inferred from a call's arguments or its expected type, a closure's parameter types follow the same rule, and a fn's own parameter types and a public fn's result are always written out.

Diese Seite gibt es noch nicht auf Deutsch, deshalb steht sie hier auf Englisch. Zur englischen Seite

Inference is local: the compiler works out a type from the one call or the one closure it is written in, never by looking at how a result is used three lines later.

Example#

fn identity<Value>(value: Value): Value {
  value
}

const number = identity 5

const double: (Int) => Int = { x => x * 2 }
print double(number)

fn twice<Value>(value: Value, transform: (Value) => Value): Value {
  transform value
}

const result = twice 5 { x => x * 2 }
print result

Syntax#

identity(5)              Value inferred from the argument
empty<String>()          Item written out: nothing in the call says what it is
into<Set<Employee>>()    the one type argument written, the rest (here, none) inferred
{ x => x * 2 }            parameter type inferred from an expected function type

Rules#

  1. A fn's type arguments are inferred from the arguments of a call, by matching each argument's type against the parameter it fills. identity 5 fills Value with Int because 5 is an Int.

  2. A type argument that cannot be inferred is written out, in the same angle brackets the declaration uses. empty<String>() writes Item because nothing about calling empty with no arguments says what it is.

  3. Type arguments can be given partially, from the left, and the rest is inferred. pair<Int>(5, "x") writes Left and lets Right follow from "x".

  4. A fn's own parameter types are always written out; they are never inferred. Only the result type can be left out, and only for a fn that is not public and not a trait method.

    Kompiliert nicht
    public fn double(value: Int) {
      value * 2
    }
    // error: A `public` function does not infer its result: declare it (`: Int64`)
  5. A closure's parameter types are inferred from an expected function type; without one, they have to be annotated. const double: (Int) => Int = { x => x * 2 } needs no annotation on x; the closure alone does.

    Kompiliert nicht
    const double = { x => x * 2 }
    // error: The parameters of this closure need types: `{ value: Int => ... }`
  6. A closure's return type is always inferred, and cannot be written on the closure itself. To spell it out, annotate the binding the closure is assigned to, or write a local fn instead - which is the same declaration as a top-level one and can be passed by name.

  7. Coercion to a trait type or a literal type never solves an inference variable. It only fires once the expected type is already known some other way, so it cannot be the reason a type argument gets picked.

  8. A type argument that only a bound mentions is read off the one implementation of that trait. In fn render<Model: Live<Message>, Message>(model: Model) the argument decides Model, and where that type implements Live exactly once, its arguments decide Message. A type that implements the trait twice decides nothing, and one that does not implement it at all is reported as such.

    trait Live<Message> {
      fn update(message: Message): Int
    }
    
    type Counter with Live<Int> {
      fn update(message: Int): Int {
        message
      }
    }
    
    fn render<Model: Live<Message>, Message>(model: Model): List<Message> {
      []
    }
    
    const messages = render Counter()
    print messages.length()

What this is not#

Inference is not global. An annotation two lines below a call does not reach back into it - a call is solved against its own arguments and its own expected type, not against how its result gets used afterward.

fn empty<Item>(): List<Item> {
  []
}

const numbers: List<Int> = empty()
print numbers
Kompiliert nicht
fn empty<Item>(): List<Item> {
  []
}

const items = empty()
const numbers: List<Int> = items
print numbers
// error: Cannot infer `Item` of `empty`

const items = empty() has to stand on its own: nothing on that line says what Item is, and the annotation on the next line, numbers, arrives one statement too late to help it.

  • Type parameters - where the type argument being inferred comes from.
  • Bounds - a bound restricts what an inferred type can be, and names the trait rule 8 reads a type argument off.
  • Traits as types - the coercion that never solves an inference variable.
Esc