“필드 구성이 같다”와 “같은 도메인 개념을 나타낸다”는 서로 다른 약속입니다. 토파즈가 데이터 형식을 여러 가지 제공하는 이유가 여기에 있습니다. 프로그램이 지켜야 할 구분을 먼저 정하세요. 그다음 그 구분을 지켜 주는 가장 단순한 형식을 고릅니다.
예제 실행하기
다음 코드를 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 소유권식 수신자는 현재 언어에 포함되지 않습니다.
학습 과정의 데이터와 제어에서 먼저 감을 잡을 수 있습니다. 별칭과 유니온은 타입을 참고하세요. 완전한 패턴 검사와 바인딩은 패턴과 제어 흐름을 참고하세요.