Clojure 메타데이터

Clojure 메타데이터 (Metadata)

Clojure의 심볼(symbol)과 컬렉션(collection)에는 메타데이터(metadata), 즉 그 대상을 설명하는 데이터 맵을 달아둘 수 있어요. 이 메타데이터 체계는 데이터에 임의의 주석을 달 수 있게 해줘요. 컴파일러에 타입 정보를 전달하는 데 쓰이기도 하지만, 애플리케이션 개발자가 데이터 소스나 정책 같은 것을 주석으로 표시하는 등 다양한 목적으로 쓸 수 있어요.

출처: Clojure 공식문서

본문

메타데이터는 값의 일부가 아니에요

메타데이터를 이해할 때 가장 중요한 점은, 메타데이터가 객체의 값(value)에 포함되지 않는다는 거예요. 그래서 메타데이터는 동등성(equality, 혹은 해시 코드)에 영향을 주지 않아요. 오직 메타데이터만 다른 두 객체는 동등하게 취급돼요.

다만 메타데이터와 객체의 관계 자체는 불변(immutable) 이에요. 메타데이터가 다른 객체는 곧 다른 객체죠. 여기서 따라오는 한 가지 결과가 있어요. 지연 시퀀스(lazy sequence)에 메타데이터를 적용하면 그 시퀀스의 머리(head)가 실현(realize)돼요. 그래야 두 객체가 같은 시퀀스를 공유할 수 있기 때문이에요.

메타데이터 읽기와 쓰기

메타데이터는 아래 함수들로 읽고 쓰고 바꿀 수 있어요.

(meta obj)

obj의 메타데이터를 돌려줘요. 메타데이터가 없으면 nil을 돌려줘요.

(pprint (meta #'+)) ;; #'+ 는 + var 입니다

;; {:added "1.2",
;;  :name +,
;;  :file "clojure/core.clj",
;;  :column 1,
;;  :line 984,
;;  :arglists ([] [x] [x y] [x y & more]),
;;  ...

(with-meta obj map)

obj같은 타입, 같은 값을 가지되 메타데이터가 map인 객체를 돌려줘요. 예시로 확인해 볼게요.

(def m ^:hi [1 2 3])
(meta (with-meta m {:bye true}))
;; {:bye true}

*print-meta*

이 플래그를 논리 참(logical true)으로 설정하면, 객체를 출력할 때 그 메타데이터도 리더(reader)가 다시 읽어들일 수 있는 형태로 함께 출력돼요.

(def m ^:hi [1 2 3])
(binding [*print-meta* true]
  (prn m))

;; ^{:hi true} [1 2 3]

(vary-meta obj f & args)

obj와 같되, 메타데이터가 (apply f (meta obj) args)인 객체를 돌려줘요. 기존 메타데이터에 함수를 적용해 새 메타데이터를 만드는 방식이에요.

(def m ^:hi [1 2 3])
(meta (vary-meta m merge {:bye true}))
;; {:hi true, :bye true}

(alter-meta! ref f & args)(reset-meta! ref map)

이 두 함수는 각각 네임스페이스/var/ref/agent/atom의 메타데이터를 수정하거나 초기화해요. alter-meta!는 함수로 바꾸고, reset-meta!는 아예 새 맵으로 설정하죠.

메타데이터 리더 (Metadata Reader)

with-meta 외에도, 읽기 시점(read-time) 에 그 뒤에 오는 표현식에 메타데이터를 적용해주는 리더 지원이 있어요. 이건 리더가 메타데이터 문법을 만나면 그대로 메타데이터를 붙여주는 방식이에요.

컴파일러가 메서드 오버로드(method overload)를 선택할 때 메타데이터를 어떻게 쓰는지는 Type Hints(타입 힌트)param-tags 문서를 보면 더 자세히 알 수 있어요.

더 알아보기