멀티메서드와 계층

멀티메서드와 계층 (Multimethods and Hierarchies)

Clojure는 각각의 새로운 상황마다 새 데이터 타입을 만드는 전통적인 객체 지향 접근을 배격하고, 대신 작은 타입 집합 위에 큰 함수 라이브러리를 구축하는 것을 선호해요. 그러나 Clojure는 유연하고 확장 가능한 시스템 아키텍처를 가능하게 하는 런타임 다형성의 가치를 충분히 인식해요. Clojure는 하나 이상의 인자에 대한 타입, 값, 속성(attribute), 메타데이터, 그리고 인자들 사이의 관계에 디스패치하는 것을 지원하는 멀티메서드 시스템을 통해 정교한 런타임 다형성을 지원해요.

Clojure 멀티메서드는 디스패치 함수 와 하나 이상의 메서드 의 결합이에요. 멀티메서드가 _defmulti_로 정의될 때 디스패치 함수를 제공해야 해요. 이 함수는 디스패치 값 을 만들기 위해 멀티메서드의 인자들에 적용돼요. 그러면 멀티메서드는 그 디스패치 값과 연관된, 또는 디스패치 값이 파생된 값을 가진 메서드를 찾으려 시도해요. 하나가 정의되어 있으면(defmethod으로), 그 메서드가 인자들과 함께 호출되고 그것이 멀티메서드 호출의 값이 돼요. 디스패치 값과 연관된 메서드가 없으면, 멀티메서드는 기본 디스패치 값(기본값은 :default)과 연관된 메서드를 찾고, 있으면 그것을 사용해요. 그렇지 않으면 호출은 오류예요.

멀티메서드 시스템은 이 API를 노출해요: defmulti는 새 멀티메서드를 만들고, defmethod는 디스패치 값과 연관된 멀티메서드의 새 메서드를 만들고 설치하며, remove-method는 디스패치 값과 연관된 메서드를 제거하고, prefer-method는 메서드들이 달리 모호할 때 메서드 사이에 순서를 만듭니다.

파생(derivation)은 Java 상속(클래스 값의 경우) 또는 Clojure의 애드혹(ad hoc) 계층 시스템 사용의 결합으로 결정돼요. 계층 시스템은 이름(심볼 또는 키워드)들 사이의 파생 관계와, 클래스와 이름 사이의 관계를 지원해요. derive 함수가 이런 관계를 만들고, isa? 함수가 그 존재를 검사해요. isa?instance?가 아니라는 점에 주의하세요.

+(derive child parent)+로 계층 관계를 정의할 수 있어요. child와 parent는 심볼 또는 키워드일 수 있고, 반드시 네임스페이스로 한정되어야 해요:

pass:[::] 리더 문법에 주목하세요. ::키워드는 네임스페이스를 해석해요.

::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는 아니에요, 클래스 자손은 열린 집합이므로):

(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

출처: Clojure 공식 문서 - Multimethods and Hierarchies

본문

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

위의 모든 예시는 멀티메서드 시스템이 사용하는 전역 계층을 사용하지만, 완전히 독립적인 계층도 make-hierarchy로 만들 수 있고, 위의 모든 함수는 선택적 계층을 첫 번째 인자로 받을 수 있어요.

이 단순한 시스템은 매우 강력해요. Clojure 멀티메서드와 전통적인 Java식 단일 디스패치의 관계를 이해하는 한 가지 방법은, 단일 디스패치가 첫 번째 인자에 대해 getClass를 호출하고 메서드들이 그 클래스들과 연관된, 일종의 Clojure 멀티메서드라는 점이에요. Clojure 멀티메서드는 클래스/타입에 고정되지 않으며, 인자의 어떤 속성에도, 여러 인자에 기반할 수도 있고, 인자 검증을 수행하고 오류-처리 메서드로 라우팅할 수도 있어요.

참고: 이 예시에서 키워드 :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

더 알아보기