| |
저자: |
| Dwight VandenBerghe |
이메일: |
[email protected] |
날짜: |
1998/07/28 |
포럼: |
comp.compilers |
|
여기서 "ML"이라는 용어는 SML 또는 Objective Caml을 의미한다고 가정하겠습니다. 저는 Ocaml의 열렬한 지지자이지만, SML/NJ가 설치되어 있고 비록 제가 Ocaml의 배포판, 도구 및 전반적인 구현을 선호하지만, 만약 Ocaml이 없었다면 SML/NJ로 작성하는 것도 기꺼이 할 수 있습니다. (Haskell, gofer, hugs 또는 어떤 지연 평가(lazy language)에 대해서는 제가 말할 수 없습니다. 저는 사람들에게 여러 종류가 있다고 생각합니다. 일부는 지연 평가 같은 것을 좋아하고 다른 일부는 엄격한 호출-값 방식(strict call-by-value)을 좋아합니다. 저는 단연코 후자에 속합니다. 엄격한 평가가 얼마나 깔끔한지, 무슨 일이 일어나고 있는지 정확히 이해하기 얼마나 쉬운지를 좋아하며, Haskell에서는 그 중 일부를 포기해야 한다고 생각합니다. 하지만 사람마다 다를 수 있습니다.) 따라서 제가 "ML"에 대해 글을 쓸 때 Ocaml에 대해 쓰는 것이지만, 제가 하는 말은 SML/NJ에도 적용된다고 생각합니다.
따라서 컴파일러를 작성하는 것을 끔찍한 노동이 아니라 즐거움으로 만들어준다고 생각되는 언어 기능들의 비순서 목록은 다음과 같습니다.
- 가비지 컬렉션(Garbage collection). 이것을 언급하는 것이 너무 기초적인 것처럼 보일 수 있지만, GC는 짧거나 중간 정도의 생명 주기를 가진 복잡한 데이터 구조가 많은 프로그램에 엄청난 도움이 됩니다. 그리고 컴파일러는 무엇보다도 복잡한 데이터 구조를 가진 프로그램입니다. 컴파일러를 좋아하려면 데이터 구조 중독자여야 합니다. C/C++, Pascal 및 그와 유사한 언어들은 제한 없는 포인터와 malloc 및 그 사촌들을 사용하게 하여 스스로 쓰레기를 처리하도록 만듭니다. 프로그래머이자 청소부 역할을 동시에 해야 하는 것입니다. 우리 중 일부는 청소부보다는 더 나은 프로그래머입니다. ML은 아마도 최고의 GC를 가지고 있을 것입니다. 너무 빨라서 많은 실제 애플리케이션에서 C++의 malloc/free만큼 빠르며, 심지어 경우에 따라서는 조금 더 빠를 수도 있습니다. Java처럼 느린 것을 사용할까 봐 걱정할 필요가 없습니다. 그냥 거기에 존재하며, 보이지 않고, 눈부시게 빠르며, 항상 작동하고, 따라서 당신의 삶을 훨씬 쉽게 만듭니다.
꼬리 재귀(Tail recursion)는 최적화됩니다. 따라서 이를 활용하는 방법을 알게 되면 스택 공간을 소모하지 않으면서도 매우 빠른 트리 순회(tree walks)를 작성할 수 있습니다. 물론 공정하게 말하자면, VisualC++ 5와 같은 C++ 컴파일러 중 일부는 꼬리 재귀 제거 기능을 갖추고 있다고 알려져 있지만, 이를 믿어서는 안 됩니다 (VC에는 많은 버그가 있기 때문입니다). ML은 재귀(recursion)와 매우 잘 맞으며, 컴파일러의 많은 데이터 구조들이 재귀적 절차로 처리되는 것이 가장 좋기 때문에 여기에서 좋은 적합성을 보입니다.
- ML의 데이터 타입은 컴파일 과정을 반영합니다. 컴파일러는 부호 없는 짧은 정수(unsigned shorts)와 부호 있는 문자(signed chars) 같은 것에 신경 쓰지 않고, 모든 곳에서 "int"를 사용하며 문자열도 사용합니다. 문자열은 사방에 사용되는데, C/C++는 템플릿이 존재하는 경우에도 문자열 처리가 매우 형편없습니다. 컴파일러의 일부 영역에서는 int보다 약간 더 큰 양에 대한 산술 연산이 유용할 때가 있으므로, "bignum" 기능은 실제로 그러한 경우(예를 들어 상수를 폴딩하거나 기본 숫자 타입보다 조금 더 높은 정밀도로 토큰화한 다음 네이티브 형태로 다시 변환해야 할 때)에 유용합니다. 만약 bignums가 없다면 직접 작성하거나 속임수에 의존해야 합니다. (제가 의미하는 바의 예시로는 전문 컴파일러 개발자인 Dave Hanson의 "C Interfaces and Implementations"를 참고하십시오.)
- ML의 타입 생성자(type constructors)는 ASTs(추상 구문 트리, abstract syntax trees)와 같은 것을 기술하는 데 정말 훌륭합니다. 이들은 때때로 "태그가 지정된 유니온(tagged unions)"이라고 불리는 것을 구현하는데, 이는 C/C++와 달리 어떤 데이터가 들어 있는지 알려주는 태그 필드가 함께 오는 효율적이고 작은 유니온 데이터 타입입니다. 이것은 강제됩니다. 즉, 우회할 방법이 없습니다. ML의 패턴 매칭(pattern matching)은 데이터 구조를 인수로 받는 함수에 대해 소스 코드를 믿을 수 없을 정도로 읽기 쉽게 만들기 위해 태그가 지정된 유니온과 함께 작동하도록 설계되었습니다.
여기에 타입 추론(type inference)과 꼬리 재귀 제거(tail recursion elimination)를 결합하면, 복잡한 데이터 구조를 인수로 받는 재귀 함수에 최적화된 언어를 얻게 됩니다... 익숙하게 들리지 않나요?
-
안전성(Safety). ML은 자동 정리 증명기(automated theorem provers)를 사용하는 수학자들이 직면하는 주요 문제에 대한 해결책으로 고안되었습니다. 즉, 해당 분야의 일반적인 언어(Lisp)가 타입이 없고 위험하며 경솔한 특성을 가지고 있기 때문에 프로그램이 제대로 작동할 것이라고 절대 확신할 수 없다는 것입니다. 물론 이는 모든 언어에 대해 말할 수 있는 것이지만, ML은 효율성과 안전성을 동시에 추가하기 위해 도메인을 약간 제한하려는 시도였습니다.
ML 프로그램은 시스템을 충돌시키지 못합니다. 컴파일이 되면 실행되며, 세그멘테이션 오류(segmentation fault)를 받지 않습니다. 프로그램에 대한 특정 속성을 증명할 수 있으며, 특정 종류의 오류는 애초에 발생할 수 없다고 신뢰할 수 있습니다. 예를 들어, 리스트는 불변(immutable)이며 단일 타입의 요소만 포함해야 하므로, 실수로 정수형을 넣어 문자열 리스트를 망가뜨릴 걱정(Scheme이나 Lisp에서 할 수 있는 것처럼)을 할 필요가 없습니다.
실제로 아무것도 넣을 수 없으며, 새로운 리스트를 생성해야 합니다. 이로 인해 근본적인 루틴들이 이중 링크(double-links), 파괴적 업데이트(destructive updating) 등에 신경 써야 하는 다른 언어의 리스트보다 훨씬 빠를 수 있습니다. 눈부시게 빠른 가비지 컬렉션(GC) 시스템 덕분에 모든 것이 아주 잘 작동하며... 밤에 더 편안하게 잠들 수 있게 됩니다.
-
ML은 복잡한 알고리즘이 실행되는 거대하고 복잡하며 재귀적인(recursive) 데이터 구조를 특징으로 하는 응용 도메인(정리 증명)을 위해 설계되었습니다. 익숙하게 들리지 않나요?
-
예외(Exceptions). ML은 빠르고 깔끔한 예외 처리(exception handling)를 구현하며, 이전에 사용해 본 적이 없다면 정말 큰 즐거움입니다. 키가 발견될 것이라고 가정하고 테이블 조회를 작성한 다음, 해당 검색을 "try" 블록으로 감싸서 예외적인 경우("not found")를 포착합니다.
따라서 찾을 수 없는 경우(not-found case)를 테스트하는 것을 잊어버려 프로그램에 오류가 생기는 일은 절대 없을 것입니다. 만약 그렇게 한다면, 런타임 시스템이 처리되지 않은 예외(uncaught exception)와 함께 해당 예외가 발생한 위치를 알려주며 중단됩니다. 예외 사용법을 익히게 되면 프로그램은 읽기 쉽고, 더 깔끔하며, 더욱 견고해집니다.
- 타입 추론 (Type inference). ML로 작성된 1000줄의 코드에서 단지 두세 개의 변수만 선언하면 될 수도 있습니다. 이 언어는 변수가 사용되는 방식을 통해 타입을 알아냅니다. 그리고 Perl처럼 추측하지 않습니다. 확실하게 알고 있습니다. 제가 Ocaml을 SML보다 좋아하는 이유 중 하나는 Ocaml이 연산자 오버로딩(operator overloading)을 좋아하지 않는다는 점입니다. 예를 들어, float 덧셈("+.")과 정수 덧셈("+")에는 서로 다른 연산자가 존재합니다. 타입 추론과 연산자 오버로딩은 불편한 조합입니다. 제 생각에 언어 설계자는 두 가지를 모두 만족시키려 노력하기보다는 둘 중 하나를 선택해야 합니다.
하지만 컴파일러가 사용법에 대해 아는 모든 것을 그냥 버리고, 우리가 명백한 사실을 반복해서(over and over and over again) 서술하도록 강요하는 Pascal이나 C보다 나은 것은 없습니다. 우리는 그저 망칠 수밖에 없고, 실제로 그렇게도 계속합니다.
-
Lex/yacc/burg. ML에는 이 표준 도구들의 훌륭한 구현체들이 있으며, 한번 사용법을 알게 되면 많은 작업을 훨씬 간단하게 만듭니다. 아닙니다. 제가 Lex의 열렬한 팬은 아니며, lalr(1)이 ll(k)보다 좋다고 생각하지는 않지만, 저는 실용주의자입니다. 만약 그것이 존재하고 잘 구현되어 있다면, 저는 그것을 사용할 것입니다. Ocaml과 SML/NJ 모두 개발자들이 사용하는 매우 훌륭하고 견고한 컴파일러 도구 구현체를 가지고 있습니다. 더 나은 툴킷을 가진 언어는 많지 않습니다.
-
Ocaml이 빠르다는 것을 언급했나요? 저는 Ocaml로 보험 계리 금융 모델링 언어의 컴파일러를 작성했는데, 코드가 약 10K 라인 정도였으며, C++로는 20K 이상이었을 것입니다. 그리고 제 Pentium 200에서 알려진 가장 큰 프로그램을 3초 만에 컴파일합니다. 대부분의 프로그램은 1초 이내에 컴파일됩니다. 저는 이것이 놀랍다고 생각합니다.
-
지원 (Support).
저는 Inria(Xavier Leroy와 Pierre Weis 등)로부터 다른 어떤 언어 벤더보다 더 나은 지원을 받았습니다. 이 언어에는 문제가 거의 없으며, 제가 마주친 몇 가지 문제들은 며칠 만에 해결되었습니다. 예를 들어 VC++나 Turbo Pascal의 지원과 비교해 보십시오.
-
라이브러리(Library). ML의 표준 라이브러리에는 많은 데이터 구조 관련 내용이 포함되어 있어 큰 도움이 됩니다. 저는 이것이 대부분의 다른 언어들에서 흔히 엉성하게 모아놓은 혼란스러운 모습보다 더 완전하고 간결하며 사용하기 좋다고 생각합니다.
-
모듈 시스템(The module system). ML은 분리 컴파일에 대해 강력하고 잘 설계된 지원을 제공하여, 별도로 컴파일된 모듈이 다형성(polymorphism)을 지원할 수 있게 합니다(즉, 임의의 타입에서 작동할 수 있습니다). 모듈 내부의 가시성은 정확하게 제어될 수 있습니다. 'Functors'는 모듈을 특정 구현체로 특수화할 수 있습니다. 이는 C++ 템플릿처럼 고통과 어려움 없이 매우 멋진 기능입니다.
결국 이것은 주로 데이터 구조에 관한 것입니다. ML은 복잡한 데이터 구조와 그 주변의 재귀 알고리즘을 표현하는 것을 비범하게 쉽게 만듭니다. 가장 기본적인 데이터 구조(리스트, 배열, 구조체(struct), 유니온(union), 속성 리스트(property lists), 해시 테이블(hash tables), 이진 트리(binary trees), 큐 등)는 이미 언어에 내장되어 있으며, 잘 구현되어 바로 사용할 수 있습니다. 다른 언어에서는 직접 구축해야 했을 것들을 처음부터 갖춘 상태로 시작할 수 있다는 의미입니다.
ML이 완벽한 언어일까요? 아닙니다. 모든 언어와 마찬가지로 ML에도 많은 결함이 있습니다. 문법은 이상하고, 일부 부분은 배우기 어렵고, 일부 부분은 사용하기 어렵습니다. Printf 같은 간단한 것조차 표현하는 것이 상당히 어색할 수 있습니다. 제가 많이 작업하는 임베디드 시스템(embedded systems)과 같은 많은 문제 영역에서 ML을 유리하게 사용할 수는 없습니다. 예를 들어 DSP 칩 내부에서 FFT를 프로그래밍하는 데 사용하는 것은 끔찍할 것입니다. 제가 보기에는 ML은 아직 GUI와 잘 맞지 않으며, OOP(객체 지향 프로그래밍) 지원이 부족합니다(물론 저는 이것을 제약이 아닌 기능으로 간주하지만 말입니다).
하지만 모든 언어는 빛을 발하는 특정 문제 영역들을 가지고 있습니다.
컴파일러 구현은 ML에 적합한 영역 중 하나라고 생각합니다. 컴파일러를 작성하다가 함수 중간에서 반대편에 박스가 달린 9mm 크레센트 렌치가 필요하다고 가정해 봅시다. 당신은 공구 상자를 열고... 위 서랍에 반짝이고 빛나며 튼튼한 그것을 발견합니다. 그것을 사용하고, 몇 분 후에는 클립과 자석이 달린 작은 십자 드라이버가 필요합니다... 그리고 또 거기에 있습니다. 반짝이고 빛나며 튼튼하죠.
공구 상자에 비범할 정도로 많은 도구가 있다는 것이 아닙니다. (아닙니다. 사실 이 공구 상자는 친구들이 사용하는 싱크대 외의 모든 것을 담는 일반적인 공구 상자보다 훨씬 작습니다.) 이 공구 상자는 매우 똑똑한 도구 제작자들에 의해 수십 년간의 경험을 응축하여 신중하고 사려 깊게 조립되었으며, 모든 좋은 공구 세트가 그러하듯이, 매우 특정한 목적을 위해 설계되었습니다. 즉, 주로 매우 복잡한 데이터 구조에 대한 재귀적 조작(recursive manipulation)을 수행하는 함수들의 분리 컴파일(separate compilation)을 중심으로 빠르고 안전하며 견고한 프로그램을 구축하기 위함입니다.
프로그램 말이죠. 예를 들어... 컴파일러 같은 것들요. 드와이트