Menu

Zero 제네릭: 함수와 shape의 타입 매개변수

Zero에서 제네릭이 어떻게 동작하는지 정리했습니다. 함수와 shape의 타입 매개변수 선언, 제네릭 함수 호출, 그리고 긴 매개변수화를 깔끔한 이름으로 바꿔 주는 타입 별칭 패턴까지 살펴봅니다.

이 페이지에는 실행 가능한 에디터가 있습니다 — 편집하고 실행하면 결과를 바로 볼 수 있습니다.

제네릭이 필요한 이유

둘 이상의 원소 타입에 동작해야 하는 타입을 곧 만나게 됩니다. 두 값의 "페어"는 그 값이 정수든, 문자열이든, 사용자 정의 shape든 상관하지 않죠. 제네릭은 타입을 한 번 작성해서 호출자가 필요로 하는 어떤 원소 타입으로든 인스턴스화할 수 있게 해줍니다.

대안 — IntPair, StringPair, BytePair 등을 각각 쓰는 것 — 은 금세 지루해지고 조합도 잘 안 됩니다. Zero에서 제네릭이 이 일에 쓰는 표준 도구예요.

제네릭 함수

함수 이름과 매개변수 목록 사이에 꺾쇠괄호로 타입 매개변수를 선언합니다.

fun makePair<T, U>(left: T, right: U) -> Pair<T, U> {
    return Pair { left: left, right: right }
}

TU는 자리 표시 타입입니다 — 호출자가 무엇으로 할지 결정하죠.

호출은 보통 명시적으로 적을 필요가 없습니다. 컴파일러가 인자 타입에서 추론합니다.

let pair = makePair(40, 2_u8)

여기서 Ti32(접미사 없는 정수 리터럴의 기본값)로, Uu8(_u8 접미사로부터)로 추론됩니다. 결과 바인딩 pair의 타입은 Pair<i32, u8>이 됩니다.

리터럴이 모호해서 추론이 잘못된 타입을 고를 수 있다면, 호출 지점에서 매개변수를 못 박을 수 있어요.

let pair = makePair<u8, u8>(1, 2)

(꺾쇠괄호 호출 문법이 정확히 위와 같은지는 Zero 버전마다 다를 수 있습니다. 정확한 표기는 현재 문서를 확인하세요. 추론 우선 동작이 안정된 부분입니다.)

제네릭 shape

shape도 같은 방식으로 타입 매개변수를 받습니다.

shape Pair<T, U> {
    left: T,
    right: U,
}

각 필드의 타입은 매개변수를 언급할 수 있어요. 인스턴스에서 그것을 못 박습니다.

let intBytes: Pair<i32, u8> = Pair { left: 40, right: 2_u8 }
let words:    Pair<String, String> = Pair { left: "hi", right: "there" }

제네릭 shape와 제네릭 함수를 함께 쓰는 예제 — Run을 눌러 추론이 어떻게 동작하는지 보세요.

제네릭 shape 선언 하나, 제네릭 함수 하나, 강한 타입의 호출 지점 하나. IntBytePair 같은 보일러플레이트는 없습니다.

타입 별칭

같은 매개변수화된 타입이 반복해서 등장한다면 type으로 이름을 붙이세요.

type BytePair = Pair<u8, u8>

이제 BytePair는 타입을 적을 수 있는 곳 어디서나 Pair<u8, u8>과 호환됩니다.

별칭은 단지 명명 기능입니다. 별개의 타입을 만들지는 않아요. BytePair를 받는 함수는 Pair<u8, u8> 값을 그대로 받아들일 수 있고, 반대도 마찬가지입니다.

표준 라이브러리의 제네릭

같은 메커니즘이 표준 라이브러리의 상당 부분을 움직입니다. 실제 Zero 코드에서 보게 될 몇 가지:

  • Maybe<T>T 또는 아무것도 없음을 담는 선택적 값.
  • Span<T>T 값에 대한 빌려온 슬라이스. Span<u8>은 바이트 버퍼 위의 표준 뷰입니다.
  • ref<T>mutref<T> — 데이터를 복사하지 않고 공유해야 할 때 쓰는 명시적 참조 타입.

한 번에 모두 익힐 필요는 없습니다. 제네릭의 핵심은 같은 shape가 손에 있는 어떤 원소 타입에든 동작한다는 것이니까요.

