불투명 타입 영역 추론 제약
이 장에서는 불투명 타입의 숨겨진 타입 Opaque<'a, 'b, .., A, B, ..> := SomeHiddenType을 정의할 때 불투명 타입의 제네릭 인자에 부과하는 여러 제약을 다룹니다.
빌림 검사는 불투명 타입 추론의 마지막 단계이므로, 이러한 제약은 빌림 검사(소스)에서 구현됩니다.
배경: 타입 및 상수 제네릭 인자
타입 인자에는 두 가지 제약이 필요합니다. 각 타입 인자는 (1) 타입 매개변수여야 하고, (2) 제네릭 인자 중에서 고유해야 합니다. 상수 인자에도 동일하게 적용됩니다.
경우 (1)의 예시:
#![allow(unused)]
fn main() {
type Opaque<X> = impl Sized;
// `T` is a type parameter.
// Opaque<T> := ();
fn good<T>() -> Opaque<T> {}
// `()` is not a type parameter.
// Opaque<()> := ();
fn bad() -> Opaque<()> {} //~ ERROR
}
경우 (2)의 예시:
#![allow(unused)]
fn main() {
type Opaque<X, Y> = impl Sized;
// `T` and `U` are unique in the generic args.
// Opaque<T, U> := T;
fn good<T, U>(t: T, _u: U) -> Opaque<T, U> { t }
// `T` appears twice in the generic args.
// Opaque<T, T> := T;
fn bad<T>(t: T) -> Opaque<T, T> { t } //~ ERROR
}
동기: 첫 번째 경우인 Opaque<()> := ()에서는 숨겨진 타입이 Opaque<X> := X와 Opaque<X> := ()라는 서로 다른 두 해석과 모두 호환되므로 모호합니다. 마찬가지로 두 번째 경우인 Opaque<T, T> := T도 Opaque<X, Y> := X로 해석해야 하는지 Opaque<X, Y> := Y로 해석해야 하는지 모호합니다. 이러한 모호성 때문에 두 경우 모두 유효하지 않은 정의 사용으로 거부됩니다.
고유성 제약
각 라이프타임 인자는 인자 목록 안에서 고유해야 하며 'static이어서는 안 됩니다. 이는 타입 매개변수의 경우와 비슷하게, 숨겨진 타입 추론에서 발생하는 모호성을 피하기 위함입니다. 예를 들어, 아래의 유효하지 않은 정의 사용 Opaque<'static> := Inv<'static>은 Opaque<'x> := Inv<'static>과 Opaque<'x> := Inv<'x> 모두와 호환됩니다.
#![allow(unused)]
fn main() {
type Opaque<'x> = impl Sized + 'x;
type Inv<'a> = Option<*mut &'a ()>;
fn good<'a>() -> Opaque<'a> { Inv::<'static>::None }
fn bad() -> Opaque<'static> { Inv::<'static>::None }
//~^ ERROR
}
#![allow(unused)]
fn main() {
type Opaque<'x, 'y> = impl Trait<'x, 'y>;
fn good<'a, 'b>() -> Opaque<'a, 'b> {}
fn bad<'a>() -> Opaque<'a, 'a> {}
//~^ ERROR
}
의미론적 라이프타임 동등성: 타입 매개변수와 비교했을 때 라이프타임이 갖는 한 가지 복잡한 점은 구문상 서로 다른 두 라이프타임이 의미상으로는 같을 수 있다는 것입니다. 따라서 라이프타임이 고유한지 검증할 때 주의해야 합니다.
#![allow(unused)]
fn main() {
// This is also invalid because `'a` is *semantically* equal to `'static`.
fn still_bad_1<'a: 'static>() -> Opaque<'a> {}
//~^ Should error!
// This is also invalid because `'a` and `'b` are *semantically* equal.
fn still_bad_2<'a: 'b, 'b: 'a>() -> Opaque<'a, 'b> {}
//~^ Should error!
}
고유성 규칙의 예외
위 고유성 규칙의 예외는 불투명 타입 정의에 있는 경계가 어떤 라이프타임 매개변수를 다른 라이프타임 매개변수 또는 'static 라이프타임과 같도록 요구하는 경우입니다.
#![allow(unused)]
fn main() {
// The definition requires `'x` to be equal to `'static`.
type Opaque<'x: 'static> = impl Sized + 'x;
fn good() -> Opaque<'static> {}
}
동기: RPIT에 고유성 제약을 구현하려던 시도는 crater에서 발견된 호환성 깨짐으로 이어졌습니다. 규칙에 대한 이 예외를 통해 이 문제를 완화할 수 있습니다. 이 예외가 없다면 깨졌을 코드의 예시:
#![allow(unused)]
fn main() {
struct Type<'a>(&'a ());
impl<'a> Type<'a> {
// `'b == 'a`
fn do_stuff<'b: 'a>(&'b self) -> impl Trait<'a, 'b> {}
}
}
이것이 올바른 이유: Opaque<'a, 'a> := &'a str과 같은 정의 사용은 Opaque<'x, 'y> := &'x str으로도, Opaque<'x, 'y> := &'y str으로도 해석될 수 있지만, 적격성(well-formedness) 규칙에 따라 Opaque의 모든 사용이 두 매개변수가 같음을 보장하므로 어느 쪽으로 해석하든 상관없습니다.
보편 라이프타임 제약
불투명 타입 인자에는 보편 양화된(universally quantified) 라이프타임만 허용됩니다. 여기에는 라이프타임 매개변수와 플레이스홀더가 포함됩니다.
#![allow(unused)]
fn main() {
type Opaque<'x> = impl Sized + 'x;
fn test<'a>() -> Opaque<'a> {
// `Opaque<'empty> := ()`
let _: Opaque<'_> = ();
//~^ ERROR
}
}
동기: 이 제약으로 라이프타임 인자와 타입 인자가 일관되게 동작하게 되지만, 이는 부수적인 이점일 뿐입니다. 이 제약의 진짜 이유는 순전히 기술적인 것으로, 멤버 제약 알고리즘에 근본적인 한계가 있기 때문입니다. 불투명 타입 정의 Opaque<'?1> := &'?2 u8을 만나면 멤버 제약 '?2 member-of ['static, '?1]이 등록됩니다. 알고리즘이 올바른 선택을 하려면, 선택 영역 ['static, '?1] 사이의 “outlives” 관계의 완전한 집합이 영역 추론을 수행하기 전에 이미 알려져 있어야 합니다. 이는 각 선택 영역이 다음 중 하나인 경우에만 충족될 수 있습니다:
- 보편 영역, 즉
RegionKind::Re{EarlyParam,LateParam,Placeholder,Static}입니다. 보편 영역 간의 관계는 영역 추론 이전에 명시적 바운드와 암시적 바운드로부터 완전히 알려져 있기 때문입니다. - 또는 보편 영역과 “엄격하게 동일한” 존재 영역입니다. 엄격한 라이프타임 동일성은 아래에서 정의하며, 전체 영역 추론 이전에 평가할 수 있는 유일한 종류의 동일성이기 때문에 여기에서 필요합니다.
엄격한 라이프타임 동일성: 두 라이프타임 사이에 양방향 outlives 제약이 있으면 두 라이프타임이 엄격하게 동일하다고 합니다. NLL 용어로 말하면, 이는 두 라이프타임이 같은 SCC에 속한다는 뜻입니다. 중요한 점은 이 종류의 동일성은 전체 영역 추론 이전에(물론 제약 수집 이후에) 평가할 수 있다는 것입니다. 다른 종류의 동일성은 두 라이프타임 변수가 엄격하게 동일하지 않더라도 영역 추론 결과 같은 값을 갖게 되는 경우입니다. 예전에 이 차이를 어떻게 혼동했는지는 #113971을 참고하십시오.
“once modulo regions” 제약과의 상호작용 위 예제에서 시그니처의 불투명 타입은 Opaque<'a>이고 잘못된 정의 사용의 불투명 타입은 Opaque<'empty>라는 점에 유의하십시오. 제안된 MiniTAIT 계획, 즉 “once modulo regions” 규칙에서는 이미 이를 허용하지 않습니다. “보편 라이프타임” 제약은 “MiniTAIT” 제약으로부터 논리적으로 도출되므로 불필요해 보일 수도 있지만, 뒤이어 나오는 라이프타임 동일성과 클로저에 관한 논의는 여전히 유효합니다.
클로저 제약
불투명 타입이 클로저/코루틴/인라인 const 본문 안에서 정의될 때, 클로저에 대해 “외부“인 보편 라이프타임은 불투명 타입 인자로 허용되지 않습니다. 외부 영역은 RegionClassification::External에 정의되어 있습니다.
예제: (이 예제는 현재 nightly에서 우연히 컴파일되지만, 더 실용적인 예제들은 이미 혼란스러운 오류와 함께 거부됩니다.)
#![allow(unused)]
fn main() {
type Opaque<'x> = impl Sized + 'x;
fn test<'a>() -> Opaque<'a> {
let _ = || {
// `'a` is external to the closure
let _: Opaque<'a> = ();
//~^ Should be an error!
};
()
}
}
동기: 클로저 본문에서 외부 라이프타임은 “보편” 라이프타임으로 분류되지만, 서로 간의 관계를 미리 알 수 없다는 점에서 오히려 존재 라이프타임처럼 동작합니다. 그 대신 값은 존재 라이프타임과 똑같이 추론되며, 요구 사항은 부모 fn으로 다시 전파됩니다. 이는 앞서 설명한 멤버 제약 알고리즘을 깨뜨립니다:
알고리즘이 올바른 선택지를 고르려면, 영역 추론을 수행하기 전에 선택지 영역
['static, '?1]사이의 “outlives” 관계 전체가 이미 알려져 있어야 합니다.
다음은 그 과정을 자세히 보여 주는 예제입니다:
#![allow(unused)]
fn main() {
type Opaque<'x, 'y> = impl Sized;
//
fn test<'a, 'b>(s: &'a str) -> impl FnOnce() -> Opaque<'a, 'b> {
move || { s }
//~^ ERROR hidden type for `Opaque<'_, '_>` captures lifetime that does not appear in bounds
}
// The above closure body is desugared into something like:
fn test::{closure#0}(_upvar: &'?8 str) -> Opaque<'?6, '?7> {
return _upvar
}
// where `['?8, '?6, ?7]` are universal lifetimes *external* to the closure.
// There are no known relations between them *inside* the closure.
// But in the parent fn it is known that `'?6: '?8`.
//
// When encountering an opaque definition `Opaque<'?6, '?7> := &'8 str`,
// The member constraints algorithm does not know enough to safely make `?8 = '?6`.
// For this reason, it errors with a sensible message:
// "hidden type captures lifetime that does not appear in bounds".
}
이러한 제약이 없으면 오류 메시지가 혼란스러워지며, 더 중요하게는 클로저에서 멤버 제약이 심각하게 망가져 있기 때문에 향후 깨질 가능성이 높은 코드를 받아들이게 될 위험이 있습니다.
출력 타입: 실제 코드에서 이 문제가 가장 흔하게 발생하는 경우는 클로저/async 블록의 출력 타입이라고 생각합니다. 클로저와 async 블록 사이에는 이 문제를 더욱 잘 보여 주는 불일치가 있다는 점도 주목할 만하며, 이는 future에만 적용되는 replace_opaque_types_with_inference_vars 핵 때문입니다.
#![allow(unused)]
fn main() {
type Opaque<'x> = impl Sized + 'x;
fn test<'a>() -> impl FnOnce() -> Opaque<'a> {
// Output type of the closure is Opaque<'a>
// -> hidden type definition happens *inside* the closure
// -> rejected.
move || {}
//~^ ERROR expected generic lifetime parameter, found `'_`
}
}
#![allow(unused)]
fn main() {
use std::future::Future;
type Opaque<'x> = impl Sized + 'x;
fn test<'a>() -> impl Future<Output = Opaque<'a>> {
// Output type of the async block is unit `()`
// -> hidden type definition happens in the parent fn
// -> accepted.
async move {}
}
}