발산(Divergence)

발산(Divergence)

발산 표현식(diverging expression)이란 정상적인 실행을 끝내지 않는 표현식을 말해요. 코드로 보면 바로 와닿을 거예요.

#![allow(unused)]
fn main() {
fn diverges() -> ! {
    panic!("This function never returns!");
}

fn example() {
    let x: i32 = diverges(); // 이 줄은 끝까지 실행되지 않아요.
    println!("This is never printed: {x}");
}
}

여기서 diverges()panic! 때문에 결코 값을 반환하지 않으니, 그 뒤의 println!은 영원히 실행되지 않죠. 이런 표현식을 발산한다고 해요.

각 표현식이 어떤 조건에서 발산하는지는 다음 규칙을 참고하면 돼요.

  • 블록 표현식 — 블록이 끝까지 도달하지 않는 경우
  • if 표현식 — 모든 분기가 발산하는 경우
  • break가 있는 레이블 블록 표현식
  • break가 있는 loop 표현식
  • break 표현식
  • continue 표현식
  • 무한 loop 표현식
  • match 표현식 — 팔(arm) 중 어느 것도 값에 도달하지 않는 경우
  • match 표현식
  • return 표현식
  • !를 반환하는 함수 호출

참고

panic! 매크로와 unreachable!처럼 panic을 일으키는 관련 매크로도 타입이 !이고 발산해요.

출처: Rust Reference

본문

언제 발산한다고 말하나요

타입이 !인 표현식은 모두 발산 표현식이에요. 그런데 발산 표현식이 ! 타입으로만 제한되는 건 아니에요. 예를 들어 Some(loop {})은 타입이 Option<!>이지만 그 자체로 발산하는 표현식이죠.

여기서 헷갈리기 쉬운 부분이 하나 있어요. !는 비어 있는 타입(uninhabited type)으로 간주되지만, 타입이 비어 있다고 해서 반드시 발산하는 건 아니에요. 아래 예시를 볼게요.

#![allow(unused)]
fn main() {
enum Empty {}
fn make_never() -> ! {loop{}}
fn make_empty() -> Empty {loop{}}

fn diverging() -> ! {
    // 이 표현식은 타입이 `!`예요.
    // 그래서 함수 전체가 발산하는 것으로 간주돼요.
    make_never();
    // OK: 본문의 타입이 `!`로 반환 타입과 일치해요.
}
fn not_diverging() -> ! {
    // 이 타입은 비어 있어요(uninhabited).
    // 하지만 함수 전체는 발산하는 것으로 간주되지 않아요.
    make_empty();
    // ERROR: 본문 타입이 `()`인데 기대 타입은 `!`예요.
}
}

make_never()처럼 타입이 !인 호출은 발산으로 보고, make_empty()처럼 Empty 타입은 비어 있지만 !가 아니므로 발산으로 보지 않아요. 그래서 not_diverging은 반환 타입 !와 본문 타입 ()이 맞지 않아 컴파일 오류가 나죠.

발산은 주변 블록으로 퍼져나갈 수도 있어요.

후보(fallback) 타입 추론

추론해야 할 타입이 오직 발산 표현식하고만 통일(unified)된다면, 그 타입은 !로 추론돼요.

#![allow(unused)]
fn main() {
fn foo() -> i32 { 22 }
match foo() {
    // ERROR: 트레잇 바운드 `!: Default`를 만족하지 못해요.
    4 => Default::default(),
    _ => return,
};
}

여기서 _ => return 분기가 발산이라서, match 전체의 타입을 !로 추론하려고 해요. 그런데 다른 분기에서 Default::default()를 요구하니 !Default 트레잇을 만족해야 하는데 그러지 못해서 오류가 나는 거예요.

2024 에디션 차이

2024 에디션 이전에는 이 경우 타입이 ()로 추론됐어요.

중요한 점은 타입 통일이 구조적으로 일어날 수 있다는 거예요. 그래서 후보 !가 더 큰 타입의 일부가 될 수도 있어요. 아래 코드는 컴파일되는데, _ => Some(return) 때문에 match 전체가 Option<!>가 되거든요.

#![allow(unused)]
fn main() {
fn foo() -> i32 { 22 }
// 이 표현식은 `!`가 아니라 `Option<!>` 타입이에요.
match foo() {
    4 => Default::default(),
    _ => Some(return),
};
}

더 알아보기

  • !(never) 타입: 발산의 기반이 되는 타입
  • 블록, if, loop, match, return 표현식의 각 발산 규칙
  • panic과 관련 매크로의 동작