하위 타입과 분산

하위 타입과 분산 (Subtyping and variance)

Rust의 타입 시스템에는 하위 타입 관계(subtyping)가 존재해요. 다만 이 관계는 명시적으로 선언하는 게 아니라 암시적으로, 타입 검사나 추론의 어느 단계에서든 발생할 수 있어요. 이 글에서는 하위 타입 관계와 함께, 제네릭 타입의 성질인 분산(variance)까지 정리할게요.

출처: Rust Reference

본문

하위 타입 관계는 두 경우로 제한돼요. 하나는 수명에 대한 분산(variance), 다른 하나는 더 높은 등급의 수명(higher-ranked lifetimes)을 가진 타입 사이의 관계예요. 타입에서 수명을 모두 지워 버리면, 하위 타입 관계는 오직 타입 동등성(type equality)에서만 나오게 돼요.

다음 예시를 볼게요. 문자열 리터럴은 항상 'static 수명을 가져요. 그런데도 우리는 st에 할당할 수 있죠.

#![allow(unused)]
fn main() {
fn bar<'a>() {
    let s: &'static str = "hi";
    let t: &'a str = s;
}
}

'static이 수명 매개변수 'a보다 오래 살기 때문에(더 길기 때문에), &'static str&'a str의 하위 타입이 돼요.

더 높은 등급의 함수 포인터와 트레이트 객체 (Higher-ranked)

더 높은 등급의 함수 포인터와 트레이트 객체도 또 하나의 하위 타입 관계를 가져요. 이들은 더 높은 등급의 수명이 치환(substitution)된 타입들의 하위 타입이 돼요. 몇 가지 예시를 볼게요.

#![allow(unused)]
fn main() {
// Here 'a is substituted for 'static
let subtype: &(for<'a> fn(&'a i32) -> &'a i32) = &((|x| x) as fn(&_) -> &_);
let supertype: &(fn(&'static i32) -> &'static i32) = subtype;

// This works similarly for trait objects
let subtype: &(dyn for<'a> Fn(&'a i32) -> &'a i32) = &|x| x;
let supertype: &(dyn Fn(&'static i32) -> &'static i32) = subtype;

// We can also substitute one higher-ranked lifetime for another
let subtype: &(for<'a, 'b> fn(&'a i32, &'b i32)) = &((|x, y| {}) as fn(&_, &_));
let supertype: &for<'c> fn(&'c i32, &'c i32) = subtype;
}

for<'a> ...처럼 구체 수명 대신 모든 수명에 대해 일반화된 타입이 구체 타입의 하위 타입이 되는 원리예요. 반대 방향은 성립하지 않아요.

분산 (Variance)

분산은 제네릭 타입이 **자기 인자(arguments)**에 대해 가지는 성질이에요. 제네릭 타입이 매개변수에서 가지는 분산은, 매개변수의 하위 타입 관계가 타입 전체의 하위 타입 관계에 어떤 영향을 주는지를 나타내요.

  • 공변(covariant): TU의 하위 타입이면 F<T>F<U>의 하위 타입이 되는 경우 (하위 타입 관계가 "통과" 한다고 표현해요)
  • 반변(contravariant): TU의 하위 타입이면 F<U>F<T>의 하위 타입이 되는 경우
  • 불변(invariant): 그 외의 경우 (하위 타입 관계를 유도할 수 없어요)

내장 타입의 분산은 자동으로 다음처럼 결정돼요.

타입 'a에서의 분산 T에서의 분산
&'a T 공변 공변
&'a mut T 공변 불변
*const T 공변
*mut T 불변
[T] and [T; n] 공변
fn() -> T 공변
fn(T) -> () 반변
std::cell::UnsafeCell<T> 불변
std::marker::PhantomData<T> 공변
dyn Trait<T> + 'a 공변 불변

사용자 정의 복합 타입 (User composite types)

다른 struct, enum, union 타입의 분산은 필드 타입들의 분산을 살펴보아 결정돼요. 매개변수가 서로 다른 분산을 가진 위치들에서 사용된다면 그 매개변수는 불변이 돼요.

예를 들어 다음 struct는 'aT에서는 공변이고, 'b, 'c, U에서는 불변이에요.

#![allow(unused)]
fn main() {
use std::cell::UnsafeCell;
struct Variance<'a, 'b, 'c, T, U: 'a> {
    x: &'a U,               // This makes `Variance` covariant in 'a, and would
                            // make it covariant in U, but U is used later
    y: *const T,            // Covariant in T
    z: UnsafeCell<&'b f64>, // Invariant in 'b
    w: *mut U,              // Invariant in U, makes the whole struct invariant

    f: fn(&'c ()) -> &'c () // Both co- and contravariant, makes 'c invariant
                            // in the struct.
}
}

x: &'a UU가 공변 위치에 쓰이지만, 뒤에서 w: *mut UU를 불변 위치에 쓰면서 전체 struct가 U에 대해 불변이 되는 흐름을 눈여겨볼게요.

내장 복합 타입 밖에서 (Builtin composite types outside)

struct, enum, union 밖에서 사용될 때는(예: 함수 시그니처 안의 튜플 같은 위치), 각 위치에서 매개변수의 분산을 개별적으로 계산해요.

#![allow(unused)]
fn main() {
use std::cell::UnsafeCell;
fn generic_tuple<'short, 'long: 'short>(
    // 'long is used inside of a tuple in both a co- and invariant position.
    x: (&'long u32, UnsafeCell<&'long u32>),
) {
    // As the variance at these positions is computed separately,
    // we can freely shrink 'long in the covariant position.
    let _: (&'short u32, UnsafeCell<&'long u32>) = x;
}

fn takes_fn_ptr<'short, 'middle: 'short>(
    // 'middle is used in both a co- and contravariant position.
    f: fn(&'middle ()) -> &'middle (),
) {
    // As the variance at these positions is computed separately,
    // we can freely shrink 'middle in the covariant position
    // and extend it in the contravariant position.
    let _: fn(&'static ()) -> &'short () = f;
}
}

각 위치의 분산이 따로 계산되기 때문에, 공변 위치에서는 수명을 자유롭게 줄일 수 있고 반변 위치에서는 늘릴 수 있어요.

더 알아보기 (Learn more)