[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.Ttrue 를 의미한다. alterMeta 함수는 IReference 에 존재한다:

package clojure.lang;

public interface IReference extends IMeta {
    IPersistentMap alterMeta(IFn alter, ISeq args) ;
    IPersistentMap resetMeta(IPersistentMap m);
}

Var.javaAReference 를 확장하고 있다:

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

  )

Author: 남영환

Created: 2024-12-21 Sat 16:39

Emacs 27.2 (Org mode 9.4.4)

Validate