Instantiations

December 28, 2023

Instantiations

A generic function or type is instantiated by substituting type arguments for the type parameters. Instantiation proceeds in two steps:

  1. Each type argument is substituted for its corresponding type parameter in the generic declaration. This substitution happens across the entire function or type declaration, including the type parameter list itself and any types in that list. After substitution, each type argument must satisfy the constraint (instantiated, if necessary) of the corresponding type parameter. Otherwise instantiation fails.
  2. Instantiating a type results in a new non-generic named type; instantiating a function produces a new non-generic function.
type parameter list    type arguments    after substitution

[P any]                int               int satisfies any
[S ~[]E, E any]        []int, int        []int satisfies ~[]int, int satisfies any
[P io.Writer]          string            illegal: string doesn't satisfy io.Writer
[P comparable]         any               any satisfies (but does not implement) comparable

Earlier this week, I took advantage of these rules to add some compile-time safety to what would otherwise be a runtime panic. The code I was working on uses the Merge function from the dario.cat/mergo library. Here’s what the function does (with a key sentence emphasized):

Merge will fill any empty for value type attributes on the dst struct using corresponding src attributes if they themselves are not empty. dst and src must be valid same-type structs and dst must be a pointer to struct. It won’t merge unexported (private) fields and will do recursively any exported field.

This library was written long before generics existed, so I added a small wrapper function:

func Merge[S any](dst *S, src S) error {
	return errors.WithStack(mergo.Merge(dst, src))
}

Why? What does this seemingly unused S type parameter provide?

Well, I had just seen my program crash after an intended refactoring. What had been a valid call: Merge(&dst, src), had stopped working when I made a change to the type of src. But my compiler (and thus IDE) didn’t know about it.

So I added the wrapper with a type parameter to effectively provide some compile-time safety, which would cause the instantiation to fail according to the above rules if I made the same mistake again. Now the function signature enforces two key parts of the emphasized sentence above: “dst and src must be valid same-type” and “dst must be a pointer”. Notably it does not enforce that dst and src are structs. There’s no way to express that rule using Go generics. But even without that, this new compile-time check easily pointed out where I had failed to update the Merge call after changing src’s type in a few more functions during my refactoring.

Quotes from The Go Programming Language Specification Version of August 2, 2023


Share this

Direct to your inbox, daily. I respect your privacy .

Unsure? Browse the archive .

Related Content


Partial type argument lists

Instantiations … A partial type argument list cannot be empty; at least the first argument must be present. The list is a prefix of the full list of type arguments, leaving the remaining arguments to be inferred. Loosely speaking, type arguments may be omitted from “right to left”. func apply[S ~[]E, E any](s S, f func(E) E) S { … } f0 := apply[] // illegal: type argument list cannot be empty f1 := apply[[]int] // type argument for S explicitly provided, type argument for E inferred f2 := apply[[]string, string] // both type arguments explicitly provided var bytes []byte r := apply(bytes, func(byte) byte { … }) // both type arguments inferred from the function arguments Let’s demonstrate this by refering to the example from yesterday.


When function instantiations can be inferred

A quick update on my livestream: It’s on hold until February, as my family and I made a bit of a last-minute trip to visit family for the month of January, so I won’t be in my studio for a while. Instantiations … When using a generic function, type arguments may be provided explicitly, or they may be partially or completely inferred from the context in which the function is used.


Generic functions as operands

Operands … An operand name denoting a generic function may be followed by a list of type arguments; the resulting operand is an instantiated function. So first off, recall that an operand may be a function literal: var x = func() { /* ... */ } Or a variable that represents a function: var F = func() { /* ... */ } var f = F // <--- `F` is a variable that represents a function Or another expression that evaluates to a function: