“필드 구성이 같다”와 “같은 도메인 개념을 나타낸다”는 서로 다른 약속입니다. 토파즈가 여러 데이터 형식을 제공하는 이유도 여기에 있습니다. 프로그램이 지켜야 할 구분부터 정한 뒤, 그 구분을 보존하는 가장 단순한 형식을 고르세요.
예제 실행하기
다음 코드를 records-nominal-data.tpz로 저장합니다.
type DisplayValue = int | string
newtype UserId = int
enum Status {
Active,
Paused(string),
}
record User {
id: UserId,
name: string,
status: Status = Status.Active,
}
function statusLabel(status: Status) -> string {
match status {
case Active => "active"
case Paused(reason) => reason
}
}
function display(value: DisplayValue) -> string {
match value {
case number: int => "number {number}"
case text: string => text
}
}
let before: User = User { id: UserId(7), name: "Ada" }
let after: User = User { ...before, status: Status.Paused("review") }
let preview = { name: "Ada", age: 36 }
let older = preview{ age: 37 }
let ready = display("ready")
print("{before.id.value()}:{before.name}:{statusLabel(before.status)}")
print("{after.name}:{statusLabel(after.status)}")
print("{preview.age}:{older.age}")
print("{display(7)}:{ready}")topaz check records-nominal-data.tpz
topaz run records-nominal-data.tpz
출력은 다음과 같습니다.
7:Ada:active
Ada:review
36:37
number 7:ready데이터 형식 고르기
| 형식 | 알맞은 상황 | 생성하고 확인하는 방법 |
|---|---|---|
| 구조적 레코드 | 지역적으로 사용할 값에 정해진 필드만 필요할 때 | { name: "Ada", age: 36 }로 만들고 value.field로 읽기 |
명목 record | 도메인 이름과 선언 정체성이 중요할 때 | User { ... }; 필드, 기본값, 이름이 있는 갱신, 레코드 패턴 |
enum | 닫힌 선택지 가운데 정확히 하나를 나타낼 때 | Status.Active 또는 Status.Paused(reason); match로 선택 |
유니온 A | B | 기존 값이 여러 타입 가운데 하나일 수 있을 때 | 리터럴·생성자·타입 패턴으로 범위 좁히기 |
newtype | 하나의 기반 값에 별도 도메인 정체성이 필요할 때 | UserId(7)로 만들고 .value()로 한 겹 벗기기 |
User는 명목 레코드입니다. status를 생략하면 기본값을 한 번 계산하므로 before는 활성 상태가 됩니다. User { ...before, status: ... }는 바깥쪽 User를 새로 만들고 나머지 필드는 유지합니다. before를 바꾸거나 기본값을 다시 계산하지 않습니다.
Status는 선택지가 닫힌 열거형입니다. 만들 때는 Status.Paused처럼 이름공간을 붙이고, 패턴에서는 case Paused(reason)처럼 변형 이름만 씁니다. 따라서 검사기는 statusLabel이 모든 변형을 다루는지 확인할 수 있습니다.
DisplayValue는 유니온 별칭일 뿐 새로운 실행 값 포장이 아닙니다. display의 타입 패턴은 기존 int나 string으로 범위를 좁히고 이름을 붙입니다. 반대로 UserId는 기반 타입이 int여도 별도의 명목 정체성을 가진 새 포장입니다.
preview는 구조적 레코드이므로 호환되는 필드 형태가 곧 정체성입니다. 갱신 표기도 preview{ age: 37 }입니다. User에 사용한 명목 갱신과는 다른 연산입니다.
생성·갱신·패턴 검사
명목 레코드의 필드는 이름과 타입을 검사합니다. 명시한 필드는 왼쪽에서 오른쪽으로 계산하고, 빠진 필드의 기본값은 그 뒤에 선언 순서대로 계산합니다. 명목 갱신은 맨 앞의 ...source 하나와 교체할 필드만 허용합니다. 바깥 레코드만 얕게 복사하므로 필드 안의 변경 가능한 값은 계속 공유됩니다.
case User { name } => 같은 명목 레코드 패턴은 값이 User 선언에서 만들어졌는지 먼저 확인한 다음 선택한 필드를 읽습니다. 쓰지 않은 필드는 무시하지만 적어도 한 필드는 지정해야 합니다. 필드가 똑같은 구조적 레코드도 User로 취급되지 않습니다.
흔한 실수
JavaScript식 { ...value }는 토파즈의 구조적 레코드 갱신이 아닙니다. 구조적 레코드는 value{ field: replacement }, 명목 레코드는 Name { ...value, field: replacement }를 사용합니다.
레코드 본문에 Rust식 수신자 문법을 넣지 마세요. 암시적인 this, &self, &mut self는 없습니다. 수신자 메서드가 필요하다면 모듈 최상위의 별도 impl Name 블록에서 값으로 받는 self만 사용합니다.
정확한 경계
- 레코드·열거형·뉴타입 선언은 모듈 최상위의 명목 선언입니다.
- 제네릭 명목 타입의 매개변수는 불변이며, 생성 시 기대 타입이나 허용된 다른 추론 문맥이 필요합니다.
- 명목 값의 동등성·순서·컬렉션 키는 내용뿐 아니라 선언 정체성도 보존합니다.
- 열거형 패턴은 변형 이름만 쓰며, 가드 없는 가지로 모든 변형을 다루는지 검사합니다.
- 명목 레코드 패턴의 머리는 이름공간을 붙이지 않은 이름이어야 하고, 비어 있는 명목 패턴은 허용하지 않습니다.
newtype생성자는 기반 값 하나만 받으며.value()는 정확히 한 겹만 벗깁니다.
반사, 상속, 행 다형성, 열린 명목 패턴, 이름공간을 붙인 생성, 동적 프로토콜 디스패치, Rust 소유권식 수신자는 현재 언어에 포함되지 않습니다.
학습 과정의 데이터와 제어에서 먼저 감을 잡을 수 있습니다. 별칭과 유니온은 타입, 완전한 패턴 검사와 바인딩은 패턴과 제어 흐름을 참고하세요.