[241220] deep walking macro
Table of Contents
1 동기
deep walking macro 는 Timothy Baldridge 는 core.async 의 go 매크로를 만드는데 중심역할을 한 사람인 것 같다. 나는 아래 링크의 유튜브 영상을 몇년 전에 보았고, 오랫만에 다시 보았는데 아주 멋진 강의였다고 생각한다. 그래서 영상의 내용을 글로 적어 놓는 것이 아주 좋을 것 같다고 생각했다. 나 또한 다른 블로그를 보면 매크로에 대해 많은 관심을 가진 적이 있기 때문에
https://www.youtube.com/watch?v=HXfDK1OYpco&ab_channel=TimothyBaldridge
2 매크로
clojure에서 매크로와의 차이는 딱하나 meta 데이터에 macro 여부 변수다.
(. (var defmacro) (setMacro))
2.1 Var
setMacro를 가진 곳은 Var.java
객체이다.
Var 는 변수를 담는 박스같은 것이다. 변수를 정의하거나 값을 참조할 때 사용된다. 동적으로 재정의할 수 있으며, 상태(state)와 값을 분리하는데 유용한다.
(def x) #'user/x user=> x #object[clojure.lang.Var$Unbound 0x14008db3 "Unbound: #'user/x"] (def x 1) #'user/x user=> x 1
기본적으로 Var는 static 이지만, binding을 통해 동적으로 표시할 수 있음.
user=> (def ^:dynamic x 1) user=> (def ^:dynamic y 1) user=> (+ x y) 2 user=> (binding [x 2 y 3] (+ x y)) 5 user=> (+ x y) 2
Var.java 의 구현체에 setMacro
메소드가 존재한다.
static Keyword macroKey = Keyword.intern(null, "macro"); public void setMacro() { alterMeta(assoc, RT.list(macroKey, RT.T)) } public boolean isMacro(){ return RT.booleanCast(meta().valAt(macroKey)); }
여기서 RT.T
는 true
를 의미한다.
alterMeta
함수는 IReference
에 존재한다:
package clojure.lang; public interface IReference extends IMeta { IPersistentMap alterMeta(IFn alter, ISeq args) ; IPersistentMap resetMeta(IPersistentMap m); }
Var.java
는 AReference
를 확장하고 있다:
public abstract class ARef extends AReference implements IRef{ ... }
AReference
구현을 보자. IReference
인터페이스를 맵으로 구현한다:
package clojure.lang; public class AReference implements IReference { private IPersistentMap _meta; public AReference() { this(null); } public AReference(IPersistentMap meta) { _meta = meta; } synchronized public IPersistentMap meta() { return _meta; } synchronized public IPersistentMap alterMeta(IFn alter, ISeq args) { _meta = (IPersistentMap) alter.applyTo(new Cons(_meta, args)); return _meta; } synchronized public IPersistentMap resetMeta(IPersistentMap m) { _meta = m; return m; } }
단순히 setMacro 를 이용해서 macro 여부만 갖고 있게된 것이 매크로이지만 이것으로 컴파일 수행되기 이전에 코드가 확장된다. 이를 매크로확장이라고 한다.
(macroexpand '(when (when x y) z)) ;-> (if (if x (do y)) (do z)) ; 모든 when이 완전히 확장됩니다
macroexpand 는 컴파일 이전에 수행된다고 했다. 어디서 수행되는가? 답은 Compiler.java
에 있다:
class Compiler { static final public IPersistentMap specials = PersistentHashMap.create( DEF, new DefExpr.Parser(), LOOP, new LetExpr.Parser(), RECUR, new RecurExpr.Parser(), IF, new IfExpr.Parser(), CASE, new CaseExpr.Parser(), LET, new LetExpr.Parser(), Do, new BodyExpr.Parser(), FN, nul // ... ) static boolean isSpecial(object sym) { return specials.containsKey(sym); } static Object macroexpand(Object form) { Object exf = macroexpand1(form); if(exf != form) // 매크로 확장이 수행되었다면, 다시 매크로확장을 해본다. return macroexpand(exf); return form } static Object macroexpand1(Object x) { // x가 ISeq(시퀀스)가 아니면 매크로 확장이 불가능하므로 그대로 반환 if(x instanceof ISeq) { ISeq form = (ISeq) x; Object op = RT.first(form); if(isSpecial(op)) return x; Var v = isMacro(op); // if it's macro, it will be a non-null object. if (v != null) { checkSpecs(v, form); try { ISeq args = RT.cons(form, RT.cons(Compiler.LOCAL_ENV.get(), form.next())); return v.applyTo(args); // 매크로 확장이 일어나는 부분. } catch(...) {...} } else { // 문법적 변환 부분 (매크로 확장과는 다름) // Java 메서드 호출과 생성자 호출을 위한 구문 변환 // (.method obj args) -> (. obj method args) // (ClassName. args) -> (new ClassName args) } } return x; } }
v.applyTo(args)
로 평가를 하면 매크로는 코드를 만들어서 리턴하기 때문에 매크로확장이 된다.
2.2 Environment
매크로에서는 Environment 로 환경을 가져올 수 있음
(macroexpand '(defmacro foo [] 42)) => (do (clojure.core/defn foo ([&form &env] 42)) (. (var foo) (setMacro)) (var foo) )
그러므로 아래 매크로를 실행하면 environment를 출력할 수 있다.
(defmacro when [test & body] (println &env) `(if ~test (do ~body) nil)) (fn [x] (when true (println "hello" 42))) => {x #<LocalBinding clojure.lang.Compiler$LocalBinding@194527c6>}
이번엔
(meta #'when) {:arglists ([test & body]), :ns #<Namespace deep-walking-macro.core>, :name when :column 1 :line 22 :macro true ;; 이게 중요 }
3 deep walking macro
영상에서 말하는 core.async
의 go 매크로에서 주요로직이 되는 코드를 AST(Abstract Syntax Tree)
로 변환하는 파서를 구현한다.
parse-item
은 기본타입분류. parse-sexpr
는 S-expression 분석이다.
간단한 구조로는 모든 AST 는 최소한 :type 키를 가진다.
또한 ctx 는 아직 쓸일이 없어서 atom을 이용한 int의 개수를 카운트하는 기능을 구현해봄으로써 어떻게 ctx가 사용될 수 있는지 알 수 있는 대목이다.
(defmulti parse-item (fn [form ctx] (cond (seq? form) :seq (int? form) :int (symbol? form) :symbol (nil? form) :nil))) (defmulti parse-sexpr (fn [[sym & rest] ctx] sym)) #_(defmethod parse-item :seq [form ctx] (parse-sexpr form ctx)) (defmethod parse-item :seq [form ctx] (let [form (macroexpand form)] (parse-sexpr form ctx))) #_(defmethod parse-item :int [form ctx] {:type :int :value form}) (defmethod parse-item :int [form ctx] (swap! ctx inc) {:type :int :value form}) (defmethod parse-item :symbol [form ctx] {:type :symbol :value form}) (defmethod parse-item :nil [form ctx] {:type :nil}) (defmethod parse-sexpr 'if ;; 첫번째 인자는 필요없음 if 인걸 알고 있음. [[_ test then else] ctx] {:type :if :test (parse-item test ctx) :then (parse-item then ctx) :else (parse-item else ctx)}) ;; 이를 보면 우리는 각 표현식을 recursive 하게 해시맵을 만들고 있고 ;; 컴파일러는 배우고 있는 사람에게는 이건 AST 와 같음을 알 수 있다. (defmethod parse-sexpr 'do [[_ & body] ctx] {:type :do :body (doall (map (fn [x] (parse-item x ctx)) body))}) (comment (parse-item 42 nil) ;; {:type :int, :value 42} (parse-item 's nil) ;; {:type :symbol, :value s} (parse-item '(if x 42 41) nil) ;; {:type :if, :test {:type :symbol, :value x}, :then {:type :int, :value 42}, :else {:type :int, :value 41}} (parse-item '(when x 42 1) nil) ; Execution error (IllegalArgumentException) at core/eval10119$fn (REPL:16). ; No method in multimethod 'parse-sexpr' for dispatch value: when (parse-item '(when x 42 1) nil) ;; 두번째 개선 후. ; Execution error (IllegalArgumentException) at core/eval10424$fn (REPL:21). ; No method in multimethod 'parse-sexpr' for dispatch value: do (parse-item '(when x 42) {}) #_{:type :if, :test {:type :symbol, :value x}, :then {:type :do, :body ({:type :int, :value 42} {:type :int, :value 1})}, :else {:type :nil}} #_(defmacro when [test & body] `(if ~test (do ~body) nil)) ;; (let [a (atom 0)] (parse-item '(when t 42) a) (println @a)) (def b (atom 0)) (parse-item '(when t 42) b) @b (defmethod parse-sexpr :default [[f & body] ctx] {:type :call :fn (parse-item f ctx) :args (doall (map (fn [x] (parse-item x ctx)) body))}) (parse-item '(+ 2 3) (atom 0)) ;; {:type :call, :fn {:type :symbol, :value +}, :args ({:type :int, :value 2} {:type :int, :value 3})} (parse-item '((comp pos? +) 2 3) (atom 0)) ;; {:type :call, ;; :fn {:type :call, :fn {:type :symbol, :value comp}, :args ({:type :symbol, :value pos?} {:type :symbol, :value +})}, ;; :args ({:type :int, :value 2} {:type :int, :value 3})} (parse-item '(+ 2 3) (atom 0)) ;; {:type :call, :fn {:type :symbol, :value +}, :args ({:type :int, :value 2} {:type :int, :value 3})} (parse-item '((comp pos? +) 2 3) (atom 0)) ;; {:type :call, ;; :fn {:type :call, :fn {:type :symbol, :value comp}, :args ({:type :symbol, :value pos?} {:type :symbol, :value +})}, ;; :args ({:type :int, :value 2} {:type :int, :value 3})} (defmacro to-ast [form] (parse-item form (atom 0))) (to-ast (+ 1 2)) )