Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

별칭과 정규화

별칭

Rust에는 어떤 “기저(underlying)” 타입과 같다고 간주되는 타입이 여럿 있습니다. 고유 연관 타입, 트레이트 연관 타입, 자유 타입 별칭(type Foo = u32), 불투명 타입(-> impl RPIT) 등이 그 예입니다. 이러한 타입을 “별칭“이라고 부릅니다. 별칭 타입은 TyKind::Alias 변형으로 표현되며, 별칭의 종류는 AliasTyKind 열거형으로 추적합니다.

정규화란 이러한 별칭 타입을 그것과 같은 기저 타입으로 대체하는 과정입니다. 예를 들어 타입 별칭 type Foo = u32가 있을 때 Foo를 정규화하면 u32가 됩니다.

타입 외에도 별칭이라는 개념은 상수/상수 제네릭에도 적용됩니다. 하지만 현재 컴파일러에서는 상수 별칭을 “일급 개념“으로 취급하지 않으므로, 이 장에서는 주로 타입을 기준으로 설명합니다(개념 자체는 상수에도 그대로 적용됩니다).

경직된 별칭, 모호한 별칭, 정규화되지 않은 별칭

별칭은 “경직(rigid)“되었거나, “모호(ambiguous)“하거나, 단순히 정규화되지 않은 상태일 수 있습니다.

타입의 “형태“가 바뀌지 않는다면 그 타입을 경직되었다고 간주합니다. 예를 들어 Box는 아무리 정규화해도 Box를 u32로 바꿀 수 없으므로 경직되어 있지만, <vec::IntoIter<u32> as Iterator>::Item은 u32로 정규화될 수 있으므로 경직되어 있지 않습니다.

별칭은 더 이상 결코 정규화할 수 없을 때 경직되었다고 합니다. 경직된 별칭의 구체적인 예로는 T: Iterator<Item = ...> 바운드 없이 T: Iterator 바운드만 있는 환경에서의 <T as Iterator>::Item을 들 수 있습니다:

#![allow(unused)]
fn main() {
fn foo<T: Iterator>() {
    // This alias is *rigid*
    let _: <T as Iterator>::Item;
}

fn bar<T: Iterator<Item = u32>>() {
    // This alias is *not* rigid as it can be normalized to `u32`
    let _: <T as Iterator>::Item;
}
}

별칭을 아직 정규화할 수는 없지만 현재 환경에서 결국 정규화할 수 있게 될 수도 있다면, 그 별칭을 “모호한” 별칭으로 간주합니다. 이는 별칭에 추론 변수가 포함되어 트레이트가 어떻게 구현되는지 결정할 수 없을 때 발생할 수 있습니다:

#![allow(unused)]
fn main() {
fn foo<T: Iterator, U: Iterator>() {
    // This alias is considered to be "ambiguous"
    let _: <_ as Iterator>::Item;
}
}

이러한 별칭을 “모호한” 별칭이라고 부르는 이유는 이것이 경직된 별칭인지 아닌지가 모호하기 때문입니다.

_: Iterator 트레이트 impl의 출처는 모호합니다(즉, 알 수 없습니다). 어떤 impl Iterator for u32일 수도 있고 어떤 T: Iterator 트레이트 바운드일 수도 있으며, 아직은 알 수 없습니다. _: Iterator가 성립하는 이유에 따라 이 별칭은 정규화되지 않은 별칭일 수도 있고 경직된 별칭일 수도 있으므로, 이것이 어떤 종류의 별칭인지 모호합니다.

마지막으로, 별칭은 단순히 정규화되지 않은 상태일 수도 있습니다. <Vec<u32> as IntoIterator>::Iter는 이미 std::vec::IntoIter<u32>로 정규화될 수 있지만 아직 정규화되지 않았을 뿐이므로 정규화되지 않은 별칭입니다.


자유 별칭과 고유 별칭은 경직되거나 모호할 수 없다는 점에 유의하십시오. 이들의 이름을 지정한다는 것은 곧 별칭의 기저 타입을 명시하는 별칭 정의를 이미 해석했다는 뜻이기 때문입니다.

발산하는 별칭

별칭의 정의가 정규화될 대상인, 별칭이 아닌 기저 타입을 명시하지 않으면 그 별칭은 “발산한다“고 간주합니다. 발산하는 별칭의 구체적인 예는 다음과 같습니다:

