−1. ML에서 Haskell로 이어진 데이터 선언
대안을 가진 데이터와 패턴 매칭은 ML 계열과 이후 함수형 언어에서 중요한 구성 요소로 발전했다. Haskell 설계자들이 쓴 2007년 회고 논문도 Haskell의 데이터 타입을 선행 언어의 계보 안에서 설명한다. 특정 현대 언어가 처음 만든 기능으로 보기보다, 생성자로 값을 만들고 패턴으로 경우를 나누는 언어 설계가 이어진 과정으로 읽을 수 있다.[각주 1]
ML은 1970년대 Robin Milner가 에든버러의 정리 증명기 LCF를 위한 메타 언어로 개발한 계보에서 출발했다. 이후 Haskell은 비엄격 순수 함수형 언어들이 여럿 존재하던 상황에서 공통 언어를 만들려는 1987년의 논의를 거쳐, 1990년 4월 1일 첫 보고서를 내놓았다. 데이터 선언은 이 과정에서 함수·타입 추론·패턴 매칭과 함께 사용할 언어의 구성 요소로 이어졌다.[각주 1]
Haskell 2010 보고서의 data 선언은 여러 생성자와 각 생성자가 받는 필드로 타입을 정의한다. 하나의 타입 안에서 서로 다른 모양의 값이 대안으로 존재한다. 재귀적인 생성자를 사용하면 리스트나 구문 트리처럼 크기가 정해지지 않은 구조도 표현한다.[각주 2]
이 계보에서 데이터 선언은 단순히 이름이 붙은 상수 목록을 만드는 데 그치지 않는다. 각 경우가 자기 데이터를 갖는다는 점이 중요하다. 실패 이유를 담는 실패 생성자와 결과를 담는 성공 생성자를 같은 결과 타입의 대안으로 둘 수 있다.
2. 합과 곱이라는 이름의 의미
곱 타입(product type)은 여러 필드가 함께 있는 값이다. 가능한 값이 2개인 필드와 3개인 필드를 독립적으로 조합하면 가능한 조합은 2 × 3 = 6개다. 튜플과 레코드가 이 구조를 보여준다.
합 타입(sum type)은 태그로 구별되는 여러 대안 중 하나인 값이다. 한 대안의 가능한 값이 2개이고 다른 대안은 3개라면 구별되는 전체 경우는 2 + 3 = 5개다. 태그가 있으므로 서로 다른 대안에서 같은 내용의 값을 사용해도 어느 경우인지 구분할 수 있다.
이 계산은 유한한 예제로 구조를 설명한 것이다. 실제 언어의 메모리 크기를 그대로 더하고 곱한다는 뜻은 아니다. 컴파일러는 태그·정렬·포인터·표현 최적화 등 언어와 구현에 맞는 방식으로 값을 배치한다.
합 안에 곱을 넣으면 “성공이면 결과와 갱신 시각을 함께 갖고, 실패면 오류를 갖는다”는 모델이 된다. 프로그램에서 필요한 관계가 데이터 선언의 구조에 드러난다.
3. 일반적인 객체 조합에서 상태의 대안으로
loading: boolean, data?: string[], error?: string을 독립 필드로 두면 코드가 여러 조합을 만들 수 있다. 로딩 중에 결과와 오류를 모두 보관하는 상태가 제품에 필요할 수도 있지만, 그런 화면을 의도하지 않았다면 각 소비자가 어떤 조합을 믿을지 판단해야 한다.
상태를 대안으로 정하면 그 판단을 데이터 선언에 모을 수 있다. 새 검색 중 이전 결과를 유지하는 화면이 필요하다면 그 경우에 맞는 필드나 별도 대안을 추가하면 된다. 목표는 상태 수를 무조건 줄이는 것이 아니라, 제품이 사용하는 조합을 명확하게 표현하는 것이다.
4. TypeScript에서 판별 유니온으로
TypeScript 1.4는 2015년에 유니온 타입을 도입했고, 2.0은 2016년에 판별 속성에 따른 tagged union 처리를 지원했다. 같은 문자열 속성의 값을 보고 각 대안의 타입을 좁힐 수 있게 된 것이다.[각주 3][각주 4]
다음 예제는 성공 상태에는 결과가 있고 실패 상태에는 오류가 있다는 관계만 표현한다. 복잡한 검색 API 없이 선언과 분기가 어떻게 연결되는지 보기 위한 코드다.
+타입 추론과 데이터 선언의 서로 다른 계보 (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년 판별 속성으로 태그가 붙은 유니온을 좁히는 기능을 지원했다. 함수형 언어의 생성자 문법과는 다르지만, 데이터 대안을 선언하고 대안별 필드를 다루는 문제를 객체 타입 안에서 풀어낸다.[각주 4][각주 5]
상태 모델링에서는 로딩·성공·실패를 독립 필드의 우연한 조합으로 두는 대신 각 상태의 데이터를 함께 선언할 수 있다. 이전 결과를 유지한 채 다시 로딩하는 화면처럼 다른 조합이 필요하면 그 조합도 모델에 명시한다.
이런 쓰임은 타입 표기에서 상태의 표현 공간을 고르는 설계로 이어진다. 어떤 값이 가능한가와 어떤 사건 뒤에 다음 상태로 갈 수 있는가는 다른 질문이다. 전이 규칙은 함수와 모듈 경계에서 표현한다.
현대의 타입 모델이 맡는 경계
합 타입은 현재 데이터의 모양을 나타내고, 매개변수적 다형성은 그 구조를 여러 타입에 재사용하게 한다. Result<T, E>에서 성공·실패의 대안과 T·E의 재사용을 나누어 보면 두 개념의 역할이 보인다.
TypeScript에서는 타입 표기가 실행 시 제거되므로 외부 JSON의 필드는 실행 코드로 검사해야 한다. 타입 단언은 검증 코드를 만들지 않는다. 내부의 경우 분석에서는 never를 이용해 처리하지 않은 대안이 남는지 검사할 수 있다.[각주 6][각주 7]
판별 유니온으로 상태를 표현하는 작은 예제
다음 코드는 앞에서 살펴본 경우 분석을 TypeScript의 검색 상태로 옮긴 예다. 성공에는 결과가, 실패에는 오류가 있다는 관계만 표현하며 실제 검색 API는 포함하지 않는다.