멀티메서드와 계층(Hierarchies)
멀티메서드와 계층(Hierarchies)
Clojure는 상황 하나마다 새로운 데이터 타입을 만들어 내는 전통적인 객체지향 방식 대신, 적은 수의 타입 위에서 동작하는 함수 라이브러리를 크게 쌓아가는 쪽을 선호해요. 그렇다고 해서 Clojure가 런타임 다형성(polymorphism)의 가치를 놓치지는 않아요. 유연하고 확장 가능한 시스템 아키텍처를 만드는 데 런타임 다형성이 얼마나 중요한지 충분히 알고 있거든요. Clojure는 하나 이상의 인자에 대해 타입, 값, 속성(attribute), 메타데이터, 그리고 인자 사이의 관계까지 디스패치할 수 있는 멀티메서드(multimethod) 시스템을 통해 정교한 런타임 다형성을 지원해요.
출처: Clojure 공식문서
본문
Clojure의 멀티메서드는 디스패칭 함수(dispatching function) 하나와 메서드(method) 하나 이상의 조합이에요. 멀티메서드를 정의할 때, 즉 defmulti로 정의할 때 반드시 디스패칭 함수를 제공해야 해요. 이 함수는 멀티메서드의 인자에 적용되어 디스패치 값(dispatching value) 을 만들어 내요. 그러면 멀티메서드는 그 디스패치 값, 또는 디스패치 값이 유도된(derived) 값과 연결된 메서드를 찾아요. 그런 메서드가 (defmethod로) 정의되어 있다면, 그 메서드를 인자와 함께 호출하고 그 결괏값이 멀티메서드 호출의 값이 돼요. 만약 디스패치 값에 연결된 메서드가 없다면, 멀티메서드는 기본 디스패치 값(기본적으로 :default)에 연결된 메서드를 찾아보고, 있으면 그걸 써요. 그렇지 않으면 호출은 오류가 돼요.
멀티메서드 시스템은 이런 API를 노출해요. defmulti는 새 멀티메서드를 만들고, defmethod는 디스패치 값에 연결된 멀티메서드의 새 메서드를 만들어 설치하며, remove-method는 디스패치 값에 연결된 메서드를 제거하고, prefer-method는 메서드들이 달리 모호할 때 메서드 사이에 순서를 만들어 줘요.
유도(derivation)는 클래스 값에 대해서는 Java 상속(inheritance), 또는 Clojure의 임시(ad hoc) 계층 시스템을 조합해 결정돼요. 계층 시스템은 이름(심볼이나 키워드) 사이의 유도 관계와, 클래스와 이름 사이의 관계를 지원해요. derive 함수가 이런 관계를 만들고, isa? 함수가 그 존재를 검사해요. 여기서 isa?는 instance?가 아니라는 점에 주의하세요.
계층 관계는 (derive child parent)로 정의할 수 있어요. child와 parent는 심볼이거나 키워드일 수 있고, 반드시 네임스페이스로 한정(namespace-qualified)되어야 해요.
참고: :: 리더 문법을 쓰는데요, ::키워드는 네임스페이스를 해석해요.
::rect
-> :user/rect
derive는 관계를 만드는 근본적인 도구예요.
(derive ::rect ::shape)
(derive ::square ::rect)
parents / ancestors / descendants 와 isa?는 계층을 조회하게 해 줘요.
(parents ::rect)
-> #{:user/shape}
(ancestors ::square)
-> #{:user/rect :user/shape}
(descendants ::shape)
-> #{:user/rect :user/square}
(= x y)는 (isa? x y)를 함의해요.
(isa? 42 42)
-> true
isa?는 계층 시스템을 사용해요.
(isa? ::square ::shape)
-> true
child로 클래스를 쓸 수도 있어요. 다만 parent로는 쓸 수 없어요. 클래스의 child가 되게 하는 유일한 방법은 Java 상속이거든요.
이 덕분에 기존 Java 클래스 계층 위에 새로운 분류 체계(taxonomy)를 얹을 수 있어요.
(derive java.util.Map ::collection)
(derive java.util.Collection ::collection)
(isa? java.util.HashMap ::collection)
-> true
isa?는 클래스 관계도 검사해요.
(isa? String Object)
-> true
parents / ancestors도 마찬가지예요. 다만 descendants는 그렇지 않아요. 클래스의 자손은 열린 집합(open set)이라서요.
(ancestors java.util.ArrayList)
-> #{java.lang.Cloneable java.lang.Object java.util.List
java.util.Collection java.io.Serializable
java.util.AbstractCollection
java.util.RandomAccess java.util.AbstractList}
isa?는 벡터에 대해서도 동작하는데, 대응하는 각 요소에 isa?를 호출하는 방식이에요.
(isa? [::square ::rect] [::shape ::shape])
-> true
isa? 기반 디스패치
멀티메서드는 디스패치 값이 일치하는지 검사할 때 = 대신 isa?를 사용해요. 다만 isa?의 첫 번째 검사가 =이기 때문에 정확히 일치하는 경우도 동작한다는 점을 기억하세요.
(defmulti foo class)
(defmethod foo ::collection [c] :a-collection)
(defmethod foo String [s] :a-string)
(foo [])
:a-collection
(foo (java.util.HashMap.))
:a-collection
(foo "bar")
:a-string
prefer-method는 둘 중 어느 쪽도 다른 쪽을 지배하지 않아서 여러 개가 일치하는 경우를 모호함을 풀어낼 때 써요. 멀티메서드마다 어떤 디스패치 값이 다른 값보다 우선하는지 그냥 선언하면 돼요.
(derive ::rect ::shape)
(defmulti bar (fn [x y] [x y]))
(defmethod bar [::rect ::shape] [x y] :rect-shape)
(defmethod bar [::shape ::rect] [x y] :shape-rect)
(bar ::rect ::rect)
-> Execution error (IllegalArgumentException) at user/eval152 (REPL:1).
Multiple methods in multimethod 'bar' match dispatch value:
[:user/rect :user/rect] -> [:user/shape :user/rect]
and [:user/rect :user/shape], and neither is preferred
(prefer-method bar [::rect ::shape] [::shape ::rect])
(bar ::rect ::rect)
-> :rect-shape
위의 모든 예시는 멀티메서드 시스템이 쓰는 전역 계층(global hierarchy)을 사용해요. 하지만 make-hierarchy로 완전히 독립적인 계층을 만들 수도 있고, 위 함수들은 전부 선택적으로 첫 인자로 계층을 받을 수 있어요.
이 단순한 시스템은 아주 강력해요. Clojure 멀티메서드와 전통적인 Java식 단일 디스패치(single dispatch)의 관계를 이해하는 한 가지 방법은, 단일 디스패치가 마치 첫 인자에 getClass를 호출하는 디스패치 함수를 가진 Clojure 멀티메서드와 같다는 거예요. 그리고 그 메서드들이 그 클래스들과 연결되어 있는 꼴이죠. Clojure 멀티메서드는 클래스/타입에 고정(hard-wired)되어 있지 않아요. 인자의 어떤 속성에든, 여러 인자에 기반할 수도 있고, 인자의 검증을 수행하고 오류 처리 메서드로 라우팅하는 일까지 할 수 있어요.
참고: 이 예시에서는 키워드 :Shape를 디스패치 함수로 쓰고 있어요. 키워드는 맵의 함수이기 때문인데, 이건 데이터 구조 문서에서 설명한 내용이에요.
(defmulti area :Shape)
(defn rect [wd ht] {:Shape :Rect :wd wd :ht ht})
(defn circle [radius] {:Shape :Circle :radius radius})
(defmethod area :Rect [r]
(* (:wd r) (:ht r)))
(defmethod area :Circle [c]
(* (. Math PI) (* (:radius c) (:radius c))))
(defmethod area :default [x] :oops)
(def r (rect 4 13))
(def c (circle 12))
(area r)
-> 52
(area c)
-> 452.3893421169302
(area {})
-> :oops
더 알아보기
- defmulti — 새 멀티메서드를 만드는 함수
- defmethod — 디스패치 값에 메서드를 설치하는 함수
- derive — 계층 유도 관계를 만드는 함수
- isa? — 유도/상속 관계를 검사하는 함수
- prefer-method — 모호한 메서드 사이의 우선순위를 정하는 함수
- make-hierarchy — 독립된 계층을 만드는 함수
- Protocols — 멀티메서드와 함께 런타임 다형성을 제공하는 또 다른 방식