[230911] Var에 대해서
Table of Contents
1 Var
Clojure는 동시성을 위해 4가지 방식을 제공한다.
- Vars
- Refs
- Agent
- Atom
하지만 Var는 그 중에서도 특별한 역할을 한다.
Vars는 동적 바인딩을 제공한다. 이는 binding을 변경하여(새로운 저장 위치로 리바인딩) 변경가능한 저장위치를 참조할 수 있게 한다는 말이다.
Var의 바인딩은 두 가지로 나뉜다.
- 스레드별 바인딩
- 루트 바인딩
말그대로 스레드별로 바인딩될 수 있고, 루트에 바인딩되어 모든 곳에서 바라볼 수 있다는 말이다.
공식문서에 따르면, def
는 Var를 생성한다(interns).
기본적으로 def
만으로는 변경될 수 없다.
user=> (def x 1) #'user/x user=> (binding [x 2] x) Execution error (IllegalStateException) at user/eval136 (REPL:1). Can't dynamically bind non-dynamic var: user/x
하지만 ^:dynamic
메타데이터 태그를 붙이면 스레드별 동적바인딩이 가능해진다.
user=> (def ^:dynamic a 1) #'user/a user=> (binding [a 2] a) 2
하지만 binding 함수를 쓸 수 없는 static Var 도 변경하고 싶은 경우가 있을텐데(mock 처럼)
이럴때를 위한 with-redef
, with-redefs-fn
가 제공된다.
2 Interning
Var가 특이한 이유는 이 Var 객체가 네임스페이스 시스템에서 관리되기 때문이다.
Namespace는 (심볼, Var) 형태의 글로벌 map을 관리한다.
만약 def
표현식이 평가될 때, 현재 네임스페이스에 기존 심볼에 대해 인턴된 항목을 찾지 못하면 하나를 사용하고,
그렇지 않으면 기존 심볼을 이용한다. 이런 find-or-create process
를 interning
이라고 한다.
즉, 매핑이 해제되지 않는 한 Var 객체는 안정적인 참조이므로 매번 조회할 필요가 없다.
또한 네임스페이스는 evaluation 에서 설명한 대로 컴파일러는 모든 free symbols를 Var로 리졸브(resolve)하려고 한다.
var
special form 혹은 #'
리더 매크로(see Reader)를 이용하여, 현재 값 대신 interend Var object
를 가져올 수 있다.
3 alter-var-root
공식문서에서 Var에 대한 함수가 많지만 alter-var-root
는 의존성을 다루는 라이브러리에서 많이 사용한다.
clip
, Component
가 있지만 아마 다른 라이브러리들도 많이 사용할 것이다.
코드를 보자. alter-var-root
(defn alter-var-root "Atomically alters the root binding of var v by applying f to its current value plus any args" {:added "1.0" :static true} [^clojure.lang.Var v f & args] (.alterRoot v f args))
사용해보자
user=> (def counter 0) #'user/counter user=> (alter-var-root #'counter inc) 1 user=> counter 1
clojure는 불변 아니었나?
맞다 하지만, Var는 동시성을 위한 기능이다. 그러니 괜찮다.
alter-var-root
에 Atomically alter라는 말이 써있다. 소스코드 내용은 java 코드임을 알 수 있다.
실제 코드를 보자. Var#alterRoot
public final class Var extends ARef implements IFn, IRef, Settable, Serializable { volatile Object val; volatile Object root; synchronized public Object alterRoot(IFn fn, ISeq args) { Object newRoot = fn.applyTo(RT.cons(root, args)); validate(getValidator(), newRoot); Object oldroot = root; this.root = newRoot; ++rev; notifyWatches(oldroot,newRoot); return newRoot; }
단순하게 fn
을 현재 root
에 있는 값에 args
를 먹여서 적용(apply
) 하고 교체해버린다.
이것이 Atomic하게 일어나는 이유는 volatile
과 synchronized
의 마법이다.
즉, REPL로 프로그램을 실행한채로 코드를 수정하고 그대로 붙이고 싶다면(on the fly) 여기 이 rootVar을 교체하면 쉽게 가능해진다.
mutable한 작업을 위해서 일반 어플리케이션 개발에 Var을 자주 사용하는 것은 잘못된 사용처럼 보인다.