단상화야말로 사전

언어 최대의 미결 설계 과제 —— 타입 클래스를 넣을 것인가 —— 는 한 오후에 소멸했다. 답이 나온 것이 아니라, codebase 안에서 이미 절반쯤 답해져 있던 더 작은 물음으로 축퇴한 것이다: 등가는 처음부터 줄곧, 다른 이유로 세워진 기구에 실려, 조용히 다상이었다. 순서는 3줄의 삭제로 거기에 합류했다. generic sort는 공짜로 도착했고, pairing heap의 comparator 세금은 사라졌다.

merepolymorphismtype-classesdesignlanguage-design

이 Part는 방법의 경제가 바뀐 이야기다. 지금까지의 Part는 실물 도구 —— 서버, 게임, inspector —— 를 세워 언어에 성장을 강제했고, 도구 하나에 며칠을 들였다. 하지만 메모리 모델이 완성되고 soundness의 구멍이 막힌 지금, 남은 물음은 충분히 정밀해져서 10분의 probe로 결정할 수 있다: 물음에 닿는 최소의 프로그램을 쓰고, 재고, 숫자에게 다음 일을 고르게 한다. 이 Part는 그런 probe 여섯 개와 그것이 낳은 아홉 릴리스의 기록이다. 첫 probe는 언어 최대의 미결 설계 과제로 향했고, 찾아낸 것은 과제에 답하는 대신 과제를 녹였다.

물음과, 물음을 재구성한 측정

장부 위의 물음은 이랬다: 이 언어에 타입 클래스는 필요한가? 앞 Part의 generic pairing heap이 정직한 측정값을 내놓았었다 —— generic 컨테이너는 쓸 수 있다, 하지만 모든 조작에 comparator closure를 통과시키는 것은 눈에 보이는 세금이고, <타입 변수 너머로 쓰는 것은 단적으로 불가능하다(fn a -> fn b -> a < b는 인자를 int에 고정한다). 이 세션용으로 기초한 설계 노트는 중간안 —— 구체적 호출 지점에서의 dictionary 합성 —— 을 제안하고 있었다. 그리고 세션은, 이곳의 세션이 시작해야 하는 방식으로 시작했다 —— 코드가 실제로 무엇을 하는지 읽는 것에서 —— 그리고 제안보다 나은 것을 찾았다: 등가는 이미 타입 변수를 지나고 있다. 오래전부터다. fn a -> fn b -> a == b는 진짜 다상이고, 그것을 위해 타입 클래스를 세운 사람은 아무도 없다. 그러니 진짜 물음은 「어떻게 제약을 더하는가」가 아니라 「왜 등가는 제약 없이 허용되는가?」였다.

왜 등가는 허용되는가

다른 이유로 세워진 두 기구가, 합쳐져 사전의 역할을 연기하고 있었음이 판명된다. interpreter에서는 실행 시의 값이 항상 구체이므로, 구조 비교 함수는 그저 그것을 걸으면 된다 —— 타입 정보는 불필요하다. 컴파일되는 backend에서는, 다상 함수는 단상화된다: 구체 타입에서의 사용마다 특수화된 instance가 만들어지고, 그 instance의 body 안에서 ==의 operand는 구체 타입을 가진다 —— 바로 derive 기구가 타입별 등가 함수를 emit해서 다루는 그 상황이다. scheme이 제약을 나르지 않는 것은, 코드 생성 시점에 제약할 것이 남아 있지 않기 때문이다. Haskell은 실행 시에 사전을 건넨다. 이 언어에서는 단상화기야말로 사전이고, 전부 컴파일 시에 해결된다. 앞 Part의 수정들 —— pristine clone, 다중 instance로의 지연 승격 —— 이 이 기구를 어느새 신뢰할 만한 것으로 만들어 두었다. 순서를 막고 있던 것은 단 하나: 「미해결 비교 대상은 int로 default한다」는 타입 검사기의 역사적 규칙이었다.

default를 지운다

수정은 그 규칙의 삭제였다 —— 순서는 operand를 unify하고, 거기서 멈춘다. 등가가 줄곧 해 온 것과 한 치도 다르지 않다. int default에 의지하던 프로그램은 instance화가 돌봐 주므로 지금도 타입이 붙는다. suite의 실패는 우발적 슬롯 번호를 고정하고 있던 어서션 2건뿐이었다. 그리고 귀결은 공짜로 연쇄했다. prelude가 이미 정직한 스타일로 쓰여 있었기 때문이다: list_sort는 comparator fn a -> fn b -> a < b를 건네는 list_sort_by로 정의되어 있다 —— 말없이 int 전용이었던 그 정의는 default가 죽는 순간 generic이 되었다. 튜플 리스트의 정렬에 이제 주석도 comparator도 필요 없다. pairing heap의 손으로 쓴, 타입 주석 딸린 comparator는 다상의 한 줄로 무너졌다. 컴파일 결과는 기구를 숨기지 않고 보여 준다: instance는 derive된 tuple 비교 함수 —— 세 Part 전에 derive-ord가 세운 바로 그것 —— 를 부르고 있다.

정하지 않음으로써 정해진 것

이것이 무엇이고 무엇이 아닌지는 정밀하게 말해 둘 가치가 있다. 타입 클래스는 여전히 없다: 타입의 순서를 구조와 다른 것으로 선언할 방법은 없고, scheme에 제약은 나타나지 않으며, instance 선언도 없다. 있는 것은 derive 일가 —— show, JSON, 등가, 순서 —— 가 임의의 구체 타입에서, 그리고 이제 임의의 타입 변수 너머로 쓰이고, 단상화가 해결 기구를 맡는다는 상태다. 이것은 이 프로젝트에서 같은 설계 본능이 나타난 세 번째다: uncurrying은 딱 포화된 호출 지점만 최적화했다. derive 일가는 딱 구체 타입에서만 특수화한다. 그리고 다상 순서는 instantiation이 사물을 구체로 만드는 바로 그 자리에서 해결한다. 일반 문제가 쉬워지는 특정한 한 점에 걸고, 프로그램이 그것 없이 살 수 없음을 실증할 때까지 일반 기구를 세우지 않는다. 타입 클래스의 물음은 크게는 열린 채다 —— 사용자 정의 instance는 forcing program을 기다리는 장래 과제로 남는다 —— 하지만 그것이 나르고 있던 쓰기 맛의 짐은 그것 없이 도착했다. 다음 probe는 훨씬 구체적인 곳으로 향했다: 프로젝트 자신의 사이트의 라이브 게임이다. 그것은, 실은 줄곧 망가져 있었던 것이다.

← Back to Mere: 언어를 만들다