클로저 코드에서 만나는 특이한 문자들

클로저 코드에서 만나는 특이한 문자들 (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를 지원해요.

  • >!!<!!는 각각 블로킹 puttake 예요
  • >!<!는 단순히 puttake 예요

차이점은 블로킹 버전이 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) 건지

어떻게 알 수 있을까요? 작성자가 emptyis-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}

더 알아보기