클로저 코드에서 만나는 특이한 문자들
클로저 코드에서 만나는 특이한 문자들 (Reading Clojure Characters)
Clojure 코드를 읽다 보면 #_, ~@, #::처럼 검색하기 어려운 문자들이 자주 나와요. 이름을 알기 전에는 '이게 뭐지?' 싶고, 구글에 문법을 그대로 붙여넣어도 제대로 안 나오죠. 이 페이지는 그런 검색하기 어려운 문자들의 문법을 정리한 가이드예요. 섹션은 특별한 순서를 따르진 않지만, 서로 관련된 항목끼리 묶어 두었어요. Clojure 리더(reader)에 대한 권위 있는 참고 자료는 리더 레퍼런스 페이지를 보면 돼요. 이 가이드는 James Hughes의 원래 블로그 글을 바탕으로 저자의 허락을 받아 확장한 내용이에요.
출처: Clojure 공식문서
본문
( ... ) — 리스트(List)
리스트는 연결 리스트(linked list)로 구현된 순차적·이종(heterogeneous) 컬렉션이에요. 세 값으로 이뤄진 리스트 하나를 볼게요.
(1 "two" 3.0)
[ ... ] — 벡터(Vector)
벡터는 순차적이고 인덱스가 있는 이종 컬렉션이에요. 인덱스는 0부터 시작해요. 세 값으로 이뤄진 벡터에서 인덱스 1의 값을 꺼내는 예시를 볼게요.
user=> (get ["a" 13.7 :foo] 1)
13.7
{ ... } — 맵(Map)
맵은 키와 값이 번갈아 나오는 이종 컬렉션이에요.
user=> (keys {:a 1 :b 2})
(:a :b)
# — 디스패치 문자(Dispatch character)
보통 #(이나 #\"처럼 옆에 다른 문자와 함께 쓰여요. #은 Clojure 리더(소스를 Clojure 데이터로 '읽는' 컴포넌트)에게 다음 문자를 어떻게 해석할지 알려 주는 특수 문자예요. 이 해석 규칙은 읽기 테이블(read table) 을 통해 정해져요. 리습 중에는 사용자가 읽기 테이블을 확장할 수 있는 것도 있지만, Clojure는 그렇게 할 수 없어요. 또 #은 신택스 쿼트(syntax quote) 안에서 생성 심볼을 만들 때 심볼 끝에 붙기도 해요.
#{ ... } — 집합(Set)
#{...}는 집합을 정의해요. 구체적으로는 hash-set인데요, 중복이 없는 값들의 컬렉션이죠. 다음 둘은 같아요.
user=> #{1 2 3 4}
#{1 2 3 4}
user=> (hash-set 1 2 3 4)
#{1 2 3 4}
집합은 중복을 허용하지 않아요. 그래서 리터럴에 중복이 있으면 유효하지 않은 리터럴이라 리더가 예외를 던져요. 집합에 이미 있는 값을 추가하면 그 값은 그냥 버려져요.
user=> #{1 2 3 4 1}
Syntax error reading source at (REPL:83:13).
Duplicate key: 1
#_ — 무시하기(Discard)
#_는 리더가 다음 폼(form)을 완전히 무시하라고 지시해요.
user=> [1 2 3 #_ 4 5]
[1 2 3 5]
#_ 뒤의 공백은 선택이라서 #_4처럼 써도 돼요.
user=> [1 2 3 #_4 5]
[1 2 3 5]
무시 문자는 edn에서도 동작해요. 또 #_를 여러 개 겹쳐서 여러 폼을 지울 수도 있어요.
user=> {:a 1, #_#_ :b 2, :c 3}
{:a 1, :c 3}
문서에 따르면 "#_ 다음의 폼은 리더가 완전히 건너뛴다(이건 nil을 돌려주는 comment 매크로보다 더 완전한 제거다)"고 해요. 디버깅 상황이나 여러 줄 주석에 유용해요.
#\"...\" — 정규 표현식(Regular Expression)
#\"는 정규 표현식의 시작을 나타내요.
user=> (re-matches #"^test$" "test")
"test"
이 폼은 읽기 시점 에 호스트별 정규식 엔진으로 컴파일되지만, edn에서는 사용할 수 없어요. Clojure에서 정규식을 쓸 때는 Java 문자열 이스케이프가 필요 없어요.
#(...) — 익명 함수(Anonymous function)
#(는 인라인 함수 정의의 축약 문법을 시작해요. 다음 두 코드는 같은 뜻이에요.
; 인자를 하나 받아 출력하는 익명 함수
(fn [line] (println line))
; 인자를 하나 받아 출력하는 익명 함수 - 축약형
#(println %)
리더는 익명 함수를 함수 정의로 펼치는데, 그 함수의 인자 개수(arity)는 % 자리표시자가 어떻게 선언됐는지로 정해져요. arity에 대한 설명은 % 문자 항목을 참고하세요.
user=> (macroexpand `#(println %))
(fn* [arg] (clojure.core/println arg)) ; 인자 이름은 보기 좋게 줄였다
#' — Var 쿼트(Var quote)
#'는 var 쿼트로, var 함수를 호출하는 형태로 펼쳐져요.
user=> (read-string "#'foo")
(var foo)
user=> (def nine 9)
#'user/nine
user=> nine
9
user=> (var nine)
#'user/nine
user=> #'nine
#'user/nine
쓰면 참조된 var를 돌려줘요. 값이 아니라 그 레퍼런스/선언 자체를 이야기하고 싶을 때 유용해요. 메타데이터(^) 항목에서 meta의 쓰임새를 함께 보면 이해가 쉬워요. var 쿼트는 edn에서는 쓸 수 없어요.
## — 상징적 값(Symbolic values)
Clojure는 ##Inf, ##-Inf, ##NaN 같은 상징적 값을 읽고 출력할 수 있어요. 이 값들은 edn에서도 쓸 수 있어요.
user=> (/ 1.0 0.0)
##Inf
user=> (/ -1.0 0.0)
##-Inf
user=> (Math/sqrt -1.0)
##NaN
#inst, #uuid, #js 등 — 태그드 리터럴(Tagged literals)
태그드 리터럴은 edn에서 정의되고 Clojure와 ClojureScript 리더가 기본 지원해요. #inst와 #uuid 태그는 edn이 정의하고, #js 태그는 ClojureScript가 정의해요. Clojure의 read-string으로 태그드 리터럴을 읽을 수 있어요(또는 직접 쓸 수도 있어요).
user=> (type #inst "2014-05-19T19:12:37.925-00:00")
java.util.Date ;; 호스트에 따라 다르다
user=> (read-string "#inst \"2014-05-19T19:12:37.925-00:00\"")
#inst "2014-05-19T19:12:37.925-00:00"
태그드 리터럴은 리터럴 값을 어떻게 파싱할지 리더에게 알려 줘요. #uuid는 UUID를 표현할 때 흔히 쓰고, ClojureScript 세계에서는 #js가 아주 흔한데, ClojureScript 데이터 구조를 JavaScript 구조로 바로 바꿔 주는 데 씁니다. 단, #js는 재귀적으로 변환하지 않아서, 중첩된 데이터 구조라면 clj->js를 쓰세요. #inst와 #uuid는 edn에 있지만 #js는 edn에 없어요.
%, %n, %& — 익명 함수 인자(Anonymous function arguments)
%는 익명 함수 #(...) 안에서 쓰는 인자예요. #(* % %)처럼 말이죠. 익명 함수가 펼쳐지면 fn 폼이 되고 % 인자들은 gensym 이름으로 바뀌는데, 여기서는 가독성을 위해 arg1 등으로 표기할게요.
user=> (macroexpand `#(println %))
(fn* [arg1] (clojure.core/println arg1))
% 바로 뒤에 숫자를 붙이면 인자 위치를 나타낼 수 있어요(1부터 시작). 익명 함수의 arity는 가장 큰 숫자의 % 인자로 정해져요.
user=> (#(println %1 %2) "Hello " "Clojure")
Hello Clojure ; 인자 2개를 받는다
user=> (macroexpand `#(println %1 %2))
(fn* [arg1 arg2] (clojure.core/println arg1 arg2)) ; 인자 2개
user=> (#(println %4) "Hello " "Clojure " ", Thank " "You!!")
You!! ; 인자 4개를 받되 앞의 3개는 안 쓴다
user=> (macroexpand `#(println %4))
(fn* [arg1 arg2 arg3 arg4] (clojure.core/println arg4)) ; 인자 4개, 3개는 안 쓴다
인자를 실제로 쓰지 않아도 되지만, 외부에서 호출할 때 기대하는 순서대로 선언은 해 줘야 해요. %와 %1은 서로 바꿔 쓸 수 있어요.
user=> (macroexpand `#(println % %1)) ; %와 %1 둘 다 사용
(fn* [arg1] (clojure.core/println arg1 arg1)) ; 여전히 인자 1개만 받는다
가변 인자 익명 함수에서는 %&가 있어요. 이건 (가장 큰 숫자로 이름 붙은 익명 인자 다음의) "나머지" 인자들을 나타내는 심볼이에요.
user=> (#(println %&) "Hello " "Clojure " ", Thank " "You!!")
(Hello Clojure , Thank You!! ) ; n개의 인자를 받는다
user=> (macroexpand '#(println %&))
(fn* [& rest__11#] (println rest__11#))
익명 함수와 %는 edn의 일부가 아니에요.
@ — Deref
@는 deref 함수를 호출하는 형태로 펼쳐져요. 그래서 다음 두 폼은 같아요.
user=> (def x (atom 1))
#'user/x
user=> @x
1
user=> (deref x)
1
@는 레퍼런스의 현재 값을 가져오는 데 써요. 위 예시는 atom의 현재 값을 @로 가져온 건데, future, delay, promise 등에도 적용할 수 있어요. 이 경우 계산을 강제하고 잠재적으로 블로킹할 수 있어요. @는 edn에 없어요.
^ (그리고 #^) — 메타데이터(Metadata)
^는 메타데이터 표시자예요. 메타데이터는 (축약 옵션이 있는) 값들의 맵으로, Clojure의 여러 폼에 붙일 수 있어요. 문서화, 컴파일 경고, 타입힌트 등 추가 정보를 제공해 주죠.
user=> (def ^{:debug true} five 5) ; 단일 불리언 값을 가진 메타 맵
#'user/five
메타데이터는 meta 함수로 접근할 수 있는데, 반환된 값이 아니라 선언 자체에 실행해야 해요.
user=> (def ^{:debug true} five 5)
#'user/five
user=> (meta #'five)
{:ns #<Namespace user>, :name five, :column 1, :debug true, :line 1, :file "NO_SOURCE_PATH"}
단일 값이라면 ^:name 축약 표기로 메타데이터를 선언할 수 있어요. 값이 true로 설정되니까 플래그 같은 데 유용하죠.
user=> (def ^:debug five 5)
#'user/five
user=> (meta #'five)
{:ns #<Namespace user>, :name five, :column 1, :debug true, :line 1, :file "NO_SOURCE_PATH"}
^의 또 다른 용도는 타입힌트예요. 값이 어떤 타입인지 컴파일러에게 알려서 타입별 최적화를 할 수 있게 해 주고, 결과적으로 코드가 더 빨라질 수 있어요.
user=> (def ^Integer five 5)
#'user/five
user=> (meta #'five)
{:ns #<Namespace user>, :name five, :column 1, :line 1, :file "NO_SOURCE_PATH", :tag java.lang.Integer}
이 예시에서 :tag 속성이 설정된 걸 볼 수 있어요. 축약 표기도 겹쳐 쓸 수 있어요.
user=> (def ^Integer ^:debug ^:private five 5)
#'user/five
user=> (meta #'five)
{:ns #<Namespace user>, :name five, :column 1, :private true, :debug true, :line 1, :file "NO_SOURCE_PATH", :tag java.lang.Integer}
원래 메타데이터는 #^로 선언했는데, 이제는 deprecated(그래도 동작은 해요)됐어요. 나중에 ^로 단순화됐고, 대부분의 Clojure 코드에서 보는 건 ^예요. 다만 오래된 코드에서 #^ 문법을 만날 수도 있어요. 메타데이터는 edn에 있지만 타입힌트는 edn에 없어요.
' — 쿼트(Quote)
쿼트는 다음 폼을 평가하지 말고 읽기만 하라는 뜻이에요. 리더는 '를 quote 특수 폼 호출로 펼쳐요.
user=> (1 3 4) ; 1을 함수로 호출하려 해서 실패한다
Execution error (ClassCastException) at myproject.person-names/eval230 (REPL:1).
class java.lang.Long cannot be cast to class clojure.lang.IFn
user=> '(1 3 4) ; 쿼트
(1 3 4)
user=> (quote (1 2 3)) ; 더 긴 쿼트 방법
(1 2 3)
; — 주석(Comment)
;는 한 줄 주석을 시작해서, 시작 지점부터 줄 끝까지의 모든 입력을 무시해요.
user=> (def x "x") ; 이것은 주석
#'user/x
user=> ; 이것도 주석
<아무것도 반환하지 않는다>
Clojure에서는 가독성이나 강조를 위해 세미콜론을 여러 개 쓰는 게 흔하지만, Clojure는 다 똑같이 취급해요.
;; 이것은 아마 이보다 더 중요하다
; 이것
: — 키워드(Keyword)
:는 키워드 표시자예요. 키워드는 맵의 키로 자주 쓰이며, 문자열보다 비교가 빠르고 메모리 오버헤드가 낮아요(인스턴스가 캐시되어 재사용되거든요).
user=> (type :test)
clojure.lang.Keyword
또는 keyword 함수로 문자열에서 키워드를 만들 수도 있어요.
user=> (keyword "test")
:test
키워드는 함수로 호출해서 맵에서 자기 자신을 키로 찾는 데 쓸 수도 있어요.
user=> (def my-map {:one 1 :two 2})
#'user/my-map
user=> (:one my-map) ; 함수로 호출해서 :one의 값을 가져온다
1
user=> (:three my-map) ; 없는 키를 안전하게 확인할 수 있다
nil
user=> (:three my-map 3) ; 기본값을 지정할 수 있다
3
user=> (get my-map :three 3) ; 위와 같지만 get을 쓴다
3
:: — 자동 해석 키워드(Auto-resolved keyword)
::는 키워드를 현재 네임스페이스에서 자동으로 해석해요. 한정자(qualifier)를 지정하지 않으면 현재 네임스페이스로 해석하고, 한정자를 지정하면 현재 네임스페이스의 별칭(alias)을 쓸 수 있어요.
user=> :my-keyword
:my-keyword
user=> ::my-keyword
:user/my-keyword
user=> (= ::my-keyword :my-keyword)
false
매크로를 만들 때 유용해요. 매크로 네임스페이스의 다른 함수를 호출하는 매크로가 그 함수를 제대로 호출하도록 보장하고 싶다면, ::my-function으로 정규화된 이름을 가리킬 수 있어요. ::는 edn에 없어요.
#:와 #:: — 네임스페이스 맵 문법(Namespace Map Syntax)
네임스페이스 맵 문법은 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"}}
이 두 맵은 완전히 동일한 객체를 나타내요. 그저 다른 문법일 뿐이에요.
#::는 맵 안의 키워드나 심볼 키의 네임스페이스를 현재 네임스페이스로 자동 해석해요. 다음 두 예시는 같아요.
user=> (keys {:user/a 1, :user/b 2})
(:user/a :user/b)
user=> (keys #::{:a 1, :b 2})
(:user/a :user/b)
자동 해석 키워드와 비슷하게, ns 폼에서 정의한 네임스페이스 별칭으로 자동 해석하려면 #::alias를 쓸 수도 있어요.
(ns rebel.core
(:require
[rebel.person :as p]
[rebel.ship :as s] ))
#::p{:first "Han"
:last "Solo"
:ship #::s{:name "Millennium Falcon"
:model "YT-1300f light freighter"}}
이건 이렇게 읽혀요.
{:rebel.person/first "Han"
:rebel.person/last "Solo"
:rebel.person/ship {:rebel.ship/name "Millennium Falcon"
:rebel.ship/model "YT-1300f light freighter"}}
마지막으로, 키워드가 한정되어 있으면 주어진 네임스페이스가 사용돼요. 그리고 키워드의 네임스페이스가 _면 네임스페이스가 없는 것으로 취급돼요.
#:shape{:_/type "Square"
:location/x 10
:location/y 12
:sides 4
:width 2
:height 2}
이건 이렇게 읽혀요.
{:type "Square"
:location/x 10
:location/y 12
:shape/sides 4
:shape/width 2
:shape/height 2}
/ — 네임스페이스 구분자(Namespace separator)
/는 나눗셈 함수 clojure.core//일 수도 있는데, 심볼 이름에서 심볼 이름과 네임스페이스 한정자를 구분하는 구분자로도 쓰여요. my-namespace/utils처럼요. 네임스페이스 한정자는 단순한 이름의 이름 충돌을 막아 줘요.
\ — 문자 리터럴(Character literal)
\는 리터럴 문자를 나타내요.
user=> (str \h \i)
"hi"
특수 ASCII 문자를 이름으로 나타내는 소수의 특수 문자도 있어요: \newline, \space, \tab, \formfeed, \backspace, \return. 또 \ 뒤에 \uNNNN 형태의 유니코드 리터럴을 쓸 수도 있어요. 예를 들어 \u03A9는 Ω의 리터럴이에요.
$ — 내부 클래스 참조(Inner class reference)
Java의 내부 클래스와 인터페이스를 참조하는 데 쓰여요. 컨테이너 클래스 이름과 내부 클래스 이름을 구분해요.
(import (basex.core BaseXClient$EventNotifier)
(defn- build-notifier [notifier-action]
(reify BaseXClient$EventNotifier
(notify [this value]
(notifier-action value))))
EventNotifier는 임포트된 Java 클래스인 BaseXClient 클래스의 내부 인터페이스예요.
->, ->>, some->, cond->, as-> 등 — 쓰레딩 매크로(Threading macros)
이들은 쓰레딩 매크로예요. 자세한 내용은 공식 Clojure 문서를 참고하세요.
` — 신택스 쿼트(Syntax quote)
`는 신택스 쿼트예요. 신택스 쿼트는 쿼트(평가를 지연)와 비슷하지만 추가 효과가 있어요. 기본 신택스 쿼트는 일반 쿼트와 비슷해 보일 수 있어요.
user=> (1 2 3)
Execution error (ClassCastException) at myproject.person-names/eval232 (REPL:1).
class java.lang.Long cannot be cast to class clojure.lang.IFn
user=> `(1 2 3)
(1 2 3)
하지만 신택스 쿼트 안에서 쓰인 심볼은 현재 네임스페이스 기준으로 완전히 해석돼요.
user=> (def five 5)
#'user/five
user=> `five
user/five
신택스 쿼트는 매크로 안에서 "템플릿" 메커니즘으로 가장 많이 쓰여요. 하나 직접 써 볼게요.
user=> (defmacro debug [body]
#_=> `(let [val# ~body]
#_=> (println "DEBUG: " val#)
#_=> val#))
#'user/debug
user=> (debug (+ 2 2))
DEBUG: 4
4
매크로는 컴파일러가 코드를 데이터로 삼아 호출하는 함수예요. 더 컴파일·평가될 수 있는 코드(데이터)를 돌려줄 것으로 기대되죠. 이 매크로는 하나의 본문 표현식을 받아, 그 본문을 평가하고 값을 출력한 뒤 값을 돌려주는 let 폼을 반환해요. 여기서 신택스 쿼트는 리스트를 만들지만 평가하지는 않아요. 그 리스트가 바로 코드인 거죠. 신택스 쿼트 안에서만 허용되는 ~@와 ~ 문법은 각 항목을 참고하세요.
~ — 언쿼트(Unquote)
~는 언쿼트예요. 신택스 쿼트는 쿼트처럼 신택스 쿼트된 폼 안에서 평가가 일어나지 않는다는 뜻이에요. 언쿼트는 쿼트를 끄고 신택스 쿼트된 표현식 안의 표현식을 평가해요.
user=> (def five 5) ; 값 5를 가진 이름 있는 var를 만든다
#'user/five
user=> five ; five 심볼은 그 값으로 평가된다
5
user=> `five ; five를 신택스 쿼트하면 심볼 평가를 막고 완전히 해석한다
user/five
user=> `~five ; 신택스 쿼트 블록 안에서 ~는 다음 폼에 대해서만 평가를 다시 켠다
5
user=> `[inc ~(+ 1 five)]
[clojure.core/inc 6]
신택스 쿼트와 언쿼트는 매크로를 쓸 때 필수 도구예요. 매크로는 컴파일 중에 호출되어 코드를 받아 코드를 돌려주는 함수죠.
~@ — 언쿼트 스플라이싱(Unquote splicing)
~@는 언쿼트-스플라이싱이에요. 언쿼트(~)가 폼을 평가하고 그 결과를 쿼트된 결과에 넣는 반면, ~@는 평가된 값이 컬렉션일 것을 기대하고 그 컬렉션의 내용 을 쿼트된 결과로 스플라이스해요.
user=> (def three-and-four (list 3 4))
#'user/three-and-four
user=> `(1 ~three-and-four) ; three-and-four를 평가해 결과에 넣는다
(1 (3 4))
user=> `(1 ~@three-and-four) ; three-and-four를 평가해 내용을 결과에 넣는다
(1 3 4)
이것도 매크로를 쓸 때 강력한 도구예요.
<symbol># — Gensym
심볼 끝에 붙은 #는 새 심볼을 자동 생성해요. 매크로 안에서 매크로 특유의 것이 사용자 공간으로 새어 나가는 걸 막는 데 유용해요. 매크로 정의 안에서 일반 let은 실패해요.
user=> (defmacro m [] `(let [x 1] x))
#'user/m
user=> (m)
Syntax error macroexpanding clojure.core/let at (REPL:1:1).
myproject.person-names/x - failed: simple-symbol? at: [:bindings :form :local-symbol]
spec: :clojure.core.specs.alpha/local-name
myproject.person-names/x - failed: vector? at: [:bindings :form :seq-destructure]
spec: :clojure.core.specs.alpha/seq-binding-form
myproject.person-names/x - failed: map? at: [:bindings :form :map-destructure]
spec: :clojure.core.specs.alpha/map-bindings
myproject.person-names/x - failed: map? at: [:bindings :form :map-destructure]
spec: :clojure.core.specs.alpha/map-special-binding
신택스 쿼트 안의 심볼은 여기 x라는 로컬 바인딩까지 포함해서 완전히 해석되기 때문이에요. 대신 변수 이름 끝에 #를 붙이면 Clojure가 고유한(한정되지 않은) 심볼을 생성하게 해 줄 수 있어요.
user=> (defmacro m [] `(let [x# 1] x#))
#'user/m
user=> (m)
1
중요한 건, 단일 신택스 쿼트 안에서 특정 x#을 쓸 때마다 같은 생성 이름이 사용된다는 거예요. 이 매크로를 펼쳐 보면 gensym된 이름을 볼 수 있어요.
user=> (macroexpand '(m))
(let* [x__681__auto__ 1] x__681__auto__)
#? — 리더 조건식(Reader conditional)
리더 조건식은 서로 다른 Clojure 방언들이 공통 코드를 공유하게 하려고 설계됐어요. 리더 조건식은 전통적인 cond와 비슷하게 동작해요. 사용 문법은 #?이고 이렇게 생겼어요.
#?(:clj (Clojure expression)
:cljs (ClojureScript expression)
:cljr (ClojureCLR expression)
:default (fallthrough expression))
#?@ — 스플라이싱 리더 조건식(Splicing Reader conditional)
스플라이싱 리더 조건식의 문법은 #?@예요. 리스트를 포함하는 폼으로 스플라이스하는 데 써요. 그래서 Clojure 리더는 이걸
(defn build-list []
(list #?@(:clj [5 6 7 8]
:cljs [1 2 3 4])))
이렇게 읽어요.
(defn build-list []
(list 5 6 7 8))
*var-name* — "이어머프(Earmuffs)"
이어머프(변수 이름 양끝을 감싸는 별표 한 쌍)는 많은 리습에서 특수 변수(special var) 를 나타내는 명명 관례예요. Clojure에서 가장 흔하게는 동적 변수, 즉 동적 스코프에 따라 달라질 수 있는 변수를 나타내는 데 써요. 이어머프는 "여기 용이 있으니 조심하라"는 경고 역할을 해서, 그 var의 상태를 절대 추측하지 말라는 뜻이에요. 기억하세요, 이건 관례지 규칙이 아니에요. Core Clojure 예시로는 표준 입력·출력 스트림을 나타내는 *out*과 *in*이 있어요.
>!!, <!!, >!, <! — core.async 채널 매크로
이 심볼들은 core.async — 채널 기반 비동기 프로그래밍(CSP, Communicating Sequential Processes)을 위한 Clojure/ClojureScript 라이브러리 — 의 채널 연산이에요. 이해를 위해 채널을 뭔가를 넣고 뺄 수 있는 큐(queue)의 일종이라고 상상해 보면, 이 심볼들이 그 단순한 API를 지원해요.
>!!와<!!는 각각 블로킹 put 과 take 예요>!와<!는 단순히 put 과 take 예요
차이점은 블로킹 버전이 go 블록 밖에서 동작하고, 동작하는 스레드를 블로킹한다는 거예요.
user=> (def my-channel (chan 10)) ; 채널을 만든다
user=> (>!! my-channel "hello") ; 채널에 넣는다
user=> (println (<!! my-channel)) ; 채널에서 뺀다
hello
논블로킹 버전은 go 블록 안에서 실행해야 해요. 그렇지 않으면 예외를 던져요.
user=> (def c (chan))
#'user/c
user=> (>! c "nope")
AssertionError Assert failed: >! used not in (go ...) block
nil clojure.core.async/>! (async.clj:123)
이 둘의 차이는 이 가이드의 범위를 벗어나지만, 기본적으로 go 블록은 스레드를 블로킹하지 않고 코드의 실행 을 일시 중지하면서 자기 자원을 스스로 관리해요. 이 덕분에 비동기로 실행되는 코드가 동기처럼 보여서, 코드베이스에서 비동기 코드를 관리하는 고통을 없애 줘요.
<symbol>? — 프레디킷 접미사(Predicate Suffix)
심볼 끝에 ?를 붙이는 것은 심볼 이름에 특수 문자를 허용하는 여러 언어에서 공통으로 쓰는 명명 관례예요. 그게 프레디킷, 즉 질문을 던지는 것임을 나타내는 데 써요. 예를 들어 버퍼 조작을 다루는 API를 쓴다고 상상해 볼게요.
(def my-buffer (buffers/create-buffer [1 2 3]))
(buffers/empty my-buffer)
한눈에, 이 경우 empty 함수가
- 전달된 버퍼가 비어 있으면
true를 돌려주는지, - 아니면 버퍼를 비우는(clear) 건지
어떻게 알 수 있을까요? 작성자가 empty를 is-empty로 이름 지을 수도 있었지만, Clojure의 풍부한 심볼 네이밍 덕분에 의도를 더 상징적으로 표현할 수 있어요.
(def my-buffer (buffers/create-buffer [1 2 3]))
(buffers/empty? my-buffer)
false
이건 그저 권장 관례일 뿐, 필수는 아니에요.
<symbol>! — 안전하지 않은 연산(Unsafe Operations)
클로저 스타일 가이드는 이렇게 말해요: "STM 트랜잭션에서 안전하지 않은 함수/매크로의 이름은 느낌표로 끝나야 한다 (예: reset!)." 가장 흔하게는 상태를 변경하는 함수 이름에 붙는 걸 볼 수 있어요. 데이터 저장소에 연결하거나, atom을 갱신하거나, 파일 스트림을 닫는 것처럼요.
user=> (def my-stateful-thing (atom 0))
#'user/my-stateful-thing
user=> (swap! my-stateful-thing inc)
1
user=> @my-stateful-thing
1
이것도 권장 관례일 뿐, 필수는 아니에요. 참고로 느낌표는 종종 "뱅(bang)"이라고 읽어요.
_ — 사용하지 않는 인자(Unused argument)
함수 인자나 let 바인딩에서 밑줄 문자를 보게 되면, _는 이 인자를 사용하지 않겠다는 뜻을 나타내는 흔한 명명 관례예요. 이것은 atom 값이 바뀔 때 콜백 방식 동작을 추가하는 데 쓰는 add-watch 함수를 사용한 예시예요. atom이 주어졌을 때, 값이 바뀔 때마다 새 값을 출력하고 싶다고 해 볼게요.
(def value (atom 0))
(add-watch value nil (fn [_ _ _ new-value]
(println new-value))
(reset! value 6)
; 6을 출력한다
(reset! value 9)
; 9를 출력한다
add-watch는 인자 네 개를 받는데, 우리가 관심 있는 건 마지막 인자 — atom의 새 값 — 뿐이에요. 그래서 나머지엔 _를 써요.
, — 공백 문자(Whitespace character)
Clojure에서 ,는 공백으로 취급돼요. 공백, 탭, 줄바꿈과 완전히 똑같죠. 그래서 리터럴 컬렉션에서 콤마는 절대 필요하지 않지만, 가독성을 높이려고 자주 써요.
user=>(def m {:a 1, :b 2, :c 3}
{:a 1, :b 2, :c 3}
더 알아보기
- Clojure 공식 리더 레퍼런스 — 리더에 대한 권위 있는 참고 자료
- edn Tagged Elements — 태그드 리터럴
- Clojure Special Forms —
quote,var등 특수 폼 - Clojure 매크로 문서 — 신택스 쿼트, 매크로
- 리더 조건식(Reader conditionals)
- core.async — 채널 기반 비동기 프로그래밍
- Clojure Style Guide —
?,!,_등의 명명 관례 - Clojure for the Brave and True — Writing Macros — 매크로 작성 입문
- Clojure from the ground up: macros