Clojure에서 프로그램 소스를 데이터 구조로 해석하는 리더(Reader)에 관한 공식 참조 문서로, 기호, 리터럴, 시퀀스 및 리더 매크로 등의 주요 구문 요소를 다루며, EDN 및 태그된 리터럴, 리더 조건문 등 확장적 데이터(X-Data) 문법까지 폭넓게 설명한다.
Clojure는 호모아이코닉(homoiconic) 언어입니다. 이는 곧 Clojure 프로그램이 Clojure의 데이터 구조 그 자체로 표현된다는 의미입니다. 이는 대부분의 프로그래밍 언어와 Clojure(및 Common Lisp) 간의 매우 중요한 차이를 만듭니다 — Clojure는 문자 스트림/파일의 문법이 아니라 데이터 구조의 평가로 정의됩니다. 그래서 Clojure 프로그램이 다른 Clojure 프로그램을 조작, 변환, 생성하는 것이 흔하고 쉽습니다.
하지만 대부분의 Clojure 프로그램은 텍스트 파일로부터 시작하며, 텍스트를 파싱해 데이터 구조를 만들어내는 역할을 하는 것이 바로 리더(reader) 입니다. 이 과정은 단순히 컴파일러의 한 단계가 아닙니다. 리더와 Clojure의 데이터 표현들은 XML이나 JSON처럼 독립적으로 유용하게 사용될 수 있습니다.
한마디로, 리더는 캐릭터 기반의 구문(syntax)을 갖고, Clojure 언어는 심볼, 리스트, 벡터, 맵 등 데이터 구조 기반의 구문을 가집니다. 리더는 read 함수로 구현되어 있는데, 이는 스트림에서 다음 폼(form; 문법 단위)을 읽어 그에 해당하는 오브젝트를 반환합니다.
이 참조는 평가가 시작되는 지점, 즉 리더 폼부터 설명을 시작합니다. 곧이어 각 데이터 구조와 이를 해석하는 컴파일러에 대한 설명이 뒤따릅니다.
기호는 숫자가 아닌 문자로 시작하며, 영문자 및 *, +, !, -, _, ', ?, <, >, = 등 일부 특수문자를 포함할 수 있습니다(추후 추가될 수도 있음).
'/'는 특별한 의미를 가지며 네임스페이스와 이름을 나누는 구분자로 한 번만 사용할 수 있습니다. 예: my-namespace/foo
. 단독 /
는 나눗셈 함수명을 의미합니다.
'.' 역시 특별한 의미를 가지며, 클래스 명처럼 중간에 하나 이상 사용될 수 있습니다. 예: java.util.BitSet
. 네임스페이스에도 사용 가능하며, 처음이나 끝에 오는 '.'는 금지되어 있습니다. /
나 .
를 포함하는 기호는 '정규화(qualified)'된 기호입니다.
':'로 시작하거나 끝나는 기호는 Clojure에서 예약되어 있습니다. 하나 이상의 ':'를 포함하는 기호도 존재합니다.
문자열: "로 감쌈. 여러 줄에 걸칠 수 있으며, 자바 표준 이스케이프 문자 지원.
숫자: 대체로 자바와 동일.
2r101010
, 052
, 8r52
, 0x2a
, 36r16
, 42
는 모두 같은 Long 값.22/7
.문자: 역슬래시()로 표기, 예: \c
, \newline
, \space
등. 자바식 유니코드(\uNNNN), 8진법(\oNNN) 지원.
nil: '값 없음'을 의미, Java의 null과 대응, 논리적 거짓으로도 판별됨.
불린: true
, false
.
기호값: ##Inf
, ##-Inf
, ##NaN
.
키워드(keyword): 심볼 비슷하지만 반드시 콜론(:)으로 시작. 예: :fred
. 네임스페이스 포함 가능: :person/name
('.' 포함 가능).
괄호로 감싼 0개 이상의 폼: (a b c)
대괄호로 감싼 0개 이상의 폼: [1 2 3]
{:a 1 :b 2}
{:a 1, :b 2}
Added in Clojure 1.9
맵 리터럴을 쓸 때 #:ns
접두어로 기본 네임스페이스를 지정 가능 (여기서 ns는 네임스페이스 명). #::
는 키워드처럼 자동 네임스페이스 정규화.
네임스페이스 구문을 쓴 맵 리터럴은 다음과 같이 읽힙니다:
_
네임스페이스는 제거)예시:
#:person{:first "Han"
:last "Solo"
:ship #:ship{:name "Millennium Falcon"
:model "YT-1300f light freighter"}}
=>
{:person/first "Han"
:person/last "Solo"
:person/ship {:ship/name "Millennium Falcon"
:ship/model "YT-1300f light freighter"}}
셋은 중괄호와 #으로 표기: #{:a :b :c}
Added in Clojure 1.3
#my.klass_or_type_or_record[:a :b :c]
#my.record{:a 1, :b 2}
리더는 built-in 구문과 읽기 테이블(read table; macro-characters 매핑)의 조합으로 동작합니다. 특정 문자는 매크로 문자로, 이 문자에 매핑된 리더 매크로가 동작합니다(대부분 심볼 내에선 사용 불가).
'form
⇒ (quote form)
위에서 설명한 대로, 문자 리터럴 생성. 예: \a \b \c
.
자주 쓰이는 특수문자도 \newline
, \space
, \tab
, \formfeed
, \backspace
, \return
으로 표기 가능.
유니코드는 자바식(\uNNNN), 예: \u03A9
=> Ω.
한 줄 주석. 세미콜론부터 라인 끝까지 무시됨.
@form ⇒ (deref form)
메타데이터는 심볼, 리스트, 벡터, 셋, 맵, 태그된 리터럴, 레코드, 타입, 생성자에 붙일 수 있는 맵입니다. 메타데이터 리더 매크로는 다음 폼을 읽으면서 메타데이터를 부여합니다(with-meta로 조회 가능):
^{:a 1 :b 2} [1 2 3]
→ [1 2 3]
벡터에 {:a 1 :b 2}
메타데이터 부여
단축형:
^String x
== ^{:tag java.lang.String} x
)^[String long _]
== ^{:param-tags [java.lang.String long _]}
)^:dynamic x
== ^{:dynamic true} x
)여러 메타데이터는 오른쪽에서 왼쪽으로 순차 병합됨.
디스패치 매크로는 다음 문자에 따라 다른 리더 매크로를 호출합니다.
#"패턴"
(: java.util.regex.Pattern), 패턴 내 백슬래시는 추가 이스케이프 불필요. (예 (re-pattern "\\s*\\d+")
== #"\s*\d+"
)#'x
⇒ (var x)
#()
⇒ (fn [args] (...))
(%, %n, %&
매개변수 기준)#_
(아예 완전히 스킵; comment
는 nil 반환)Symbols, Lists, Vectors, Sets, Maps 이외에는 x
가 'x
와 동일.
예제:
user=> (def x 5)
user=> (def lst '(a b c))
user=> `(fred x ~x lst ~@lst 7 8 :nine)
(user/fred user/x 5 user/lst a b c 7 8 :nine)
리더 테이블(read table)은 사용자 코드에서 직접 접근 불가.
Clojure 리더는 edn(extensible data notation) 규격의 확장 집합을 지원합니다. edn 명세는 Clojure 데이터 구문 일부를 언어 중립적으로 표준화합니다.
태그된 리터럴은 Clojure의 edn 태그 요소 구현입니다.
Clojure 시작 시 classpath 루트에서 data_readers.clj
혹은 data_readers.cljc
파일을 찾아, 이 안에 심볼 - 함수 매핑 맵이 있어야 합니다. 예시:
{foo/bar my.project.foo/bar
foo/baz my.project/baz}
각 키는 리더가 인식할 태그, 값은 호출될 완전한 이름의 Var입니다. 예를 들어, 위 파일에서 #foo/bar [1 2 3]
리터럴이 있으면 Clojure는 벡터 [1 2 3]
을 읽은 뒤 'my.project.foo/bar
Var를 호출합니다.
네임스페이스 없는 리더 태그는 Clojure가 예약합니다. default-data-readers에 정의된 기본 태그들은 data_readers.clj/cljc
, 혹은 data-readers 바 재바인딩으로 덮어쓸 수 있습니다. 읽기 시 태그에 맞는 함수가 없으면, default-data-reader-fn에 바인딩된 함수가 (기본 nil) 호출되거나 없으면 예외 발생.
data_readers.cljc
파일은 reader 조건문과 같은 규칙으로 해석됩니다.
Clojure 1.4부터 instant 와 UUID 태그 리터럴이 있습니다. Instant 예: #inst "2018-03-28T10:48:00.000"
(자세한 포맷은 구현 참고). 디폴트 리더는 java.util.Date로 파싱:
(def instant #inst "2018-03-28T10:48:00.000")
(= java.util.Date (class instant)) ;=> true
data-readers를 바인딩하여 다른 파서를 쓸 수 있습니다. 예: clojure.instant/read-instant-calendar
(java.util.Calendar), clojure.instant/read-instant-timestamp
(java.util.Timestamp):
(binding [*data-readers* {'inst read-instant-calendar}]
(= java.util.Calendar (class (read-string (pr-str instant))))) ;=> true
(binding [*data-readers* {'inst read-instant-timestamp}]
(= java.util.Timestamp (class (read-string (pr-str instant))))) ;=> true
#uuid
태그 리터럴은 java.util.UUID로 파싱:
(= java.util.UUID (class (read-string "#uuid \"3b8a31ed-fd89-4f1b-a00f-42e3d60cf5ce\""))) ;=> true
태그 리더 함수가 없으면 default-data-reader-fn가 호출됩니다. tagged-literal을 이용해, 인식 못한 리터럴을 객체로 저장할 수도 있습니다(키워드로 :tag, :form 조회 가능):
(set! *default-data-reader-fn* tagged-literal)
;; #object 읽기 시 TaggedLiteral 객체 생성
(def x #object[clojure.lang.Namespace 0x23bff419 "user"])
[(:tag x) (:form x)] ;=> [object [clojure.lang.Namespace 599782425 "user"]]
Clojure 1.7부터 다중 플랫폼 지원용 .cljc 파일이 도입되었습니다. 플랫폼별 분기를 최소 네임스페이스로 분리 or 소수 코드만 분기 필요할 때 _리더 조건문_을 사용합니다. 리더 조건문은 .cljc 파일과 디폴트 REPL만 지원, 반드시 꼭 필요할 때만 사용 권장합니다.
리더 조건문은 #?
혹은 #?@
로 시작하며 cond 처럼 feature:expr 쌍. 각 클로저 플랫폼은 고유 feature (:clj, :cljs, :cljr 등)을 갖고, 매칭되는 첫 feature의 expr만 실행. 미매칭 branch expr은 읽지만 건너뜀. :default feature도 항상 매칭. 미매칭시 폼 자체가 생략됨.
비공식 Clojure 플랫폼은 자기 고유 prefix를 권장합니다. 비정규화 platform feature 명(:clj, :cljs 등)은 공식 플랫폼 전용 예약.
예시 (Clojure: Double/NaN, ClojureScript: js/NaN, 기타: nil):
#?(:clj Double/NaN
:cljs js/NaN
:default nil)
#?@
는 expr이 컬렉션이며 unquote-splicing에 대응. 최상위에서는 예외 발생. 예:
[1 2 #?@(:clj [3 4] :cljs [5 6])]
;; clj => [1 2 3 4]
;; cljs => [1 2 5 6]
;; 이외 => [1 2]
read와 read-string은 옵션 맵을 첫 인자로 받음. :read-cond는 :allow(리더 조건문 처리), :preserve(모든 분기 유지) 설정 가능.
:read-cond - :allow(리더 조건문 처리), :preserve(모든 분기 보존)
:features - 활성 feature 키 집합
예시(클로저에서 ClojureScript 조건문 테스트):
(read-string
{:read-cond :allow
:features #{:cljs}}
"#?(:cljs :works! :default :boo)")
;; :works!
단, Clojure 리더는 항상 :clj feature를 기본 주입. 플랫폼 불문 방식으론 tools.reader 참고.
{:read-cond :preserve}로 호출하면 비선택 분기까지 데이터로 보존:
(read-string
{:read-cond :preserve}
"[1 2 #?@(:clj [3 4] :cljs [5 6])]")
;; [1 2 #?@(:clj [3 4] :cljs [5 6])]