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

더 알아보기