제네릭이 가치를 발휘할 때 (그리고 그렇지 않을 때)

같은 함수나 shape를 다른 원소 타입으로 두 번 쓰고 있는 자신을 발견했다면 제네릭을 꺼내세요. 다음 같은 경우에는 구체 타입을 쓰세요.

  • 함수의 로직이 특정 타입에 대해서만 의미가 있을 때(예: String 파서).
  • 에러 메시지에 타입이 나타나서 디버깅이 쉬워지기를 원할 때.
  • 성능 특성이 메모리상의 특정 크기에 의존할 때.

제네릭에는 실제 비용이 따릅니다. 각 인스턴스화마다 새 코드가 생성되어 바이너리가 커지고 컴파일도 약간 느려집니다. 대부분의 애플리케이션 코드에서는 그 비용이 무시할 만하지만, 바이너리 크기가 중요한 임베디드 스타일 코드를 짠다면 알아둘 만합니다.

제약 조건에 관한 메모

일부 제네릭 시스템은 타입 매개변수에 제약을 둘 수 있게 합니다("T==를 지원해야 한다", "TIterator를 구현해야 한다" 같은). 1.0 이전 Zero의 제약 조건 이야기는 아직 발전 중이에요. 공식 저장소의 예제들은 정교한 경계 없이 제네릭을 평범한 형태로 사용합니다. 언어가 안정화되면서 제약 조건 문법이 언어 나머지와 일관된 작고 규칙적인 형태로 들어올 거예요. 지금은 실제로 전달하는 어떤 T에든 동작하는 제네릭을 작성하고, 어떤 연산이 지원되지 않으면 컴파일러가 알려주도록 두세요.

다음 글: enum

제네릭은 타입에 대해 매개변수화하는 도구입니다. 다음 빌딩 블록은 스펙트럼의 반대쪽 끝에 있는 enum입니다. Zero의 평범한 열거 타입으로, 변종이 추가 데이터를 담지 않는 경우에 어울려요.

자주 묻는 질문

Zero에서 제네릭은 어떻게 동작하나요?

함수나 shape 이름 뒤에 꺾쇠괄호로 타입 매개변수를 선언합니다: fun makePair<T, U>(left: T, right: U) -> Pair<T, U>shape Pair<T, U> { left: T, right: U } 같은 식이에요. 호출자는 매개변수를 명시적으로 못 박거나(Pair<i32, u8>), 호출 인자에서 컴파일러가 추론하도록 둡니다.

Zero에서 shape도 제네릭이 될 수 있나요?

네. shape도 함수와 같은 꺾쇠괄호 문법으로 타입 매개변수를 받을 수 있습니다: shape Pair<T, U> { left: T, right: U }. 각 필드의 타입은 그 매개변수를 사용할 수 있어요. 인스턴스는 매개변수가 채워진 타입을 적어서 만듭니다 — 예를 들면 Pair<i32, u8> 같은 식이죠.

제네릭 함수를 호출할 때 타입 매개변수를 꼭 지정해야 하나요?

보통은 안 해도 됩니다. 컴파일러가 인자 타입에서 추론하거든요. makePair(40, 2_u8)을 호출하는 것만으로 Ti32, Uu8이 됩니다. 추론이 잘못된 타입을 고를 만하거나 호출 지점에 매개변수를 문서화하고 싶을 때는 명시적으로 못 박을 수 있어요.

Zero에서 타입 별칭이 뭔가요?

타입 별칭은 긴 타입 표현에 붙이는 짧은 이름입니다. type BytePair = Pair<u8, u8>라고 적으면 Pair<u8, u8>을 적을 수 있는 자리 어디에나 BytePair를 쓸 수 있어요. 별칭은 순수한 명명 기능이라 새 타입을 도입하지 않고, 기존 타입을 가리키는 더 짧은 방법을 제공할 뿐입니다.

Zero 표준 라이브러리에서는 제네릭이 어디에 등장하나요?

어디든요. 임의의 원소 타입을 담거나 다뤄야 하는 타입이라면 어디서나 등장합니다. 선택적 값을 위한 Maybe<T>, 바이트 슬라이스를 위한 Span<u8>, 원소 타입에 대해 매개변수화된 컨테이너 타입 등이 있어요. 사용자 정의 타입이든 표준 라이브러리 타입이든 같은 제네릭 메커니즘이 처리합니다.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기