ML과 OCaml이 컴파일러 구현에 특히 잘 맞는 이유를, 가비지 컬렉션부터 타입 추론과 모듈 시스템까지 언어 기능 중심으로 설명한다.
**저자:**Dwight VandenBerghe **이메일:**dwight@pentasoft.com **날짜:**1998/07/28 **포럼:**comp.compilers "ML"이라는 용어를 SML 또는 Objective Caml을 뜻하는 말로 사용하겠습니다. 저는 Ocaml의 열렬한 지지자이지만, SML/NJ도 설치해 두었고 배포판, 도구, 전반적인 구현은 Ocaml 쪽을 더 선호하더라도, Ocaml이 없었다면 기꺼이 SML/NJ로 작성했을 것입니다. (Haskell, gofer, hugs, 또는 다른 지연 평가 언어들에 대해서는 제가 말할 수 없습니다. 제 생각에는 사람마다 성향이 달라서, 어떤 사람은 지연 평가 같은 것을 좋아하고 어떤 사람은 엄격한 call-by-value를 좋아합니다. 저는 분명 후자에 속합니다. 엄격한 평가가 얼마나 깔끔한지, 정확히 무엇이 일어나고 있는지 이해하기 얼마나 쉬운지를 좋아하며, Haskell에서는 그 점을 조금 포기해야 한다고 느낍니다. 하지만 각자의 경험은 다를 수 있습니다.) 그래서 제가 "ML"에 대해 쓸 때 실제로는 Ocaml을 염두에 두고 있지만, 제가 하는 말은 SML/NJ에도 대체로 적용된다고 생각합니다.
그래서 여기, 컴파일러 작성을 끔찍한 고역이 아니라 즐거운 일로 만들어 준다고 제가 생각하는 언어 특성들을 순서 없이 나열해 보겠습니다.
가비지 컬렉션. 이것을 언급하는 것이 너무 기초적으로 보일 수도 있지만, gc는 수명이 짧거나 중간 정도인 복잡한 데이터 구조를 많이 다루는 프로그램에 엄청난 축복입니다. 그리고 컴파일러는 무엇보다도 복잡한 데이터 구조를 다루는 프로그램입니다. 컴파일러를 좋아하려면 데이터 구조에 푹 빠진 사람이어야 합니다. 그런데 C/C++, Pascal, 그리고 그 계열 언어들은 제한 없는 포인터와 malloc 및 그 사촌들을 통해 스스로 쓰레기를 치우게 만듭니다. 즉, 프로그래머이면서 동시에 청소부 역할도 해야 합니다. 우리 중에는 청소부보다 프로그래머로서 더 나은 사람들이 있습니다. ML은 아마도 가장 뛰어난 gc를 갖고 있습니다. 너무 빨라서 많은 실제 응용에서는 C++의 malloc/free만큼 빠르고, 경우에 따라서는 조금 더 빠를지도 모릅니다. Java의 gc처럼 느려서 손을 비틀며 걱정할 필요가 없습니다. 그저 거기에 있을 뿐입니다. 보이지 않고, 눈부시게 빠르며, 항상 동작하고, 그래서 삶이 훨씬 쉬워집니다.
꼬리 재귀가 최적화됩니다. 따라서 이것을 활용하는 방법만 알면 스택 공간을 잡아먹지 않고 매우 빠른 트리 순회를 작성할 수 있습니다. 공정하게 말하자면, VisualC++ 5 같은 C++ 컴파일러 중에도 일부 꼬리 재귀를 제거해 준다고 하는 것들이 있지만, 그것을 믿고 의존할 수는 없습니다. (VC의 구현에는 버그가 많습니다.) ML은 재귀와 매우 잘 맞으며, 컴파일러 안의 많은 데이터 구조들이 재귀적 프로시저로 처리하는 것이 가장 적합하기 때문에 아주 좋은 궁합을 이룹니다.
ML의 데이터 타입은 컴파일 과정과 잘 맞습니다. 컴파일러는 unsigned short와 signed char의 구분에는 대체로 신경 쓰지 않고, 거의 어디서나 "ints"와 문자열을 사용합니다. 문자열은 곳곳에서 사용되는데, C/C++는 템플릿이 있더라도 문자열 처리에 꽤 형편없습니다. 컴파일러 안에는 ints보다 조금 더 큰 양에 대한 산술이 있으면 좋은 곳들이 있어서, "bignum" 기능이 실제로 유용한 경우가 있습니다. (예를 들어 상수 폴딩을 하거나, 기본 숫자 타입보다 조금 더 정밀하게 토큰화한 뒤 다시 네이티브 형식으로 변환해야 할 때입니다.) bignum이 없으면 직접 구현하거나 꼼수를 써야 합니다. (제가 무슨 뜻인지 알고 싶다면 대가급 컴파일러 작성자 Dave Hanson의 "C Interfaces and Implementations"를 보십시오.)
ML의 타입 생성자는 AST(abstract syntax trees) 같은 것을 기술하는 데 그야말로 훌륭합니다. 이것들은 흔히 "tagged unions"라고 불리는 것을 구현합니다. 즉, union 데이터 타입으로서 효율적이고 작지만, C/C++와는 달리 union 안에 무엇이 들어 있는지를 알려 주는 tag 필드가 함께 옵니다. 그리고 이것은 강제됩니다. 다시 말해, 우회할 방법이 없습니다. ML의 패턴 매칭은 이러한 tagged unions와 잘 작동하도록 설계되어 있어서, 데이터 구조를 인자로 받는 함수의 소스 코드가 믿을 수 없을 정도로 읽기 쉬워집니다. 여기에 타입 추론과 꼬리 재귀 제거를 결합하면, 복잡한 데이터 구조를 인자로 받는 재귀 함수를 위해 최적화된 언어가 됩니다 ... 어디선가 익숙하지 않습니까?
안전성. ML은 자동 정리 증명기를 사용하는 수학자들이 직면한 주요 문제를 해결하기 위한 해법으로 고안되었습니다. 그 문제란, 그 영역의 일반적인 언어였던 Lisp의 무타입적이고 위험하며 지나치게 무모한 성격 때문에 프로그램이 제대로 동작할지 결코 확신할 수 없다는 것이었습니다. 물론 이것은 모든 언어에 대해 어느 정도 말할 수 있지만, ML은 영역을 조금 제한하는 대신 효율성과 안전성을 동시에 더하려는 시도였습니다. ML 프로그램은 시스템을 크래시시킬 수 없습니다. 컴파일만 되면 실행되며, segmentation fault가 나지 않습니다. 프로그램에 대해 어떤 성질들은 증명할 수 있고, 특정 종류의 오류는 아예 일어날 수 없다고 믿을 수 있습니다. 예를 들어 리스트는 불변이고 단일 타입의 원소만 포함해야 하므로, 실수로 문자열 리스트에 정수를 넣어 망쳐 버릴까 걱정할 필요가 없습니다. (Scheme이나 Lisp에서는 그런 일이 가능합니다.) 사실 그 리스트에 무엇을 "넣는" 것 자체가 불가능합니다. 새 리스트를 만들어야 합니다. 이것은 기저 루틴들이 다른 언어의 리스트보다 훨씬 더 빠르게 동작할 수 있게 해 줍니다. 다른 언어에서는 이중 연결, 파괴적 갱신 등등을 신경 써야 하기 때문입니다. 눈부시게 빠른 GC 시스템과 함께라면 이 모든 것이 아주 잘 돌아가고 ... 밤에도 더 편히 잠들 수 있습니다.
ML은 대규모이고 복잡하며 재귀적인 데이터 구조와, 그것들을 대상으로 돌아가는 복잡한 알고리즘이 특징인 응용 영역, 즉 정리 증명을 위해 설계되었습니다. 익숙하게 들리지 않습니까?
예외. ML은 빠르고 깔끔한 예외 처리를 구현하며, 이전에 써 본 적이 없다면 정말 즐거운 경험입니다. 예를 들어 테이블 조회를 작성할 때, 키가 발견될 것이라고 가정한 뒤 검색을 "try" 블록으로 감싸고 예외적인 경우("not found")를 잡으면 됩니다. 그래서 발견되지 않은 경우를 검사하는 것을 잊어버려 프로그램을 망칠 일이 없습니다. 그렇게 하면 런타임 시스템이 잡히지 않은 예외와 함께 멈추고, 예외가 정확히 어디서 던져졌는지를 알려 줄 것이기 때문입니다. 예외를 쓰는 법을 익히면 프로그램은 더 읽기 쉽고, 더 깔끔하며, 더 견고해집니다.
타입 추론. ML 코드 천 줄에서 변수 선언이 두세 개면 많을 정도일 수 있습니다. 변수들이 어떻게 사용되는지를 보고 타입을 알아서 계산해 냅니다. 그리고 Perl 같은 식으로 추측하는 것이 아닙니다. 확실히 압니다. 제가 SML보다 Ocaml을 좋아하는 이유 중 하나는, Ocaml이 연산자 오버로딩을 좋아하지 않는다는 점입니다. 예를 들어 부동소수점 덧셈에는 "+."를, 정수 덧셈에는 "+"를 사용합니다. 타입 추론과 연산자 오버로딩은 불편한 동침 상대입니다. 제 생각에는 언어 설계자는 둘 중 하나를 선택해야지, 두 주인을 동시에 섬기려 해서는 안 됩니다. 하지만 Pascal이나 C보다는 무엇이든 낫습니다. 그런 언어들에서는 컴파일러가 사용 방식에 대해 알고 있는 것을 전부 던져 버리고, 명백한 것을 계속해서, 계속해서, 계속해서 적으라고 강요합니다. 우리가 할 수 있는 일이라곤 그것을 망치는 것뿐이고, 그래서 우리는 계속해서 망칩니다.
Lex/yacc/burg. ML에는 이런 표준 도구들의 훌륭한 구현이 있으며, 일단 사용법을 익히고 나면 많은 작업이 훨씬 단순해집니다. 아니요, 저는 lex의 열렬한 팬도 아니고, lalr(1)을 ll(k)보다 선호하는 것도 아닙니다. 하지만 저는 실용주의자입니다. 그게 거기 있고, 구현이 잘 되어 있다면 쓰겠습니다. Ocaml과 SML/NJ는 둘 다 개발자들 자신이 사용하는 정말 훌륭하고 탄탄한 컴파일러 도구 구현을 갖고 있습니다. 이보다 더 나은 툴킷을 기본으로 제공하는 언어는 많지 않습니다.
Ocaml이 빠르다는 말, 제가 했던가요? 저는 Ocaml로 보험수리 재무 모델링 언어용 컴파일러를 하나 작성했는데, 아마 코드가 10K줄 정도였고, C++였다면 20K줄 이상이 되었을 것입니다. 그리고 제 pentium 200에서 가장 큰 것으로 알려진 프로그램을 3초 만에 컴파일합니다. 대부분의 프로그램은 1초 안에 컴파일됩니다. 저는 이것이 놀랍습니다.
지원. 저는 다른 어떤 언어 벤더에게서 받은 것보다 Inria(Xavier Leroy와 Pierre Weis 등을 포함하여)로부터 더 나은 지원을 받았습니다. 이 언어에는 문제가 그리 많지 않고, 제가 겪은 몇 안 되는 문제들도 며칠 안에 수정되었습니다. 이것을 예컨대 VC++나 Turbo Pascal의 지원과 비교해 보십시오.
라이브러리. ML의 표준 라이브러리에는 데이터 구조 관련 내용이 많이 들어 있어서 큰 도움이 됩니다. 저는 그것이 다른 대부분의 언어에서 볼 수 있는 보통의 여기저기 덕지덕지 붙인 난장판보다 더 완전하고, 더 간결하고, 더 쓰기 쉽다고 느낍니다.
모듈 시스템. ML은 분리 컴파일에 대해 강력하고 잘 고안된 지원을 갖고 있으며, 그 덕분에 별도로 컴파일된 모듈이 다형성을 지원할 수 있습니다. (즉, 임의의 타입에 대해 동작할 수 있습니다.) 모듈 내부 구현의 가시성은 정확하게 제어할 수 있습니다. "Functors"는 모듈을 특정한 형태로 특수화할 수 있습니다. 꽤 멋집니다. 고통과 괴로움이 없는 C++ 템플릿 같습니다.
그러니까 결국 대부분은 데이터 구조에 관한 이야기입니다. ML은 복잡한 데이터 구조와 그 주변의 재귀 알고리즘을 표현하게 해 주는 데 유난히 능숙합니다. 가장 기본적인 데이터 구조들, 즉 리스트, 배열, 구조체, 유니언, 프로퍼티 리스트, 해시 테이블, 이진 트리, 큐 등등이 이미 언어 안에 잘 구현되어 준비되어 있습니다. 다른 언어였다면 먼저 쌓아 올려야 했을 출발점을, 여기서는 처음부터 갖고 시작하는 셈입니다.
ML이 완벽한 언어인가요? 세상에, 아닙니다. 다른 모든 언어처럼 결함이 많습니다. 문법은 이상하고, 어떤 부분은 배우기 어렵고, 어떤 부분은 쓰기 어렵습니다. Printf 같은 단순한 것조차 표현이 꽤 낯설 수 있습니다. 많은 문제 영역에서는 ML을 유리하게 사용할 수 없습니다. (예를 들어 제가 많이 일하는 분야인 임베디드 시스템이 그렇습니다.) 예컨대 DSP 칩 내부에서 FFT를 프로그래밍하는 데 사용한다면 끔찍할 것입니다. 제가 보기에는 아직 GUI와도 아주 잘 어울리지는 않고, OOP 지원도 부족합니다. (비록 저는 그것을 제약이 아니라 장점으로 봅니다만.)
하지만 모든 언어에는 특히 빛나는 문제 영역이 있습니다. 저는 컴파일러 구현이 ML에게 그런 영역 중 하나라고 생각합니다. 컴파일러를 작성하다가 함수 한가운데서 반대쪽 끝에 박스가 달린 9mm 초승달 렌치가 필요해졌다고 해 봅시다. 공구 상자를 열면 ... 거기 있습니다. 맨 위 서랍에, 밝게 빛나고 튼튼하게. 그것을 쓰고 나면 몇 분 뒤에는 클립과 자석이 달린 작은 십자 드라이버가 필요해집니다 ... 그리고 또 거기 있습니다. 밝게 빛나고 튼튼하게.
공구 상자 안에 도구 수가 유난히 많다는 뜻은 아닙니다. (아닙니다. 사실 이 공구 상자는 보통의 공구 상자들보다 훨씬 작습니다. 친구들이 쓰는, 싱크대만 빼고 다 들어 있는 그런 상자들 말입니다.) 중요한 것은, 이 공구 상자가 매우 똑똑한 공구 장인들에 의해 세심하고 사려 깊게 구성되었다는 점입니다. 그들은 수십 년의 경험을 응축해 넣었고, 모든 좋은 툴킷이 그렇듯 아주 특정한 목적을 위해 설계했습니다. 즉, 빠르고, 안전하고, 견고한 프로그램을 만들기 위한 것이며, 그런 프로그램은 주로 매우 복잡한 데이터 구조를 재귀적으로 조작하는 함수들의 분리 컴파일을 중심으로 구성됩니다.
이를테면, 바로 ... 컴파일러 같은 프로그램 말입니다.
Dwight