Clojure 매크로
Clojure 매크로 (Macros)
Clojure는 프로그램이 컴파일러를 확장할 수 있는 매크로 시스템을 갖고 있어요. 이 덕분에 다른 언어라면 문법 구조를 만들기 위해 프리미티브나 언어 차원의 지원이 필요했을 작업을, 그냥 일반 코드로 처리할 수 있습니다. 흥미로운 점은 Clojure의 핵심 구조 중 상당수가 사실 프리미티브가 아니라 보통의 매크로라는 거예요. 매크로가 실제로 어떻게 동작하는지 조금만 들여다보면 Clojure의 철학이 더 또렷하게 보입니다.
출처: Clojure 공식문서
본문
매크로는 프리미티브의 조합이다
어떤 매크로는 여러 프리미티브 형태를 단순히 조합해 만들기도 해요. 대표적인 예가 when인데, when은 if와 do를 합쳐 놓은 것입니다.
아래 예시에서 macroexpand로 when 호출을 펼쳐 보면 실제로 어떤 형태로 바뀌는지 바로 드러나요. 조건이 참이면 do 블록 안의 표현들이 순서대로 실행됩니다.
user=> (macroexpand '(when (pos? a) (println "positive") (/ b a)))
(if (pos? a) (do (println "positive") (/ b a)))
어떤 매크로는 형태를 유용하게 재배열해 주기도 합니다. ->(스레딩 매크로)가 대표적인 예인데, 각 표현식을 다음 표현식의 첫 번째 인자로 재귀적으로 집어넣어요. 그래서 코드가 데이터가 흐르는 방향 그대로 읽힙니다.
user=> (-> {} (assoc :a 1) (assoc :b 2))
{:b 2, :a 1}
user=> (macroexpand '(-> {} (assoc :a 1) (assoc :b 2)))
(assoc (assoc {} :a 1) :b 2)
macroexpand로 확인해 보면, ->는 중첩된 assoc 호출로 펼쳐지는 걸 볼 수 있어요. 같은 결과를 얻는 코드지만, 값이 각 호출의 처음 인자로 자동으로 흘러 들어가므로 읽기 훨씬 편해집니다.
매크로 안에서 쓸 수 있는 특별 변수
defmacro 안에서는 더 고급스러운 용도를 위해 두 가지 특별 변수를 사용할 수 있어요.
&form— 호출되고 있는 실제 형태(데이터로서).&env— 매크로 확장 시점의 로컬 바인딩 맵. 이 맵은 심볼을 그 바인딩에 대한 컴파일러 정보를 담은 객체로 연결해 줍니다.
주요 매크로 분류
아래 매크로들은 모두 API 페이지에 문서화되어 있어요. 여러 개는 관련 주제 페이지에서도 함께 다뤄집니다.
- 매크로 만들기:
defmacro,definline,macroexpand-1,macroexpand - 분기 (Branching):
and,or,when,when-not,when-let,when-first,if-not,if-let,cond,condp - 반복 (Looping) — Sequences도 참고:
for,doseq,dotimes,while - var 다루기 — Vars and Environment도 참고:
ns,declare,defn,defmacro,definline,defmethod,defmulti,defn-,defonce,defstruct - 코드 배치 바꾸기:
..,doto,-> - 동적 스코프 — Vars and Environment도 참고:
binding,locking,time,with-in-str,with-local-vars,with-open,with-out-str,with-precision - lazy한 것 만들기 — Sequences도 참고:
lazy-seq,lazy-cat,delay - Java interop 매크로:
..,amap,areduce,gen-class,gen-interface,proxy,proxy-super,memfn - 코드 문서화:
assert,comment,doc - 트랜잭션:
dosync,io!
한 가지 재미있는 사실은, 몇몇 특별 형태(special forms)가 실제로는 매크로로 구현되어 있다는 점입니다. 주로 디스트럭처링(destructuring)을 지원하기 위한 것들이에요. 대표적으로 fn, let, loop가 그렇습니다.