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

Binder 인스턴스화하기

EarlyBinder와 마찬가지로, Binder의 내부에 접근할 때는 먼저 바운드 변수를 다른 값으로 치환하여 바인더를 해제해야 합니다. 이는 EarlyBinder의 경우와 거의 같은 이유 때문이며, Binder가 도입한 매개변수를 참조하는 타입은 해당 바인더 밖에서는 아무런 의미가 없습니다. 다음의 오류가 발생하는 예제를 보십시오.

fn foo<'a>(a: &'a u32) -> &'a u32 {
    a
}
fn bar<T>(a: fn(&u32) -> T) -> T {
    a(&10)
}

fn main() {
    let higher_ranked_fn_ptr = foo as for<'a> fn(&'a u32) -> &'a u32;
    // Attempt to infer `T=for<'a> &'a u32` which is not satsifiable
    let references_bound_vars = bar(higher_ranked_fn_ptr);
}

이 예제에서는 for<'a> fn(&'^0 u32) -> &'^0 u32 타입의 인자를 bar에 전달하고 있습니다. T가 &'^0 u32 타입으로 추론되도록 허용해서는 안 되는데, 그렇게 되면 상당히 무의미하기 때문입니다(그리고 우연히 ICE가 발생하지 않는다면 아마도 건전하지 않을 것입니다). main은 'a에 대해 알지 못하므로 빌림 검사기는 라이프타임 'a를 가진 빌림을 처리할 수 없을 것입니다.

EarlyBinder와 달리, 일반적으로 Binder는 사용자로부터 받은 구체적인 인자 집합으로 인스턴스화하지 않습니다. 예를 들어 for<'a1, 'a2> fn(&'a1 u32, &'a2 u32)에 대한 인자로 ['b, 'static]을 사용하는 식으로 인스턴스화하지 않습니다. 대신 보통은 추론 변수나 플레이스홀더로 바인더를 인스턴스화합니다.

추론 변수로 인스턴스화하기

바인더의 가능한 인스턴스화 하나를 추론하려고 할 때, 예를 들어 고차 함수 포인터를 호출하거나 고차 where 절을 사용해 어떤 바운드를 증명하려고 할 때 추론 변수로 바인더를 인스턴스화합니다. 예를 들어, 위 예제의 higher_ranked_fn_ptr이 주어졌을 때 이를 &10_u32로 호출한다면 다음과 같이 진행합니다.

  • 추론 변수로 바인더를 인스턴스화하여 fn(&'?0 u32) -> &'?0 u32) 시그니처를 얻습니다
  • 전달된 인자 &10_u32의 타입(&’static u32)을 시그니처의 타입인 &'?0 u32와 동일시하여 '?0 = 'static을 추론합니다
  • 전달된 인자의 타입을 함수 포인터 시그니처에 있는 인자의 타입과 성공적으로 단일화할 수 있었으므로, 전달된 인자는 올바릅니다

추론 변수로 인스턴스화하는 또 다른 예로, for<'a> T: Trait<'a> where 절이 주어졌을 때 T: Trait<'static>이 성립함을 증명하려 한다면 다음과 같이 진행합니다.

  • 추론 변수로 바인더를 인스턴스화하여 T: Trait<'?0> where 절을 얻습니다
  • T: Trait<'static> 목표를 인스턴스화된 where 절과 동일시하여 '?0 = 'static을 추론합니다
  • T: Trait<'static>을 T: Trait<'?0>과 성공적으로 단일화할 수 있었으므로 목표가 성립합니다

추론 변수로 바인더를 인스턴스화하려면 InferCtxt의 instantiate_binder_with_fresh_vars 메서드를 사용하면 됩니다. 바인더의 특정한 인스턴스화 하나에만 관심이 있을 때는 추론 변수로 바인더를 인스턴스화해야 하며, 그 대신 바인더의 가능한 모든 인스턴스화에 대해 추론하고자 한다면 플레이스홀더를 사용해야 합니다.

