인스턴스화

인스턴스화 (Instantiations)

제네릭 함수나 메서드, 타입을 실제로 '쓸 수 있는' 형태로 만들려면, 타입 매개변수 자리에 구체적인 타입 인자를 채워 넣어야 해요. 바로 이 과정을 인스턴스화(instantiation) 라고 부르는데요, Go 1.18과 Go 1.27에서 정의된 제네릭의 핵심 동작이에요. 이번 절에서는 그 인스턴스화가 정확히 어떤 두 단계로 진행되는지, 그리고 타입 인자를 언제 생략할 수 있는지를 하나씩 살펴볼게요.

출처: Go Specification

본문

제네릭 함수, 메서드, 타입은 타입 매개변수 자리에 타입 인자(type argument)를 대입하면서 인스턴스화됩니다 (Go 1.18 / Go 1.27). 인스턴스화는 두 단계로 진행돼요.

  1. 각 타입 인자를 제네릭 선언 안의 해당 타입 매개변수 자리에 대입합니다. 이 대입은 함수나 타입 선언 전체에 걸쳐 일어나는데, 타입 매개변수 목록 자체와 그 목록 안의 모든 타입도 포함돼요.
  2. 대입이 끝나면, 각 타입 인자는 해당 타입 매개변수의 제약(constraint)(필요하면 인스턴스화된)을 만족(satisfy)해야 합니다. 그렇지 않으면 인스턴스화는 실패해요.

제네릭 타입, 함수, 메서드를 인스턴스화하면 각각 제네릭이 아닌(non-generic) 타입, 함수, 메서드가 됩니다.

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

제네릭 함수나 메서드를 사용할 때는 타입 인자를 명시적으로 줄 수도 있고, 함수가 쓰이는 문맥에서 타입 인자를 부분적으로 또는 완전히 추론(infer)할 수도 있어요. 타입 인자를 추론할 수 있다면, 다음 경우에는 타입 인자 목록을 아예 생략할 수 있죠.

그 밖의 모든 경우에는 (부분적일 수 있는) 타입 인자 목록이 반드시 있어야 해요. 타입 인자 목록이 아예 없거나 부분적이라면, 빠진 모든 타입 인자는 함수가 쓰이는 문맥에서 추론할 수 있어야 합니다.

// sum returns the sum (concatenation, for strings) of its arguments.
func sum[T ~int | ~float64 | ~string](x... T) T { … }

x := sum                       // illegal: the type of x is unknown
intSum := sum[int]             // intSum has type func(x... int) int
a := intSum(2, 3)              // a has value 5 of type int
b := sum[float64](2.0, 3)      // b has value 5.0 of type float64
c := sum(b, -1)                // c has value 4.0 of type float64

type sumFunc func(x... string) string
var f sumFunc = sum            // same as var f sumFunc = sum[string]
f = sum                        // same as f = sum[string]

부분 타입 인자 목록은 비어 있을 수 없어요. 적어도 첫 번째 인자는 반드시 있어야 합니다. 그 목록은 전체 타입 인자 목록의 접두사(prefix) 역할을 해서, 나머지 인자들은 추론에 맡겨요. 대략적으로 말하면 타입 인자는 "오른쪽에서 왼쪽으로" 생략할 수 있다고 볼 수 있죠.

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

제네릭 타입의 경우에는 타입 인자를 항상 전부 명시적으로 제공해야 합니다.

더 알아보기

  • Type inference — 타입 인자를 문맥에서 추론하는 규칙. 인스턴스화와 짝을 이루는 절이에요.
  • Type parameter declarations — 타입 매개변수와 제약을 선언하는 문법.
  • Satisfying a type constraint — 타입 인자가 제약을 만족하는지 판정하는 규칙.
  • Calls — 제네릭 함수를 인자 가진 채로 호출하면서 타입 인자를 생략하는 경우.