#![allow(unused)]
fn main() {
type Diverges = Diverges;

trait Trait {
    type DivergingAssoc;
}
impl Trait for () {
    type DivergingAssoc = <() as Trait>::DivergingAssoc;
}
}

이 예에서 Diverges와 DivergingAssoc은 모두 자기 자신과 같다고 정의된, 발산하는 타입 별칭의 “자명한” 사례입니다. Diverges가 정규화될 수 있는 기저 타입은 결코 존재하지 않습니다.

일반적으로 발산하는 별칭이 정의되면 오류를 내려고 하지만, 이는 전적으로 “최선의 노력(best effort)” 수준의 검사입니다. 앞의 예에서는 정의가 탐지할 수 있을 만큼 “충분히 단순“하므로 오류가 발생합니다. 그러나 더 복잡한 경우나 제네릭 매개변수의 일부 인스턴스화에서만 발산하는 별칭이 되는 경우에는 오류를 내지 않습니다:

#![allow(unused)]
fn main() {
trait Trait {
    type DivergingAssoc<U: Trait>;
}
impl<T: ?Sized> Trait for T {
    // This alias always diverges but we don't emit an error because
    // the compiler can't "see" that.
    type DivergingAssoc<U: Trait> = <U as Trait>::DivergingAssoc<U>;
}
}

결국 이는 타입 시스템의 별칭이 발산하지 않는다는 보장이 없음을 의미합니다. 별칭은 특정 제네릭 인자에 대해서만 발산할 수 있으므로, 별칭이 발산하는지 여부는 별칭이 완전히 구체화된 후에야 알 수 있다는 것도 의미합니다. 따라서 코드 생성/상수 평가 역시 발산하는 별칭을 처리해야 합니다.

trait Trait {
    type Diverges<U: Trait>;
}
impl<T: ?Sized> Trait for T {
    type Diverges<U: Trait> = <U as Trait>::Diverges<U>;
}

fn foo<T: Trait>() {
    let a: T::Diverges<T>;
}

fn main() {
    foo::<()>();
}

이 예시에서는 foo::<()>의 코드 생성 중에만 발산하는 별칭으로 인한 오류가 발생합니다. foo 호출을 제거하면 컴파일 오류가 발생하지 않습니다.

불투명 타입

불투명 타입은 비교적 특수한 종류의 별칭이며, 별도의 장에서 다룹니다.

상수 별칭

타입 별칭과 달리 상수 별칭은 타입 시스템에서 직접 표현되지 않습니다. 대신 상수 별칭은 항상 상수 항목을 가리키는 경로 표현식을 담은 익명 본문입니다. 즉, 타입 시스템에 존재하는 유일한 “상수 별칭“은 평가되지 않은 익명 상수 본문입니다.

따라서 ConstKind::Alias(AliasCtKind::Projection/Inherent/Free, _)는 존재하지 않습니다. 대신 익명 상수를 표현하는 데 사용되는 ConstKind::Unevaluated만 있습니다.

#![allow(unused)]
fn main() {
fn foo<const N: usize>() {}

const FREE_CONST: usize = 1 + 1;

fn bar() {
    foo::<{ FREE_CONST }>();
    // The const arg is represented with some anonymous constant:
    // ```pseudo-rust
    // const ANON: usize = FREE_CONST;
    // foo::<ConstKind::Unevaluated(DefId(ANON), [])>();
    // ```
}
}

이는 상수 제네릭 기능이 개선됨에 따라 바뀔 가능성이 높습니다. 예를 들어 feature(associated_const_equality)와 feature(min_generic_const_args)는 모두 상수 별칭을 (모든 상수 인자를 감싸는 익명 상수 없이) 타입과 비슷하게 처리해야 합니다.

정규화란 무엇인가

구조적 정규화와 깊은 정규화

정규화에는 구조적 정규화(얕은 정규화라고도 합니다)와 깊은 정규화의 두 가지 형태가 있습니다. 구조적 정규화는 타입의 “가장 바깥” 부분만 정규화하는 것으로 생각해야 합니다. 반면 깊은 정규화는 타입 안의 모든 별칭을 정규화합니다.

