Type unification
…
Unification uses a combination of exact and loose unification depending on whether two types have to be identical, assignment-compatible, or only structurally equal. The respective type unification rules are spelled out in detail in the Appendix.
The precise definitions of “exact” and “loose” unification are buried in the appendix, and depend on the specific types involved.
In general, I think it’s not terribly inaccurate to say that exact unification applies when the two types are identical, for composite types with identical structure (i.e. two structs or slices) and the elements of said type are identical, and other similar cases.
Loose unification is a bit more forgiving in a few situations.
You shouldn’t need to care about these details when writing code, really, but if you’re curious, the appendix explains it all.
For an equation of the form
X ≡
A
Y
, whereX
andY
are types involved in an assignment (including parameter passing and return statements), the top-level type structures may unify loosely but element types must unify exactly, matching the rules for assignments.
So if we’re trying to unify []struct{ Foo P }
and []struct{ Foo string}
, the top-level type structures (i.e. []struct{ Foo _ }
and []struct{ Foo _ }
) may loosely unify, but the element types (the types of Foo
and Foo
) must unify exactly.
For an equation of the form
P ≡
C
C
, whereP
is a type parameter andC
its corresponding constraint, the unification rules are bit more complicated:
- If
C
has a core typecore(C)
andP
has a known type argumentA
,core(C)
andA
must unify loosely. IfP
does not have a known type argument andC
contains exactly one type termT
that is not an underlying (tilde) type, unification adds the mappingP ➞ T
to the map.- If
C
does not have a core type andP
has a known type argumentA
,A
must have all methods ofC
, if any, and corresponding method types must unify exactly.
Man. I hope there’s not a quiz after this one.
So a (slightly simplified) refresher on core types:
-
Each non-interface type has a core type, which is the same as its underlying type.
For example, given
type Name string
,Name
’s underlying type and core type isstring
. -
An interface has a core type if there is a single type which is the underlying type of all types in its typeset.
For example, given an interface type of
interface { int }
has a core type ofint
, whileinterface { int | float64 }
has no core type.
Now with this knowledge freshly in mind, we have three scenarios:
C
has a core type, andP
has a known type argument: OK if they unify looselyC
has a core type, andP
does not have a known type argument andC
contains one non-underlying type term: OKC
does not have a core type, andP
has a known type argument: OK if method types unify exactly
When solving type equations from type constraints, solving one equation may infer additional type arguments, which in turn may enable solving other equations that depend on those type arguments. Type inference repeats type unification as long as new type arguments are inferred.
Man.
I hope I never have to care about this again!
Let’s talk about something more directly useful starting tomorrow: Operators!
Quotes from The Go Programming Language Specification Version of August 2, 2023