플레이스홀더로 인스턴스화하기

플레이스홀더는 Ty/ConstKind::Param/ReEarlyParam과 매우 비슷하며, 자기 자신과만 같은 어떤 알 수 없는 타입을 나타냅니다. Ty/Const와 Region은 모두 Universe와 BoundVar로 구성된 Placeholder 변형을 가지고 있습니다.

Universe는 플레이스홀더가 어느 바인더에서 비롯되었는지를 추적하고, BoundVar는 이 플레이스홀더가 해당 바인더의 어느 매개변수에 대응하는지를 추적합니다. 플레이스홀더의 동등성은 오직 유니버스가 같은지와 BoundVar가 같은지에 의해서만 결정됩니다. 자세한 내용은 플레이스홀더와 유니버스에 관한 장을 참조하십시오.

다른 rustc 개발자와 이야기하거나 Debug 형식으로 출력된 Ty/Const/Region을 볼 때, Placeholder는 흔히 '!UNIVERSE_BOUNDVARS로 표기됩니다. 예를 들어, for<'a> fn(&'a u32, for<'b> fn(&'b &'a u32)) 타입이 주어졌을 때 두 바인더를 모두 인스턴스화하고 나면(현재 InferCtxt의 Universe가 그 전에 U0이었다고 가정합니다), &'b &'a u32의 타입은 &'!2_0 &!1_0 u32로 표현됩니다.

플레이스홀더의 유니버스가 0이면 디버그 출력에서 완전히 생략됩니다. 즉, !0_2는 !2로 출력됩니다. 하지만 플레이스홀더로 바인더를 인스턴스화할 때 InferCtxt의 유니버스를 증가시키므로 실제로는 이런 경우가 드물며, 보통 마주칠 수 있는 가장 낮은 유니버스의 플레이스홀더는 U1에 있는 것입니다.

Binder는 InferCtxt의 enter_forall 메서드를 통해 플레이스홀더로 인스턴스화할 수 있습니다. 컴파일러가 바인더의 구체적인 인스턴스화 하나가 아니라 가능한 모든 인스턴스화를 고려해야 할 때는 언제나 이 메서드를 사용해야 합니다.

참고: 이 장의 원래 예제에서는 지역 변수의 타입을 &'^0 u32로 추론해서는 안 된다고 언급했습니다. 이 코드는 유니버스를 통해 컴파일되지 않도록 막힙니다(링크된 장에서 설명한 것과 같습니다).

RePlaceholder와 ReBound가 둘 다 있는 이유는 무엇입니까?

이 두 변형이 왜 모두 있는지 궁금하실 수 있습니다. 결국 Placeholder에 저장되는 데이터는 사실상 ReBound의 데이터와 같기 때문입니다. 즉, 어떤 바인더인지 추적하는 정보와, Binder가 도입한 매개변수 중 어느 것인지 추적하는 인덱스입니다.

그 주된 이유는 Bound가 바운드 변수의 보다 구문적인 표현인 반면, Placeholder는 보다 의미적인 표현이기 때문입니다. 구체적인 예는 다음과 같습니다.

#![allow(unused)]
fn main() {
impl<'a> Other<'a> for &'a u32 { }

impl<T> Trait for T
where
    for<'a> T: Other<'a>,
{ ... }

impl<T> Bar for T
where
    for<'a> &'a T: Trait
{ ... }
}

