Skip to content

Inference works from arguments, not results

Level: 301 · anyone who met cannot infer T on a call that looked obvious

One line: Type inference reads the types of a call's arguments — since Go 1.21 that includes a generic function passed as an argument, or assigned to a variable of function type — and never the type a result is assigned to; a type parameter that appears only in the result is instantiated by hand, Zero[int](), a function value needs every type argument written out, and inside a call a partial Map[int](…) leaves the rest to inference.

Seven calls to three small generic functions: Map[T, U], whose two parameters are both in its arguments; Identity[T], passed around as a value; and Zero[T], whose only T is in its result.

Verified output of inference_works_from_arguments_not_results_go.go — regenerated by tools/run_examples.py, never hand-typed.

1. [PEAR APPLE]
2. [pear apple]
3. kept
4. "" 0 true
5. [7 12]
5. [7 12]
6. int float64 float64
7. [apple pear]

Reading the output

1 — from the arguments. Map(names, strings.ToUpper): T is string because names is a []string, and U is string because ToUpper is a func(string) string. The spec ↗ describes this as a set of type equations, one per typed argument, solved by unification ↗; Robert Griesemer's blog post ↗ walks through the same call shapes.

2 and 3 — Go 1.21 reaches into a generic argument. Map(names, Identity) passes Identity with no type argument, and the compiler infers Identity's T from Map's parameter type func(T) U once Map's own T is known. The Go 1.21 release notes ↗ added this: a call may take arguments that are themselves generic functions, and their missing type arguments are inferred too. Line 3 is the other 1.21 addition — a generic function assigned to a variable of function type, var keep func(string) string = Identity, is inferred from that type. This is the one place where the target of an assignment counts, and it counts because the target is a function type that can be unified with Identity's signature.

4 — a result alone is not enough. Zero[T]() T has no argument that mentions T, so the call names it: Zero[string](), Zero[int](), Zero[[]int](). Assigning the result to an int variable does not help — the driver below records the refusal. The FAQ ↗ says the limit is deliberate: inference that reached across statements would make it hard to see, at the call, which type was chosen.

5 — a value needs every type argument, a call needs only the missing ones. toLabel := Map[int, string] makes a concrete function value: both arguments written. Inside a call, Map[int]([]int{7, 12}, strconv.Itoa) writes only T, and U comes from Itoa — the spec's Instantiations ↗ allows a partial list when the rest can be inferred from the call.

6 — untyped constants take their default type, or the wider kind. Twice(3) is an int and Twice(3.5) a float64: an untyped constant contributes its default type ↗. Larger(3, 2.5) is a float64, because since 1.21 two untyped constants of different kinds bound to the same type parameter are resolved the way 3 + 2.5 is, to the kind that holds both; before 1.21 that call was an error (from the release notes; not machine-checked here).

7 — the standard library's case. slices.SortFunc(names, cmp.Compare) passes the generic cmp.Compare ↗ as an argument and lets SortFunc instantiate it from the slice's element type — the 1.21 change again, and the reason the cmp and slices packages read as they do.

Where there is nothing to read

Verified output of inference_from_a_result_sh.sh — regenerated by tools/run_examples.py, never hand-typed.

$ go build from_result.go
# command-line-arguments
./from_result.go:19:22: in call to Zero, cannot infer T (declared at ./from_result.go:5:11)
./from_result.go:20:13: cannot use generic function Map without instantiation
./from_result.go:21:16: cannot infer U (declared at ./from_result.go:10:13)
exit status 1

var count int = Zero() — the int on the left is not consulted, and the message names the parameter it could not fill and where it was declared. toLabel := Map — a generic function is not a value; cannot use generic function Map without instantiation. fromInt := Map[int] — a partial instantiation is still generic, and with no call and no typed target, U has nothing to come from.

inference_works_from_arguments_not_results_go.go and inference_from_a_result_sh.sh

inference_works_from_arguments_not_results_go.go in full — pasted here by tools/run_examples.py from the file CI runs.

// Type inference reads the arguments of a call. It does not read what the
// result is assigned to, so a function whose type parameter appears only in
// its result is instantiated by hand: Zero[int](). Since Go 1.21 a generic
// function passed as an argument, or assigned to a variable of function type,
// is inferred too.
//
//  go build inference_works_from_arguments_not_results_go.go && ./inference_works_from_arguments_not_results_go
package main

import (
    "cmp"
    "fmt"
    "slices"
    "strconv"
    "strings"
)

func Map[T, U any](in []T, f func(T) U) []U {
    out := make([]U, 0, len(in))
    for _, v := range in {
        out = append(out, f(v))
    }
    return out
}

func Identity[T any](v T) T { return v }

// Zero's T appears in the result only: no argument can name it.
func Zero[T any]() T {
    var zero T
    return zero
}

func Twice[T ~int | ~float64](v T) T { return v * 2 }

func Larger[T ~int | ~float64](a, b T) T {
    if a > b {
        return a
    }
    return b
}

