Clojure와 다른 Lisp들의 차이점
Clojure와 다른 Lisp들의 차이점
이 문서는 Common Lisp이나 Scheme에 익숙한 프로그래머를 위해 마련됐어요. Clojure를 처음 배울 때 가장 낯선 부분이 바로 "기존 Lisp과 정확히 뭐가 다르지?"라는 질문인데요, 그 차이를 한눈에 정리한 문서예요. Clojure가 Lisp 계열이면서도 중간중간 다르게 설계된 지점들이 많으니, 하나씩 훑어보면 "아, 여기는 이렇게 다른 거구나" 하고 이해가 잡힐 거예요.
출처: Clojure 공식문서
본문
한 번에 보는 차이점 목록
기본적인 차이부터 정리해 볼게요.
- Clojure는 대소문자를 구분해요 (case sensitive)
- Clojure는 Lisp-1이에요. 함수와 변수가 같은 네임스페이스를 공유하죠.
()는 nil과 같지 않아요- 리더(reader)는 부수 효과(side-effect)가 없어요
- 키워드는 심볼(symbol)이 아니에요
- 심볼은 저장 위치가 아니에요 (Var 참조)
nil은 심볼이 아니에요t는 문법이 아니에요.true를 써요- 리드 테이블(read table)은 사용자 프로그램이 접근할 수 없어요
let은 순차적으로 바인딩해요 (병렬이 아니라)do는 루프 구성이 아니에요- 꼬리 호출 최적화(tail-call optimization)가 없어요.
recur를 써요 - syntax-quote는 심볼 해석을 해요. 그래서
`x는'x와 같지 않아요 - syntax-quote(
`)에는 자동 겐심(auto-gensyms)이 있어요 ~는 언쿼트(unquote)이고,,는 공백이에요- 맵, 벡터, 집합을 위한 리더 문법이 있어요
cons,first,rest는 구체적인 cons 셀(cell)이 아니라 시퀀스 추상화(sequence abstraction)를 다뤄요- 대부분의 자료 구조는 불변(immutable)이에요
- lambda는
fn이고, 아리티(arity)에 따른 오버로딩을 지원해요 =는 동등성 술어(predicate)예요
심볼과 바인딩에 대한 차이
- 전역 Var는 동적으로 다시 바인딩할 수 있어요 (dynamic으로 선언된 경우). 이때 어휘적 지역 바인딩(lexical local binding)을 방해하지 않죠. dynamic과 lexical 바인딩을 구분하기 위한 특별한 선언은 필요 없어요. Clojure는 Lisp-1이므로 (전역) 함수도 dynamic으로 표시되면 동적으로 다시 바인딩할 수 있어요.
letrec,labels,flet은 없어요. 자기 참조(self-reference)가 필요하면(fn name [args]...)를, 상호 참조(mutual reference)가 필요하면 letfn을 사용해요.
nil, false, 빈 컬렉션
- Clojure에서
nil은 '아무것도 없음'을 의미해요. 어떤 타입이든 값이 없음을 나타내며, 리스트나 시퀀스에만 한정된 개념이 아니에요. - 빈 컬렉션은
nil과 구분돼요. Clojure는nil과'()을 동일시하지 않아요. false는 두 개의 불리언 값 중 하나예요. 나머지 하나는true이고요.- 컬렉션에는 리스트 외에도 종류가 많아요. 빈 컬렉션의 인스턴스가 여러 개 있을 수 있고, 그중 일부는 리터럴(
[],{},())을 지원해요. 그래서 "대표적인 빈 컬렉션 값"(sentinel empty collection)이라는 게 존재할 수 없어요. - Scheme에서 오셨다면,
nil은#f라는 개념에 가장 가깝게 대응될 수 있어요.
시퀀스(Sequence)라는 큰 차이
Clojure의 아주 큰 차이점 중 하나가 바로 **시퀀스(sequence)**예요.
시퀀스는 특정 컬렉션에 묶여 있지 않아요. 특히 반드시 구체적인 리스트일 필요도 없죠. 어떤 동작이 벌어지는지 볼게요.
- 빈 컬렉션에 원소들의 시퀀스를 요청하면(즉
seq를 호출하면)nil을 반환해요. 비유하자면 **"나는 시퀀스를 만들어 낼 수 없어"**라고 말하는 거예요. - 시퀀스의 마지막 원소에
rest를 요청하면 **또 다른 논리적 시퀀스**를 반환해요. 그 시퀀스가 비어 있는지는 다시seq를 호출해 봐야만 알 수 있죠. - 이 덕분에 시퀀스와 시퀀스 프로토콜은 게으르게(lazy) 동작할 수 있어요.
시퀀스 함수 중 일부는 Scheme과 Common Lisp에서 '리스트'(페어/cons)만 다루고 '()와 nil 같은 sentinel 값을 반환해 '빈' 리스트를 표현했던 함수들에 대응돼요. 다만 Clojure의 반환 값은 특정한 빈 컬렉션을 돌려주는 대신 또 다른 논리적 시퀀스를 돌려준다는 점에서 달라요.
어떤 시퀀스 함수는 Scheme/CL에는 대응하는 함수가 없고, Haskell/ML 계열의 함수처럼 동작해요. 그중 일부는 무한하거나 계산된 시퀀스를 반환하기도 하는데, 이때는 Scheme/CL의 리스트처럼 구체적인 자료 구조에 비유하기가 어려워져요.
그리고 한 가지 구분이 도움이 돼요. 컬렉션/자료 구조와 시퀀스/반복을 별개의 것으로 보는 거예요. Common Lisp과 Scheme에서는 이 둘이 합쳐져 있지만, Clojure에서는 깔끔하게 분리되어 있어요.
차이점 비교표
각 언어에서 nil, true/false, 동등성, 시퀀스가 어떻게 다뤄지는지 표로 정리해 볼게요.
| 구분 | Clojure | Common Lisp | Scheme | Java |
|---|---|---|---|---|
| nil이 있나요? | nil - '아무것도 없음'을 의미 | nil - false 또는 빈 리스트를 의미 | - | null |
| true가 있나요? | true | - | #t | true (primitive) |
| false가 있나요? | false | - | #f | false (primitive) |
| 조건문이 구분하는 것 | nil 또는 false / 나머지 전부 | nil / nil 아님 | #f / #f 아님 | false / true |
| 리스트/시퀀스 라이브러리가 구별되는 구체적 타입을 다루나요? | 아니요 - 많은 컬렉션 구현을 가진 시퀀스 추상화 | 예 - cons와 vector | 예 - pair | 아니요 - 많은 컬렉션 구현을 가진 Iterator 추상화 |
| 대표(singleton) 빈 리스트 값이 있나요? | 아니요 - 구체적인 컬렉션 타입의 각기 다른 빈 값을 가질 수 있음 | nil | '() | 아니요 |
| 시퀀스의 끝이 반환하는 것 | seq가 nil을 반환하는 논리적 시퀀스 |
nil | '() | false |
| 호스트의 null | nil | NA | NA | NA |
| 호스트의 true | true (boxed) | NA | NA | NA |
| 호스트의 false | false (boxed) | NA | NA | NA |