실제로는 구조적 정규화가 타입의 바깥 계층보다 더 많은 부분을 정규화할 수도 있지만, 이 동작에 의존해서는 안 됩니다. 바운드 변수(for<'a>)를 사용하는 정규화 불가능한 비경직 별칭은 어느 종류의 정규화로도 정규화할 수 없습니다.

예를 들어, 개념적으로 Vec<<u8 as Identity>::Assoc> 타입을 구조적으로 정규화하면 아무 변화가 없지만, 깊게 정규화하면 Vec<u8>이 됩니다. 실제로는 구조적 정규화로도 Vec<u8>이 되지만, 다시 말하지만 이에 의존해서는 안 됩니다.

별칭이 바운드 변수를 사용하도록 바꾸면 동작이 달라집니다. Vec<for<'a> fn(<&'a u8 as Identity>::Assoc)>는 구조적으로 정규화하면 아무 변화가 없지만, 깊게 정규화하면 Vec<for<'a> fn(&'a u8)>이 됩니다.

핵심 정규화 로직

별칭을 구조적으로 정규화하는 것은 별칭을 그 정의에서 같다고 정의된 대상으로 단순히 바꾸는 것보다 조금 더 미묘합니다. 별칭을 정규화한 결과는 경직(rigid) 타입이거나 (나중에 경직 타입으로 추론될) 추론 변수여야 합니다. 이를 위해 두 가지 일을 합니다.

첫째, 모호한 별칭을 정규화할 때는 그대로 두는 대신 추론 변수로 정규화합니다. 이는 두 가지 주요 효과가 있습니다.

  • 추론 변수는 경직 타입이 아니지만 결국 항상 경직 타입으로 추론되므로, 정규화 결과를 다시 정규화할 필요가 없음을 보장합니다
  • 타입이 경직되지 않은 모든 경우에 추론 변수를 사용하므로, 컴파일러의 나머지 부분은 모호한 별칭과 추론 변수를 모두 다룰 필요가 없습니다

둘째로, 정규화가 별칭 정의에 명시된 타입을 곧바로 반환하는 대신, 해당 타입을 먼저 정규화한 다음 반환합니다1. 이렇게 하는 이유는 정규화가 멱등성을 갖도록 하여 호출자가 정규화를 루프 안에서 반복해서 실행할 필요가 없게 하기 위함입니다.

#![allow(unused)]
#![feature(checked_type_alias)]

fn main() {
type Foo<T: Iterator> = Bar<T>;
type Bar<T: Iterator> = <T as Iterator>::Item;

fn foo() {
    let a_: Foo<_>;
}
}

이 예시에서는 다음과 같습니다.

  • Foo<?x>를 정규화하면 Bar<?x>가 되겠지만, 여기서는 Foo와 같다고 정의된 타입 안의 별칭까지 정규화하려고 합니다
  • Bar<?x>를 정규화하면 <?x as Iterator>::Item이 되겠지만, 여기서도 마찬가지로 Bar와 같다고 정의된 타입 안의 별칭까지 정규화하려고 합니다
  • <?x as Iterator>::Item은 모호한 별칭이므로, <?x as Iterator>::Item을 정규화하면 새로운 추론 변수 ?y가 생성됩니다
  • 최종적으로 Foo<?x>를 정규화한 결과는 ?y입니다

정규화하는 방법

타입 시스템과 상호작용하다 보면 타입의 정규화를 요청해야 하는 경우가 자주 있습니다. 내부 정규화 로직에는 여러 진입점이 있으며, 각 진입점은 컴파일러의 특정 부분에서만 사용해야 합니다.

게다가 현재 컴파일러가 기존 트레이트 솔버에서 새 트레이트 솔버로 전환하는 중이라는 점도 상황을 더 복잡하게 만듭니다. 이 전환의 일환으로 컴파일러의 정규화 방식이 상당히 크게 바뀌었으며, 그 결과 일부 정규화 진입점은 “기존 솔버 전용“이 되었고 새 솔버가 안정화되면 장기적으로 제거될 예정입니다. 이 전환 과정은 GitHub의 WG-trait-system-refactor 레이블(트레이트 시스템 리팩터링 작업 그룹)을 통해 추적할 수 있습니다.

다음은 컴파일러에서 정규화를 수행하는 여러 진입점을 대략적으로 정리한 것입니다.

  • infcx.at.structurally_normalize
  • infcx.at.(deeply_)?normalize
  • infcx.query_normalize
  • tcx.normalize_erasing_regions
  • traits::normalize_with_depth(_to)
  • EvalCtxt::structurally_normalize

