프로그램 구조

Null·Option·결과·실행 오류

값이 없는 정상 상황, null이 들어오는 데이터, 복구 가능한 실패, 런타임 계약 위반을 각각 어떻게 표현할지 정리합니다.

값이 없을 수 있거나 작업이 실패할 수 있다면 이 페이지를 보세요. 먼저 던져야 할 질문은 “어느 연산자가 더 짧은가?”가 아닙니다. “호출자가 계속 진행할 수 있는가?”입니다.

의미에 맞춰 선택하기

상황사용할 형식
토파즈 안에서 값이 없는 것이 정상적인 결과Some(value) 또는 None을 담는 Option<T>
외부 데이터가 명시적으로 null 허용 필드를 가짐T | null
호출자가 오류의 이유를 보고 대응할 수 있음Ok(value) 또는 Err(error)를 담는 Result<T, E>
프로그램이 런타임 계약을 위반함fault. 값을 만들지 않고 실행을 중단함

토파즈 API에서 값이 없는 것이 정상이라면 Option으로 표현하세요. null이 실제로 들어 있는 데이터와 상호 운용 경계에만 null을 남겨 두세요. 호출자가 실패 이유를 알아야 하거나 복구 여부를 판단해야 하면 Result를 사용합니다.

잘못된 입력을 복구 가능한 결과로 바꾸기

다음은 그대로 실행되는 완전한 프로그램입니다. toInt가 값을 만들지 못한 상황을 이유가 담긴 Result로 바꿉니다.

TOPAZ
record StudyTask {
    title: string,
    minutes: int,
}

function parseMinutes(text: string) -> Result<int, string> {
    match toInt(text) {
        case Some(value) if value > 0 => Ok(value)
        case _ => Err("minutes must be a positive integer")
    }
}

function makeTask(title: string, minuteText: string) -> Result<StudyTask, string> {
    let minutes = parseMinutes(minuteText)?
    Ok(StudyTask { title: title, minutes: minutes })
}

function inspect(minuteText: string) -> () {
    print("input: {minuteText}")
    defer { print("inspection finished") }
    match makeTask("Build application", minuteText) {
        case Ok(task) => print("ready: {task.title}, {task.minutes} minutes")
        case Err(message) => print("cannot add task: {message}")
    }
}

inspect("25")
inspect("later")

검사하고 실행합니다.

BASH
topaz check failures-resources.tpz
topaz run failures-resources.tpz
출력
input: 25
ready: Build application, 25 minutes
inspection finished
input: later
cannot add task: minutes must be a positive integer
inspection finished

정수로 읽을 수 없는 텍스트는 값을 만들어 내지 못합니다. 그래서 toIntOption<int>를 반환합니다. parseMinutes는 값이 없다는 사실을 호출자가 알아야 한다고 보고 그것을 Err로 바꿉니다. makeTask의 후위 ?Ok 안에 든 값을 꺼냅니다. Err를 만나면 그 오류를 함수 밖으로 곧바로 반환합니다. 이때 감싸는 함수의 반환 타입은 그 오류를 담을 수 있는 Result여야 합니다. 마지막 match는 두 결과를 하나씩 직접 처리합니다.

선택 값과 null 허용 값 다루기

선택 접근과 기본값 연산은 값이 없는 두 방식을 서로 섞지 않습니다.

  • maybeUser: Option<{ name: string }>이면 maybeUser?.name의 결과도 Option입니다. maybeUser?.name ?? "guest"string을 만듭니다.
  • configPath: string | null이면 configPath ?? "default.toml"은 값이 null일 때만 기본값을 씁니다.
  • target ??= value는 대상이 비었을 때만 대입하는 문장입니다. Option<int> 대상에는 42가 아니라 Some(42)가 필요합니다. 토파즈는 Some을 자동으로 씌우지 않습니다.

?.??는 모두 왼쪽을 한 번만 평가합니다. ?.은 수신자가 비어 있으면 메서드 인자도 평가하지 않습니다. 각 연산자는 선택 레이어를 한 겹만 풀거나 평탄화합니다. Option을 슬쩍 null로 바꾸는 일은 없습니다.

흔한 실수 바로잡기. 값이 선택형이나 null 허용형이 아니라면 보통의 .을 쓰세요. 타입에 Option<T>null이 함께 들어 있다면 단축 연산자를 겹쳐 쓰지 마세요. 각 경우를 match로 하나씩 밝히는 편이 낫습니다.

fault는 오류 값이 아닙니다

다음 프로그램은 배열에 없는 원소를 꺼내려 합니다.

TOPAZ
let tasks = ["Run first program", "Build application"]
print(tasks[2])

topaz run intentional-fault.tpz를 실행하면 종료 코드가 0이 아닙니다.

출력
error[TPZ4001]: index 2 is out of bounds for an array of length 2
 --> intentional-fault.tpz:2:7
  |
2 | print(tasks[2])
  |       ^^^^^^^^

범위 위반은 fault입니다. fault는 실행을 그 자리에서 멈춥니다. None이나 Err 같은 값을 만들지 않습니다. 후위 ?, ??, 선택 체인, concurrent는 fault를 잡거나 다른 값으로 바꾸지 않습니다. 잘못된 상수 산술처럼 검사 시점에 미리 알 수 있는 형태도 있습니다. 이런 코드는 실행 전에 정적 오류로 거부될 수 있습니다.

정확한 경계

  • 후위 ?는 같은 오류 값을 담을 수 있는 반환 타입을 가진 함수나 클로저 안에서만 쓸 수 있습니다.
  • 일반 try, 예외, fault 잡기, panic 키워드, 자동 재시도는 현재 토파즈 형식이 아닙니다.
  • assert(...)test-profile을 명시적으로 선택한 코드에서만 쓸 수 있습니다. 일반 프로그램용 단언문이 아닙니다.
  • 스코프가 정상적으로 끝날 때의 정리는 defer가 맡습니다. fault가 났을 때는 정리 경계가 더 좁습니다. 그러니 fault를 복구 가능한 Result처럼 다루면 안 됩니다.

따라 하는 연습은 실패와 리소스에서 할 수 있습니다. 정리 시점은 Defer와 리소스에서 다룹니다. 런타임 fault 경계는 실행 환경 동작에서 이어서 확인하세요.