데이터와 제어

레코드와 명목 데이터

구조적 레코드, 명목 레코드, 열거형, 유니온, 뉴타입을 목적에 맞게 선택하는 방법.

이 페이지는 토파즈 v5.17을 설명합니다. 현재 문서로 판단하기 전에 /version.json의 version 값을 5.17과 대조하세요.

“필드 구성이 같다”와 “같은 도메인 개념을 나타낸다”는 서로 다른 약속입니다. 토파즈가 데이터 형식을 여러 가지 제공하는 이유가 여기에 있습니다. 프로그램이 지켜야 할 구분을 먼저 정하세요. 그다음 그 구분을 지켜 주는 가장 단순한 형식을 고릅니다.

예제 실행하기

다음 코드를 records-nominal-data.tpz로 저장합니다.

TOPAZ
type 표시값 = int | string

newtype 사용자번호 = int

enum 상태 {
    활성,
    일시정지(string),
}

record 사용자 {
    번호: 사용자번호,
    이름: string,
    상태값: 상태 = 상태.활성,
}

function 상태표시(상태값: 상태) -> string {
    match 상태값 {
        case 활성 => "active"
        case 일시정지(이유) => 이유
    }
}

function 표시(: 표시값) -> string {
    match {
        case 숫자: int => "number {숫자}"
        case 텍스트: string => 텍스트
    }
}

let 이전: 사용자 = 사용자 { 번호: 사용자번호(7), 이름: "Ada" }
let 이후: 사용자 = 사용자 { ...이전, 상태값: 상태.일시정지("review") }
let 미리보기 = { 이름: "Ada", 나이: 36 }
let 나이든 = 미리보기{ 나이: 37 }
let 준비 = 표시("ready")

print("{이전.번호.value()}:{이전.이름}:{상태표시(이전.상태값)}")
print("{이후.이름}:{상태표시(이후.상태값)}")
print("{미리보기.나이}:{나이든.나이}")
print("{표시(7)}:{준비}")
BASH
topaz check records-nominal-data.tpz
topaz run records-nominal-data.tpz

출력은 다음과 같습니다.

출력
7:Ada:active
Ada:review
36:37
number 7:ready

데이터 형식 고르기

형식알맞은 상황생성하고 확인하는 방법
구조적 레코드지역적으로 사용할 값에 정해진 필드만 필요할 때{ 이름: "Ada", 나이: 36 }로 만들고 값.필드로 읽기
명목 record도메인 이름과 선언 정체성이 중요할 때사용자 { ... }; 필드, 기본값, 이름이 있는 갱신, 레코드 패턴
enum닫힌 선택지 가운데 정확히 하나를 나타낼 때상태.활성 또는 상태.일시정지(이유); match로 선택
유니온 A | B기존 값이 여러 타입 가운데 하나일 수 있을 때리터럴·생성자·타입 패턴으로 범위 좁히기
newtype하나의 기반 값에 별도 도메인 정체성이 필요할 때사용자번호(7)로 만들고 .value()로 한 겹 벗기기

사용자는 명목 레코드입니다. 상태값을 적지 않으면 기본값을 한 번 계산합니다. 그래서 이전은 활성 상태가 됩니다. 사용자 { ...이전, 상태값: ... }는 바깥쪽 사용자를 새로 만들고 나머지 필드는 그대로 가져옵니다. 이전을 바꾸지 않고 기본값도 다시 계산하지 않습니다.

상태는 선택지가 닫혀 있는 열거형입니다. 값을 만들 때는 상태.일시정지처럼 이름공간을 붙입니다. 패턴에서는 case 일시정지(이유)처럼 변형 이름만 씁니다. 그래서 검사기는 상태표시가 모든 변형을 다루는지 확인할 수 있습니다.

표시값은 유니온에 붙인 별칭입니다. 실행할 때 값을 새로 포장하지 않습니다. 표시의 타입 패턴은 원래 있던 intstring으로 범위를 좁히고 이름을 붙입니다. 사용자번호는 반대입니다. 기반 타입이 int여도 별도의 명목 정체성을 가진 새 포장입니다.

미리보기는 구조적 레코드입니다. 그래서 호환되는 필드 모양이 곧 정체성입니다. 갱신은 미리보기{ 나이: 37 }처럼 씁니다. 사용자에 쓴 명목 갱신과는 다른 연산입니다.

생성·갱신·패턴 검사

명목 레코드는 필드의 이름과 타입을 검사합니다. 직접 적은 필드는 왼쪽에서 오른쪽으로 계산합니다. 빠뜨린 필드의 기본값은 그다음에 선언 순서대로 계산합니다. 명목 갱신에는 맨 앞의 ...source 하나와 바꿀 필드만 쓸 수 있습니다. 이때 바깥 레코드만 얕게 복사합니다. 그래서 필드 안에 든 변경 가능한 값은 계속 함께 씁니다.

case 사용자 { 이름 } => 같은 명목 레코드 패턴은 값이 사용자 선언에서 나왔는지 먼저 확인합니다. 그다음 고른 필드를 읽습니다. 적지 않은 필드는 무시합니다. 다만 필드를 적어도 하나는 지정해야 합니다. 필드가 똑같은 구조적 레코드도 사용자로 보지 않습니다.

흔한 실수

JavaScript에서 쓰는 { ...값 }는 토파즈의 구조적 레코드 갱신이 아닙니다. 구조적 레코드는 값{ 필드: 대체값 }로 갱신합니다. 명목 레코드는 이름 { ...값, 필드: 대체값 }로 갱신합니다.

레코드 본문에 Rust식 수신자 문법을 넣지 마세요. 암시적인 this, &self, &mut self는 없습니다. 수신자 메서드가 필요하면 모듈 최상위에 impl 이름 블록을 따로 두세요. 그 안에서는 값으로 받는 self만 씁니다.

정확한 경계

  • 레코드, 열거형, 뉴타입 선언은 모듈 최상위에 두는 명목 선언입니다.
  • 제네릭 명목 타입의 매개변수는 불변입니다. 값을 만들 때는 기대 타입이나 허용된 다른 추론 문맥이 있어야 합니다.
  • 명목 값은 동등성 비교, 순서, 컬렉션 키에서 내용뿐 아니라 선언 정체성도 지킵니다.
  • 열거형 패턴은 변형 이름만 씁니다. 가드가 없는 가지들이 모든 변형을 다루는지 검사합니다.
  • 명목 레코드 패턴의 머리에는 이름공간을 붙이지 않은 이름을 씁니다. 비어 있는 명목 패턴은 쓸 수 없습니다.
  • newtype 생성자는 기반 값 하나만 받습니다. .value()는 정확히 한 겹만 벗깁니다.

리플렉션, 상속, 행 다형성, 열린 명목 패턴, 이름공간을 붙인 생성, 동적 프로토콜 디스패치, Rust 소유권식 수신자는 현재 언어에 포함되지 않습니다.

학습 과정의 데이터와 제어에서 먼저 감을 잡을 수 있습니다. 별칭과 유니온은 타입을 참고하세요. 완전한 패턴 검사와 바인딩은 패턴과 제어 흐름을 참고하세요.