트레이트 솔버 외부

InferCtxt 타입은 분석 중에 정규화하는 “주요” 방법인 normalize, deeply_normalize, structurally_normalize를 제공합니다. 이 함수들은 FnCtxt나 ObligationCtxt 같은 다양한 InferCtxt 래퍼 타입에서 래핑되어 다시 제공되는 경우가 많으며, 이때 일부 인자나 반환 타입의 일부를 자동으로 처리하도록 API가 약간 조정되어 있습니다.

구조적 InferCtxt 정규화

infcx.at.structurally_normalize는 추론 변수와 리전을 처리할 수 있는 구조적 정규화를 제공합니다. 일반적으로 타입의 종류를 검사할 때마다 이 함수를 사용해야 합니다.

HIR 타입 검사 내부에는 이와 관련된 정규화 메서드인 fcx.structurally_resolve가 있으며, 이 메서드는 해석하려는 타입이 아직 해결되지 않은 추론 변수이면 오류를 발생시킵니다. 새 솔버가 활성화된 경우에는 이 메서드가 해당 타입의 구조적 정규화도 시도합니다.

이 때문에 HIR 타입 검사에는 먼저 normalize로 타입을 정규화한 다음(기존 솔버에서만 정규화됨) structurally_resolve를 적용하는(새 솔버에서만 정규화됨) 패턴이 있습니다. structurally_resolve는 목표를 평가하여 추론을 진전시키려고 시도하지만 structurally_normalize는 그렇지 않으므로, HIR 타입 검사 중에는 structurally_normalize를 호출하기보다 이 패턴을 사용하는 것이 좋습니다.

깊은 InferCtxt 정규화

infcx.at.(deeply_)?normalize

InferCtxt로 깊은 정규화를 수행하는 방법에는 normalize와 deeply_normalize 두 가지가 있습니다. 그 이유는 normalize는 기존 솔버에서만 사용되는 “레거시” 정규화 진입점인 반면, deeply_normalize는 장기적으로 깊은 정규화를 수행하는 방법이 될 예정이기 때문입니다. 두 메서드 모두 영역(region)을 처리할 수 있습니다.

새 솔버가 안정화되면 infcx.at.normalize 함수는 제거되고, 모든 코드가 새로운 깊은 정규화 또는 구조적 정규화 메서드로 이전될 것입니다. 이러한 이유로 normalize 함수는 새 솔버에서는 아무 동작도 하지 않으므로, 기존 솔버에서는 정규화가 필요하지만 새 솔버에서는 필요하지 않은 경우에만 사용하기에 적합합니다.

모든 모호한 별칭을 추론 변수로 정규화하는 것은 지원할 수 없으므로2, deeply_normalize를 사용하면 모호한 별칭을 만났을 때3 오류가 발생합니다. deeply_normalize는 일반적으로 모호한 별칭을 만나지 않을 것으로 예상되는 경우, 예를 들어 항목 시그니처의 타입을 다룰 때에만 사용해야 합니다.

infcx.query_normalize

infcx.query_normalize는 매우 드물게 사용됩니다. 이 함수는 normalize_erasing_regions와 거의 동일한 제약(추론 변수를 처리할 수 없고, 진단을 지원하지 않음)을 가지며, 주된 차이점은 라이프타임 정보를 유지한다는 것입니다. 이러한 이유로 거의 모든 상황에서 normalize_erasing_regions가 더 나은 선택입니다. 라이프타임이 지워진 쿼리를 캐싱하므로 더 효율적이기 때문입니다.

실제로 query_normalize는 빌림 검사기에서 정규화에 사용되며, 그 밖의 곳에서는 infcx.normalize보다 성능을 높이기 위한 최적화 수단으로 사용됩니다. 새 솔버가 안정화되면 query_normalize를 컴파일러에서 제거할 수 있을 것으로 예상됩니다. 새 솔버의 정규화 구현은 성능이 충분히 좋아서 query_normalize를 제거해도 성능 회귀가 발생하지 않을 것이기 때문입니다.

tcx.normalize_erasing_regions

normalize_erasing_regions는 일반적으로 컴파일러에서 타입 시스템 분석을 수행하지 않는 부분에서 사용됩니다. 이 정규화 진입점은 추론 변수, 라이프타임, 진단을 전혀 처리하지 않습니다. 린트와 코드 생성은 일반적으로 올바른 형태(well-formed)라고 가정할 수 있는(또는 적어도 그에 대한 오류를 보고할 책임이 없는) 완전히 추론된 별칭을 다루므로 이 진입점을 많이 사용합니다.