func main() {
    names := []string{"pear", "apple"}

    // 1. From the arguments: T = string from names, U = string from ToUpper.
    fmt.Println("1.", Map(names, strings.ToUpper))

    // 2. Go 1.21: a generic function passed as an argument is inferred as well.
    fmt.Println("2.", Map(names, Identity))

    // 3. Go 1.21: assigned to a variable of function type, Identity is inferred
    // from that type.
    var keep func(string) string = Identity
    fmt.Println("3.", keep("kept"))

    // 4. Nothing in Zero's arguments names T: instantiate it explicitly.
    fmt.Printf("4. %q %d %v\n", Zero[string](), Zero[int](), Zero[[]int]() == nil)

    // 5. A function value needs the full instantiation ...
    toLabel := Map[int, string]
    fmt.Println("5.", toLabel([]int{7, 12}, strconv.Itoa))
    // ... but inside a call a partial one is enough; U comes from Itoa.
    fmt.Println("5.", Map[int]([]int{7, 12}, strconv.Itoa))

    // 6. Untyped constants: the default type, or, since 1.21, the kind that
    // holds both, as 3 + 2.5 would.
    fmt.Printf("6. %T %T %T\n", Twice(3), Twice(3.5), Larger(3, 2.5))

    // 7. The standard library's own case: cmp.Compare is generic, and
    // slices.SortFunc instantiates it from the slice's element type.
    slices.SortFunc(names, cmp.Compare)
    fmt.Println("7.", names)
}

inference_from_a_result_sh.sh in full — pasted here by tools/run_examples.py from the file CI runs.

#!/usr/bin/env bash
# Three places where inference has nothing to read: a type parameter that
# appears only in the result, a generic function assigned to a variable with
# no type, and a partial instantiation assigned the same way.
set -u
export GOTOOLCHAIN=local

dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT

cat >"$dir/from_result.go" <<'GO'
package main

import "fmt"

func Zero[T any]() T {
    var zero T
    return zero
}

func Map[T, U any](in []T, f func(T) U) []U {
    out := make([]U, 0, len(in))
    for _, v := range in {
        out = append(out, f(v))
    }
    return out
}

func main() {
    var count int = Zero()
    toLabel := Map
    fromInt := Map[int]
    fmt.Println(count, toLabel, fromInt)
}
GO

say() { printf '$ %s\n' "$*"; }

say 'go build from_result.go'
(cd "$dir" && go build from_result.go) 2>&1
echo "exit status $?"

What to do

  • Let the arguments carry the type parameters where you can. A function whose T is only in its result, like Zero, is instantiated by hand at every call — sometimes right, never convenient.
  • On a function value, write every type argument; in a call, write only what the arguments cannot supply — Map[int](values, strconv.Itoa) — and let inference do the rest.
  • When cannot infer appears, read the arguments, not the assignment. A nil argument says nothing about its type (the kata); a []any fixes T to any, and a later []int argument does not widen it (the compiler errors page).
  • Pass generic functions as arguments — slices.SortFunc(names, cmp.Compare) — rather than instantiating them at the call; on Go 1.21 or later the compiler does it.

Practice

Write Reduce[T, U any](values []T, start U, f func(U, T) U) U, which folds a slice into a summary of another type. Call it three ways: fully inferred, summing a []int of quantities into an int (the untyped 0 gives U its default type); as a function value Reduce[string, string] with both type arguments written, joining names; and explicitly instantiated as Reduce[int, []string], because passing nil as the start value says nothing about U. Print each result, with %T on the first to show what 0 became.

Solution

inference_works_from_arguments_not_results_kata_go.go in full — pasted here by tools/run_examples.py from the file CI runs.

// Kata: a Reduce[T, U] called three ways -- fully inferred from its arguments,
// as a function value with both type arguments written out, and explicitly
// instantiated because a nil argument says nothing about U.
//
//  go build inference_works_from_arguments_not_results_kata_go.go && ./inference_works_from_arguments_not_results_kata_go
package main

import (
    "fmt"
    "strconv"
)

func Reduce[T, U any](values []T, start U, f func(U, T) U) U {
    acc := start
    for _, v := range values {
        acc = f(acc, v)
    }
    return acc
}

func main() {
    quantities := []int{4, 7, 10}

    // T from quantities, U from the untyped constant 0 (default type int).
    total := Reduce(quantities, 0, func(acc, q int) int { return acc + q })
    fmt.Printf("inferred:     total %d (%T)\n", total, total)

    // A function value: every type argument spelled out.
    join := Reduce[string, string]
    names := []string{"pear", "apple"}
    fmt.Printf("as a value:   %q\n", join(names, "", func(acc, n string) string { return acc + n + ";" }))

    // nil could be a []string, a map, a pointer... so U is written.
    labels := Reduce[int, []string](quantities, nil, func(acc []string, q int) []string {
        return append(acc, "qty-"+strconv.Itoa(q))
    })
    fmt.Printf("instantiated: %q\n", labels)
}

Verified output of inference_works_from_arguments_not_results_kata_go.go — regenerated by tools/run_examples.py, never hand-typed.

inferred:     total 21 (int)
as a value:   "pear;apple;"
instantiated: ["qty-4" "qty-7" "qty-10"]

In other languages

  • Rust — When the compiler cannot infer ↗: Rust's inference reads the whole function body, including what a value is assigned to and how it is used later, so its failure — type annotations needed — means nothing anywhere pins T down. Go's reads one call's arguments and stops. The spelling is the same idea in different brackets: Rust's turbofish parse::<i32>() is Go's Zero[int]().
  • C++ — template argument deduction works from the function arguments and never from the return type (cppreference ↗), which is Go's rule exactly: make_zero<int>() is written out, as Zero[int]() is.
  • Java — infers from the target type as well: List<String> names = Collections.emptyList() works because the call is a poly expression (the Java tutorial ↗). In Go, var count int = Zero() is the refusal in the driver above.

Sources