이러한 트레이트 구현이 주어졌을 때 u32: Bar는 성립하지 않아야 합니다. &'a u32는 빌림의 라이프타임과 트레이트의 라이프타임이 같을 때에만 Other<'a>를 구현합니다. 그러나 ReBound만 사용하고 플레이스홀더가 없었다면, 그 트레이트 바운드가 성립한다고 실수로 믿기 쉬웠을 것입니다. 이를 설명하기 위해 rustc에 플레이스홀더가 없는 세계에서 u32: Bar를 증명하려는 예를 차례로 살펴봅시다.

  • 먼저 u32: Bar를 증명하려고 시도합니다.
  • impl<T> Bar for T impl을 찾게 되고, 결국 EarlyBinder를 u32로 인스턴스화하게 됩니다(참고: 실제로는 먼저 바인더를 추론 변수로 인스턴스화한 뒤 그 추론 변수를 u32로 추론하므로 이 설명이 완전히 정확하지는 않지만, 여기서 그 차이는 그다지 중요하지 않습니다).
  • impl에는 for<'a> &'^0 T: Trait라는 where 절이 있으며, 초기 바인더를 u32로 인스턴스화했으므로 실제로는 for<'a> &'^0 u32: Trait를 증명해야 합니다.
  • impl<T> Trait for T impl을 찾게 되고, 결국 EarlyBinder를 &'^0 u32로 인스턴스화하게 됩니다.
  • for<'a> T: Other<'^0>라는 where 절이 있으며, 초기 바인더를 &'^0 u32로 인스턴스화했으므로 실제로는 for<'a> &'^0 u32: Other<'^0>를 증명해야 합니다.
  • impl<'a> Other<'a> for &'a u32를 찾게 되며, 빌림의 라이프타임과 트레이트의 라이프타임이 모두 '^0이므로 이 impl만으로 바운드를 증명하기에 충분합니다.

이 최종 결과는 잘못되었습니다. 각자 고유한 제네릭 매개변수를 도입하는 별개의 바인더가 두 개 있었으므로, 트레이트 바운드는 for<'a1, 'a2> &'^1 u32: Other<'^0>와 같은 형태가 되었어야 하며, 이는 impl<'a> Other<'a> for &'a u32로 충족되지 않습니다.

이론적으로는 이것이 동작하도록 만들 수 있지만, 상당히 번거롭고 현재 구성보다 복잡해질 것입니다. 다음과 같은 작업이 필요합니다.

  • Binder/EarlyBinder를 Bound ty/const/region으로 인스턴스화할 때마다 바운드 변수가 더 높은 DebruijnIndex를 갖도록 “다시 작성“합니다
  • 추론 변수를 바운드 변수로 추론할 때, 그 바운드 변수가 추론 변수를 생성한 이후에 진입한 바인더에서 온 것이라면 해당 변수의 DebruijnIndex를 낮춰야 합니다.
  • 추론 변수가 어떤 바인더 안에서 생성되었는지, 그리고 매개변수를 지칭할 수 있는 가장 안쪽 바인더가 무엇인지를 별도로 추적해야 합니다(현재는 후자만 추적하면 됩니다).
  • 추론 변수를 해석할 때 infcx의 현재 바인더 깊이에 따라 모든 바운드 변수를 다시 작성합니다
  • 이 밖에도 더 있을 수 있습니다(이 목록을 작성하는 동안 항목이 계속 추가되었으므로, 이것이 전부라고 생각하는 것은 순진해 보입니다).

근본적으로 이 모든 복잡성은 Bound 타입/상수/리전이 Binder의 특정 매개변수에 대해, 그 매개변수를 도입한 바인더와 매개변수가 사용되는 위치 사이에 다른 Binder가 몇 개 있는지에 따라 서로 다른 표현을 갖기 때문에 생깁니다. 예를 들어, 다음 코드가 있다고 합시다.

#![allow(unused)]
fn main() {
fn foo<T>()
where
    for<'a> T: Trait<'a, for<'b> fn(&'b T, &'a u32)>
{ ... }
}

이 where 절은 for<'a> T: Trait<'^0, for<'b> fn(&'^0 T, &'^1_0 u32)>로 작성됩니다. 'a 매개변수에 대한 참조가 두 개 있지만, 뒤쪽의 사용이 내부 함수 포인터 타입에 대한 두 번째 Binder 아래에 중첩되어 있기 때문에 두 참조는 각각 ^0과 ^1_0으로 서로 다르게 표현됩니다.

