대수적 데이터 타입과 상태 모델링
개요
대수적 데이터 타입(algebraic data type)은 여러 값을 함께 갖는 구조와, 여러 대안 중 하나를 고르는 구조를 조합해 데이터를 표현하는 방식이다. 이름의 대수적이라는 말은 값의 조합을 곱과 합으로 해석할 수 있다는 데서 나온다.
회원의 이름과 이메일을 함께 갖는 레코드는 두 필드의 조합이다. 반면 검색 결과가 대기·성공·실패 중 하나라면 각 경우에 필요한 데이터가 달라진다. 성공에는 결과 목록이 필요하고 실패에는 오류가 필요하다. 모든 필드를 선택 사항으로 둔 객체보다, 각 대안을 따로 정의하면 무엇이 함께 존재해야 하는지 타입에 나타낼 수 있다.
이런 모델은 값의 표현 공간을 설계하는 방법이다. 프로그램이 만들 수 있는 조합 중 업무에서 의미가 있는 조합을 골라 표현하고, 소비하는 코드는 각 경우에 맞는 필드를 다룬다. 흔히 “잘못된 상태를 표현하기 어렵게 만든다”는 설명이 이 지점에서 나온다.
타입은 데이터의 모양과 관계를 설명한다. 어떤 주문이 실제로 결제됐는지, 응답을 보낸 서버를 신뢰할 수 있는지 같은 사실은 실행 중 확인해야 한다. 상태의 표현, 전이 규칙, 외부 입력 검증을 각각 어디에서 맡는지 구분하면 타입 모델이 해결하는 범위가 분명해진다.
타입 추론과 데이터 선언의 서로 다른 계보 (1970년대)
함수형 언어의 역사에서 타입 추론과 대수적 데이터 타입은 함께 쓰이게 됐지만 출발점을 그대로 겹쳐 읽으면 안 된다. Robin Milner의 ML은 에든버러의 정리 증명기 LCF를 위한 메타 언어로 출발했고, 다형적 타입 추론의 발전에 큰 영향을 주었다.
Haskell 설계자들의 2007년 회고는 초기 ML에는 대수적 타입이 없었다고 구분한다. 같은 회고가 언어 기능으로서의 대수적 타입을 설명할 때 드는 선행 사례는 Burstall의 NPL(1977)과 Burstall·MacQueen·Sannella의 Hope(1980)다. ML의 기원과 ADT 선언의 계보를 구분해야 하는 이유다.1
생성자로 데이터의 경우를 선언하다 (1977~1980)
NPL과 Hope로 이어지는 흐름에서는 데이터가 어떤 경우들로 만들어지는지 언어 안에 선언하고, 각 경우를 패턴으로 분해하는 방식이 중요해진다. Haskell 회고는 더 앞선 Burstall의 구조적 귀납과 프로그램 변환 연구도 이 프로그래밍 방식의 배경으로 든다.1
Hope 논문은 사용자 정의 타입에서 서로 구별되는 생성자로 값의 모양을 정하고 각 경우를 다루려는 동기를 보여준다. 트리는 비어 있거나 값을 가진 끝점이거나 두 하위 트리를 가진 가지일 수 있다. 선언과 경우 분석이 이어지면 소비하는 코드는 데이터의 가능한 모양을 따라갈 수 있다.2
합과 곱은 이 데이터 구성의 관계를 설명한다. 태그로 구별되는 대안은 합이고, 한 대안 안에 함께 들어 있는 필드는 곱이다. 유한한 예에서 2개와 3개 값의 독립 필드는 6개 조합을 만들고, 태그가 다른 두 대안은 5개 경우를 만든다.
Standard ML·Miranda를 거쳐 Haskell의 선언으로
Haskell 회고는 초기 ML과 그 뒤의 Standard ML을 구별하고, Miranda도 대수적 타입을 가진 선행 언어로 기록한다. 이름이 비슷한 언어의 여러 판을 하나로 묶으면 이 변화가 사라진다.1
같은 회고에 따르면 Haskell은 Miranda에서 대수적 타입을 sum of products로 선언하는 방식을 받아들였다. Hope와 Standard ML에서 합 구조와 튜플 곱을 나누어 표현한 경우와 비교하면, 가지가 두 필드를 직접 받는지 두 하위 트리의 튜플 하나를 받는지에서 선언 형태가 달라진다.1
이 차이는 가능한 값들의 관계와 그것을 선언하는 언어 문법을 나누어 보게 한다. Haskell 문법은 그 관계를 표현하는 한 방식이며, 표기가 달라도 같은 데이터 구성을 표현할 수 있다.
공통 함수형 언어의 데이터 모델로 정리되다 (1987~1990)
Haskell은 비엄격 순수 함수형 언어들이 여럿 존재하던 상황에서 공통 언어를 만들려는 1987년 논의를 거쳐, 1990년 4월 1일 첫 보고서를 내놓았다. 대수적 데이터 선언은 함수·타입 추론·패턴 매칭과 함께 사용하는 구성 요소가 됐다.1
Haskell 2010 보고서는 이후의 공식 정의에서 생성자와 각 생성자의 필드로 데이터 타입을 선언하는 방식을 명시한다. 재귀적인 선언으로 리스트나 구문 트리를 만들고, 패턴 매칭으로 값의 형태에 맞는 계산을 선택할 수 있다.3
성공 값과 실패 값을 같은 결과 타입으로 다루려면 각 생성자에 필요한 데이터를 붙일 수 있다. “성공이면 결과가 있다”는 관계가 프로그램 곳곳의 암묵적인 약속에서 데이터 선언으로 드러난다.
유니온과 판별 속성으로 옮겨온 경우 분석 (2015~2016)
TypeScript 1.4는 2015년 유니온 타입을 도입했고, 2.0은 2016년 판별 속성으로 태그가 붙은 유니온을 좁히는 기능을 지원했다. 함수형 언어의 생성자 문법과는 다르지만, 데이터 대안을 선언하고 대안별 필드를 다루는 문제를 객체 타입 안에서 풀어낸다.45
상태 모델링에서는 로딩·성공·실패를 독립 필드의 우연한 조합으로 두는 대신 각 상태의 데이터를 함께 선언할 수 있다. 이전 결과를 유지한 채 다시 로딩하는 화면처럼 다른 조합이 필요하면 그 조합도 모델에 명시한다.
이런 쓰임은 타입 표기에서 상태의 표현 공간을 고르는 설계로 이어진다. 어떤 값이 가능한가와 어떤 사건 뒤에 다음 상태로 갈 수 있는가는 다른 질문이다. 전이 규칙은 함수와 모듈 경계에서 표현한다.
현대의 타입 모델이 맡는 경계
합 타입은 현재 데이터의 모양을 나타내고, 매개변수적 다형성은 그 구조를 여러 타입에 재사용하게 한다. Result<T, E>에서 성공·실패의 대안과 T·E의 재사용을 나누어 보면 두 개념의 역할이 보인다.
TypeScript에서는 타입 표기가 실행 시 제거되므로 외부 JSON의 필드는 실행 코드로 검사해야 한다. 타입 단언은 검증 코드를 만들지 않는다. 내부의 경우 분석에서는 never를 이용해 처리하지 않은 대안이 남는지 검사할 수 있다.67
판별 유니온으로 상태를 표현하는 작은 예제
다음 코드는 앞에서 살펴본 경우 분석을 TypeScript의 검색 상태로 옮긴 예다. 성공에는 결과가, 실패에는 오류가 있다는 관계만 표현하며 실제 검색 API는 포함하지 않는다.
type Search =
| { kind: 'loading' }
| { kind: 'ready'; data: string[] }
| { kind: 'failed'; error: string };
function describe(state: Search): string {
switch (state.kind) {
case 'loading': return 'Loading';
case 'ready': return state.data.join(', ');
case 'failed': return state.error;
}
const impossible: never = state;
return impossible;
}
console.log(describe({ kind: 'ready', data: ['Ada', 'Grace'] }));
출력은 Ada, Grace다. strict 검사에서 { kind: 'ready' }는 필수 결과를 빠뜨린 값으로 거절된다. 마지막 never 대입은 새 상태를 추가하고도 분기를 빠뜨렸을 때 오류가 나도록 한다.
각주
- Paul Hudak·John Hughes·Simon Peyton Jones·Philip Wadler, 「A History of Haskell: Being Lazy With Class」, HOPL III, 2007, 도입부 및 §5–5.1. NPL·Hope·초기 ML·Miranda의 계보와 Haskell의 설계 선택에 관한 설계자 회고. 원문. ↩
- R. M. Burstall·D. B. MacQueen·D. T. Sannella, 「HOPE: An Experimental Applicative Language」, LISP Conference, 1980, §2–3.1. 저자 대학 공개본. ↩
- Haskell 2010 Language Report, §4.2.1 “Algebraic Datatype Declarations”. 공식 보고서. ↩
- Microsoft, 「Announcing TypeScript 1.4」, 2015-01-16. 당시 발표. ↩
- Microsoft, TypeScript 2.0, 2016, “Tagged union types”. 릴리스 노트. ↩
- TypeScript Handbook, “Type Assertions”. 공식 문서. ↩
- TypeScript Handbook, “Exhaustiveness checking”. 공식 문서. ↩