트레이트 솔버 내부

traits::normalize_with_depth(_to)와 EvalCtxt::structurally_normalize는 각각 이전 트레이트 솔버와 새 트레이트 솔버의 내부에서만 사용됩니다. 이는 사실상 각 트레이트 솔버가 정규화를 구현하는 내부 로직으로 곧바로 들어가는 원시 진입점입니다. 다른 정규화 진입점은 목표(goal) 순환과 재귀 깊이를 올바르게 처리하지 못하므로 트레이트 해결 내부에서는 사용할 수 없습니다.

언제/어디서 정규화하는가 (이전 솔버 대 새 솔버)

이전 솔버와 새 솔버 사이의 큰 변화 중 하나는 별칭이 언제 정규화되어 있을 것으로 기대하는지에 대한 접근 방식입니다.

기존 솔버

모든 타입은 가능한 한 빨리 정규화될 것으로 기대되므로, 타입 시스템에서 만나는 모든 타입은 경직된(rigid) 타입이거나 추론 변수(나중에 경직된 항으로 추론됨)입니다.

구체적인 예로, 별칭의 동등성은 별칭이 경직되어 있다고 가정하고 별칭의 제네릭 인자를 재귀적으로 동등화하는 방식으로 구현됩니다.

새 솔버

모든 타입은 모호하거나 정규화되지 않은 별칭을 포함할 수 있는 것으로 간주됩니다. 별칭의 정규화가 필요한 연산이 수행될 때마다, 별칭을 정규화하는 것은 해당 로직의 책임입니다. 따라서 ty.kind()에 대해 매칭할 때는 거의 항상 먼저 구조적 정규화를 수행해야 합니다.

구체적인 예로, 별칭의 동등성은 동등화되는 모든 별칭 타입이 경직되어 있다고 가정하는 대신 별칭의 정규화를 스스로 처리할 수 있도록 사용자 정의 목표 종류(PredicateKind::AliasRelate)로 구현됩니다.

이러한 접근 방식에도 불구하고, 성능과 단순성을 위해 writeback 중에는 여전히 깊은 정규화를 수행하므로, MIR의 타입은 여전히 깊은 정규화가 완료되었다고 가정할 수 있습니다.


기존 솔버의 정규화 방식에는 몇 가지 주요 문제가 있었고, 이 문제들 때문에 새 솔버에서 방식을 변경하게 되었습니다:

누락된 정규화 호출

정규화 호출이 누락되는 일이 잦았으며, 그 결과 모든 것이 이미 정규화되어 있다고 가정하는 API에 정규화되지 않은 타입이 전달되었습니다. 모호하거나 정규화되지 않은 별칭을 고정(rigid)된 것으로 취급하면, 별칭끼리 서로 같다고 간주되지 않아 온갖 이상한 오류가 발생하거나, 정규화되지 않은 별칭의 제네릭 인자를 동일시하는 과정에서 예상치 못한 추론 유도가 발생합니다.

매개변수 환경 정규화

또 다른 문제는 기존 솔버에서 ParamEnv를 올바르게 정규화할 수 없었다는 점입니다. 정규화 자체가 올바른 결과를 내기 위해 이미 정규화된 ParamEnv를 요구했기 때문입니다. 자세한 내용은 ParamEnvs에 관한 장을 참고하십시오: Typing/ParamEnvs: 모든 바운드 정규화

고차 랭크 타입 안의 정규화할 수 없는 비고정 별칭

for<'a> fn(<?x as Trait<'a>::Assoc>)와 같은 타입은 기존 솔버의 정규화 방식으로는 올바르게 처리할 수 없습니다.

이를 for<'a> fn(?y)로 정규화하고 for<'a> <?x as Trait<'a>>::Assoc -> ?y를 정규화하는 목표를 등록한다면, <?x as Trait<'a>>::Assoc가 &'a u32로 정규화되는 경우에 오류가 발생합니다. 추론 변수 ?y는 for<'a> 바인더를 인스턴스화할 때 만들어진 플레이스홀더보다 낮은 유니버스에 있게 됩니다.

