Clojure with Polymorphism: 멀티메서드(Multimethod) 사용하기

2022년 10월 20일

Clojure

# clojure# multimethod# polymorphism# defmulti# defmethod

📕 목차

개요

클로저에서는 다형성 (polymorphism)을 다루기 위한 다양한 방법을 제공한다. 이 글에서는 그 방법중 하나인 멀티메서드(Multimethod) 를 살펴보도록 하겠다.

  • 멀티메서드는 하나의 변수 (이름)에 key-value 형태의 methodTable을 만들어 연관테이블을 기반으로 다형성을 제공한다.
  • defmulti 매크로를 사용해서 dispatch function을 구현한다.
  • defmethod 매크로를 사용해서 methodTable에 함수를 추가한다.

defmulti

  • defmulti는 다형성을 위해 dispatch function를 구현하기 위한 clojure.lang.MultiFn 인스턴스를 생성해주는 매크로이다.
  • 내부적으로 ReentrantReadWriteLock 락을 사용하는데 나중에 알아보자. 🤔
  • :default 를 default dispatch value로 사용한다.
(defmacro defmulti
  "..."
  [mm-name & options]
  (let [...
        options     (if (map? (first options))
                      (next options)
                      options)
        dispatch-fn (first options)
        options     (next options)
        m           (if docstring
                      (assoc m :doc docstring)
                      m)
        ...
        mm-name (with-meta mm-name m)]
    (let [options   (apply hash-map options)
          default   (get options :default :default)
          hierarchy (get options :hierarchy #'global-hierarchy)]
      (check-valid-options options :default :hierarchy)
      `(let [v# (def ~mm-name)]
         (when-not (and (.hasRoot v#) (instance? clojure.lang.MultiFn (deref v#)))
           (def ~mm-name
                ;; clojure.lang.MultiFn 클래스의 인스턴스를 생성한다.
                (new clojure.lang.MultiFn ~(name mm-name) ~dispatch-fn ~default ~hierarchy)))))))

defmethod

  • defmulti와 항상 함께다니는 짝꿍이다.
  • defmethod는 multifn 인스턴스의 addMethod 함수를 호출하는 매크로이다.
(defmacro defmethod
  "Creates and installs a new method of multimethod associated with dispatch-value. "
  {:added "1.0"}
  [multifn dispatch-val & fn-tail]
  `(. ~(with-meta multifn {:tag 'clojure.lang.MultiFn}) addMethod ~dispatch-val (fn ~@fn-tail)))
  • multifn 인스턴스에 tag 메타정보와 함께 addMethod 함수를 호출한다.
    • 여기서 addMethod를 호출할 때 사용한 방법은 instance-expr 인지 Classname-symbol 인지 잘 모르겠다. 🤔
    • with-meta에 대해서 공부해보자.
    • 자세한 내용은 공식문서를 참고하자.

defmethod에서 호출되는 addMethod의 코드를 한번 살펴보자.

public MultiFn addMethod(Object dispatchVal, IFn method) {
	rw.writeLock().lock();
	try{
		methodTable = getMethodTable().assoc(dispatchVal, method);
		resetCache();
		return this;
	}
	finally {
		rw.writeLock().unlock();
	}
}
  • addMethod는 위와 같이 작성되어 있는데 dispatchVal를 키로 method를 값으로 해서 methodTable(PersistentHashMap)에 저장한다.
  • 인자로 받는 method는 IFn 타입인 것을 확인할 수 있다.

동작방식

위에서 멀티메서드를 위한 defmultidefmethod 의 코드를 보면서 각각의 동작에 대해서 알아 보았다. 이번에는 이 두개의 함수가 어떻게 서로 연관되어 동작하는지 알아보겠다.

실제로 정의된 multimethod를 호출하는 것은 두 개의 함수를 평가하는 것을 수반한다.

  1. dispatch function 함수를 실행해서 결과 값을 받는다.
  2. 위의 결과 값으로 methodTable에서 찾아서 해당 함수를 실행시킨다.
(defmulti coffee
  (fn [coffee]
    coffee))

(defmethod coffee :아메리카노
  [coffee]
  (str (name coffee) " 냠냠"))

(defmethod coffee :카페라떼
  [coffee]
  (str (name coffee) " 맛있당"))

(coffee :아메리카노)
=> 아메리카노 냠냠

(coffee :카페라떼)
=> 카페라떼 맛있당

위의 예시에서 우리는 coffee라는 심볼의 멀티메서드를 정의했다. 그리고 :아메리카노 , :카페라떼 두 개의 메서드를 추가했다.

여기서 (coffee :아메리카노) 를 실행할 때 다음과 같은 과정이 일어난다.

  1. coffee는 MultiFn 클래스의 인스턴스이고 해당 인스턴스의 invoke 함수가 호출된다.

    1. invoke 함수는 내부적으로 getFn 함수를 호출한다.
    public Object invoke(Object arg1) {
      // defmulti에서 작성한 dispatchFn을 호출한다.
      // 그 결과값을 getFn 함수에 전달한다.
    	return getFn(dispatchFn.invoke(arg1)).invoke(Util.ret1(arg1,arg1=null));
    }
  2. getFn 함수는 내부적으로 getMethod 함수를 실행해서 targetFn을 반환한다.

    • 반환되는 targetFn의 타입은 IFn 인터페이스이다.
    private IFn getFn(Object dispatchVal) {
    	IFn targetFn = **getMethod(dispatchVal);**
    	if(targetFn == null)
    		throw new IllegalArgumentException(String.format("No method in multimethod '%s' for dispatch value: %s",
    		                                                 name, dispatchVal));
    	return targetFn;
    }
  • 위의 getMethod 의 동작을 살펴보자.

    public IFn getMethod(Object dispatchVal) {
    	if(cachedHierarchy != hierarchy.deref())
    		resetCache();
    	IFn targetFn = (IFn) methodCache.valAt(dispatchVal);
    	if(targetFn != null)
    		return targetFn;
    	return **findAndCacheBestMethod(dispatchVal);**
    }
    • methodCache로부터 dispatchVal을 사용해서 targetFn을 조회하고 찾지 못하면 findAndCacheBestMethod 메서드를 호출한다.
  • findAndCacheBestMethod 메서드는 다음과 같다.

    private IFn findAndCacheBestMethod(Object dispatchVal) {
    	rw.readLock().lock();
    	Object bestValue;
    	IPersistentMap mt = methodTable;
    	IPersistentMap pt = preferTable;
    	Object ch = cachedHierarchy;
    	try {
        Map.Entry bestEntry = null;
    
        // getMethodTable로부터 엔트리를 가져온다.
        for(Object o : getMethodTable()) {
    			Map.Entry e = (Map.Entry) o;
    			if(isA(ch, dispatchVal, e.getKey())) {
    				if(bestEntry == null || dominates(ch, e.getKey(), bestEntry.getKey()))
    					bestEntry = e;
    				if(!dominates(ch, bestEntry.getKey(), e.getKey()))
    					throw new IllegalArgumentException(
    							String.format(
    									"Multiple methods in multimethod '%s' match dispatch value: %s -> %s and %s, and neither is preferred",
    									name, dispatchVal, e.getKey(), bestEntry.getKey()));
    				}
    			}
    		if(bestEntry == null) {
    			bestValue = methodTable.valAt(defaultDispatchVal);
          if(bestValue == null) return null;
    		}
        else bestValue = bestEntry.getValue();
    	} finally {
            rw.readLock().unlock();
    	}
    
    	//ensure basis has stayed stable throughout, else redo
    	rw.writeLock().lock();
    	try {
    		if( mt == methodTable &&
    		    pt == preferTable &&
    		    ch == cachedHierarchy &&
    			cachedHierarchy == hierarchy.deref()) {
    			//place in cache
          // 여기서 캐시를 해준다.
    			methodCache = methodCache.assoc(dispatchVal, bestValue);
    			**return (IFn) bestValue;**
    		}
    		else {
    			resetCache();
    			return findAndCacheBestMethod(dispatchVal);
    		}
    	} finally {
    		    rw.writeLock().unlock();
    		}
    }
    • 결국 dispatch 값에 의해 선택된 메서드를 찾아서 반환한다.
  1. 그리고 methodTable에서 찾은 메서드를 호출한다.
    1. getFn(dispatchFn.invoke(arg1)).invoke(Util.ret1(arg1,arg1=null));
    2. getFn으로 찾아온 것은 :아메리카노에 해당하는 메서드이고 (str (name coffee) " 냠냠") 이 실행되어 “아메리카노 냠냠” 이 반환한다.

요약

  • dispatch function 평가 후 methodTable에서 dispatch value와 일치하는 메서드 찾은 후 → 찾은 메서드 실행하는 2단계로 동작한다.
  • 멀티메서드는 내부적으로 한번 찾고나면 methodCache에 캐싱해 O(1)로 메서드를 찾아서 실행할 수 있다.

참고자료

profile

박민기

단순하게 살아라. 현대인은 쓸데없는 절차와 일 때문에 얼마나 복잡한 삶을 살아가는가? - 이드리스 샤흐

© 2023, 미나리와 함께 만들었음