[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))
)