기존 솔버는 모든 별칭이 경직(rigid)되어 있다고 가정하므로, 별칭을 정규화하지 않은 채로 두는 것 역시 잘못된 처리입니다. 이는 새 솔버가 coherence에서 안정화되기 전까지 건전성 버그였습니다: coherence 중에 프로젝션 치환을 관계 짓는 것은 건전하지 않음.

결국 이는 값 내부의 모든 별칭이 고정되어 있음을 항상 보장할 수는 없다는 것을 의미합니다.

발산하는 별칭의 사용 처리

발산하는 별칭은 모호한 별칭과 마찬가지로 추론 변수로 정규화됩니다. 발산하는 별칭을 정규화하면 트레이트 솔버 순환이 발생하므로, 기존 솔버에서는 항상 오류가 발생합니다. 새 솔버에서는 현재 컨텍스트에서 모든 목표가 성립해야 하는 상황에 이르렀을 때에만 오류가 발생합니다. 예를 들어, HIR 타입 검사 중에 발산하는 별칭을 정규화하면 두 솔버 모두에서 오류가 발생합니다.

별칭의 적격성(well formedness)은 해당 별칭이 발산하지 않을 것을 요구하지 않습니다4. 이는 별칭이 적격한지 검사하는 것만으로는 발산하는 별칭에 대해 오류를 발생시키기에 충분하지 않으며, 실제로 별칭을 정규화하려고 시도해야 한다는 것을 의미합니다.

발산하는 별칭에 대한 오류가 정규화의 부수 효과라는 것은 실제로 오류를 발생시킬지 여부가 매우 임의적이라는 뜻이며, 이제는 정규화하는 곳이 더 적어졌으므로 이전 솔버와 새 솔버 사이에서도 차이가 납니다.

발산하는 별칭에 대한 오류 처리가 임시방편적이어서 “문제“를 일으키는 예시는 다음과 같습니다:

#![allow(unused)]
fn main() {
trait Trait {
    type Diverges<D: Trait>;
}

impl<T> Trait for T {
    type Diverges<D: Trait> = D::Diverges<D>;
}

struct Bar<T: ?Sized = <u8 as Trait>::Diverges<u8>>(Box<T>);
}

이 예시에서는 발산하는 별칭이 사용되지만, 제네릭 매개변수의 기본값을 명시적으로 정규화하는 일이 없으므로 우연히 오류가 발생하지 않습니다. ?Sized 옵트아웃을 제거하면 <u8 as Trait>::Diverges<u8>: Sized 목표를 정규화하게 되고, 그 부수 효과로 발산하는 별칭에 대한 오류가 발생하므로 오류가 보고됩니다.

이 점에서 상수 별칭은 타입 별칭과 약간 다릅니다. 상수 별칭의 적격성은 (ConstEvaluatable 목표를 통해) 성공적으로 평가될 수 있을 것을 요구합니다. 이는 상수 인자의 적격성을 검사하는 것만으로도 평가에 실패할 경우 오류를 발생시키기에 충분하다는 것을 의미합니다. 이를 타입 별칭에도 도입하는 것이 타당한지, 아니면 상수 별칭이 적격성을 위해 이를 더 이상 요구하지 않아야 하는지는 다소 불분명합니다5.


  1. 새 솔버에서는 이것이 암묵적으로 수행됩니다 ↩

  2. 바인더 내부의 모호한 별칭은 추론 변수로 정규화할 수 없으며, 이에 대해서는 뒤에서 더 자세히 다룹니다. ↩

  3. 바인더 안의 모호한 별칭을 처리하는 방식에는 이전 솔버와 새 솔버 사이에 미묘한 차이가 있습니다. 기존 솔버에서는 고차 타입 내부의 일부 모호한 별칭에 대해 오류를 내지 못하는 반면, 새 솔버는 올바르게 오류를 냅니다. ↩

  4. 별칭이 발산하지 않는지는 별칭이 완전히 구체화되기 전까지 검사할 수 없으므로, 이는 코드 생성/상수 평가 이전에 별칭의 적격성을 검사할 수 없거나, 단형화 이후에 별칭이 적격한 상태에서 적격하지 않은 상태로 바뀔 수 있다는 것을 의미하게 됩니다. ↩

  5. 이를 더 이상 하지 않더라도 상수 별칭이 타입 별칭보다 덜 건전해지지는 않을 것이 분명합니다 ↩