반면 Placeholder 타입/상수/리전에는 이러한 제약이 없습니다. Universe는 매개변수의 사용 위치가 아니라 현재 InferCtxt에 고유하기 때문입니다.

Placeholder는 어떤 문맥에 있든 동일한 표현을 가지므로, EarlyBinder를 인스턴스화하거나 추론 변수를 기존 Placeholder와 단일화하는 일은 아주 간단하게 할 수 있습니다. 예를 들어, 위의 고랭크(higher ranked) where 절의 바인더를 인스턴스화하면 T: Trait<'!1_0, for<'b> fn(&'^0 T, &'!1_0 u32)>와 같이 표현됩니다. 둘 중 하나가 다른 Binder 아래에 있는데도 'a의 두 사용에 대한 RePlaceholder 표현은 동일합니다.

이어서 함수 포인터의 바인더를 인스턴스화하면 fn(&'!2_0 T, ^'!1_0 u32)와 같은 타입을 얻게 되며, 'b 매개변수에 대한 RePlaceholder는 그 바인더가 'a의 바인더보다 나중에 인스턴스화되었다는 사실을 추적하기 위해 더 높은 유니버스에 있습니다.

ReLateParam으로 인스턴스화하기

타입 표현에 관한 장에서 설명했듯이, RegionKind에는 제네릭 매개변수를 표현하기 위한 두 가지 변형인 ReLateParam과 ReEarlyParam이 있습니다. ReLateParam은 개념적으로 항상 루트 유니버스(U0)에 있는 Placeholder입니다. 이는 함수/클로저 내부에 있는 동안 해당 함수/클로저의 후기 바인딩 매개변수를 인스턴스화할 때 사용됩니다. 실제 표현은 ReEarlyParam과도, RePlaceholder와도 상당히 다릅니다:

  • 후기 바인딩 제네릭 매개변수를 도입한 항목의 DefId
  • 제네릭 매개변수의 DefId와 (Symbol을 통한) 그 이름을 지정하거나, 이 플레이스홀더가 Fn/FnMut 클로저의 self 빌림에 대한 익명 라이프타임을 나타낸다는 것을 지정하는 BoundRegionKind BrAnon에 대한 변형도 있지만, 이는 ReLateParam에는 사용되지 않습니다.

예를 들어, 다음 코드가 있다고 합시다.

impl Trait for Whatever {
    fn foo<'a>(a: &'a u32) -> &'a u32 {
        let b: &'a u32 = a;
        b
    }
}

함수 본문에 있는 &'a u32 타입의 라이프타임 'a는 다음과 같이 표현됩니다:

ReLateParam(
    {impl#0}::foo,
    BoundRegionKind::BrNamed({impl#0}::foo::'a, "'a")
)

함수 본문 내부에서 그 함수의 후기 바인딩 제네릭 매개변수를 참조하는 이 특정한 경우에는, 어딘가에서 Binder를 인스턴스화할 때 명시적으로 수행되는 것이 아니라 hir_ty_lowering 중에 암묵적으로 수행됩니다. 그러나 경우에 따라서는 Binder를 ReLateParam으로 명시적으로 인스턴스화하기도 합니다.

일반적으로 함수/클로저의 후기 바인딩 매개변수에 대한 Binder가 있고 개념적으로 이미 그 바인더 내부에 있을 때는, liberate_late_bound_regions를 사용하여 이를 ReLateParam으로 인스턴스화합니다. 따라서 이 연산은 EarlyBinder의 instantiate_identity에 대응하는 Binder의 연산이 됩니다.

구체적인 예로, 타입 검사 중인 함수의 시그니처에 접근하면 EarlyBinder<Binder<FnSig>>로 표현됩니다. 이미 이 바인더들의 “내부“에 있으므로, instantiate_identity를 호출한 뒤 liberate_late_bound_regions를 호출하게 됩니다.