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
Tis only in its result, likeZero, 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 inferappears, read the arguments, not the assignment. Anilargument says nothing about its type (the kata); a[]anyfixesTtoany, and a later[]intargument 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.
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 pinsTdown. Go's reads one call's arguments and stops. The spelling is the same idea in different brackets: Rust's turbofishparse::<i32>()is Go'sZero[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, asZero[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¶
- The Go specification: Type inference ↗, Type unification ↗, Instantiations ↗ (partial type argument lists) and Constants ↗ (default types).
- Go 1.21 release notes, Changes to the language ↗ — generic functions as arguments, assignment to function-typed variables, untyped constants of different kinds.
- Robert Griesemer, Everything You Always Wanted to Know About Type Inference - And a Little Bit More ↗, the Go blog, 9 October 2023.
- The Go FAQ: Why can't the compiler infer the type argument in my program? ↗.
cmp.Compare↗ andslices.SortFunc↗.- Jon Bodner, Learning Go, 2nd ed. (O'Reilly, 2024), chapter 8, "Generics" — the sections "Type Inference and Generics" and "Type Elements Limit Constants".