Ruby double dispatch is flawed?

타입이 없이는 우아한 double dispatch는 불가능한 걸까요….

다음은 루비에서 정수와 새로 정의한 Roman 숫자간의 덧셈을 간략히 구현한 것입니다.

class Roman
    def initialize(val)
        @value = val
    end

    def coerce(other)
        if Integer === other
            [other, @value]
        else
            [Float(other), Float(@value)]
        end
    end
end

class IntSim
    def initialize(val)
        @value = val
    end

    def +(other)
        v1, v2 = other.coerce(@value)
        v1 + v2
    end
end


int_3 = IntSim.new(3)
vi = Roman.new(6)

# Shows you how to compute 3 + vi.
puts int_3 + vi

루비에서는 2개 숫자간의 덧셈시 int+int는 int, float+int는 float, float+float은 float같은 타입 conversion을 위해 coerce를 씁니다. 예를들어, 앞서의 예처럼 3+vi 를 호출하게 되면 3이 vi.coerce(3)을 호출합니다. 그러면 vi의 클래스인 Roman 쪽에서는 넘어온 3과 자기 자신의 타입을 바탕으로 하여 두개 숫자를 conversion합니다. 그러면 3의 operator+는 이 변환된 타입을 가지고 최종 결과를 계산합니다. (그래서 double dispatch라고 불립니다. 2번 호출하니까.)

문제는 이 예제를 본 Programming Ruby책이 잘못된 건지, 루비의 한계인건지 사실상 이와같은 double dispatch가 아무런 의미도 없다는 것입니다. double dispatch는 이러한 예와같이 두개의 hierarchy가 존재할 때, 양쪽 hierarchy에 따라서 적절히 메소드를 호출하기 위함입니다. 그리고 그 구현은 RTTI(i.e., reflection)과 if-else 사다리를 쓰는 방법, A의 a라는 메소드를 부르면 a는 B의 b를 부르는 방법이 있습니다. 물론 후자가 더 나은 방법이죠.

두번째 방법에서는, A의 a를 부르면, a라는 메소드에서는 자신의 type이 결정납니다. 따라서 B의 b를 부를때는 자신의 타입정보를 b에게 넘깁니다. b는 호출되자마자 B hierarhcy 측에서의 자신의 타입을 알게되면, 결국 모든 타입정보를 알게 된 셈이니까 양쪽의 타입에 근간한 dispatch가 가능한 것이죠.

하지만 앞서의 예에서는 IntSim에서 Roman 을 부르지만, Roman쪽에 “나는 int거든”이란 정보를 넘기지 않으니 다시 Roman에서는 if-else 사다리를 타고 있군요.. 이건 말이 안되는거 아닌지.

귀찮아서 대강 적절히 C++이라면 어떨까를 짜봤습니다.

class Roman
{
private:
    int val;

public:
    Roman(int val):val(val) { }
    int operator+(const int &i) const
    {
        return val + i;
    }

    float operator+(const float &f) const
    {
        return val + f;
    }
};


class Numeric { };

class IntSim: public Numeric
{
private:
    int val;

public:
    IntSim(int val):val(val) { }
    int operator+(const Roman &o) const
    {
        return o.operator+(val);
    }
};

class FloatSim: public Numeric
{
private:
    float val;

public:
    FloatSim(float val):val(val) { }
    float operator+(const Roman &o) const
    {
        return o.operator+(val);
    }
};

제가 생각할 땐 Programming Ruby 책이 잘못된 것 같군요.. 적어도 루비같은 duck typing언어에서는 아예 double dispatch를 포기하던가, 아니면 확실히 IntSim에서 Roman으로 갈때 Int측의 정보를 줄 수 있게 보조 장치를 만들어야 한다는 생각이 듭니다. 사실 Double Dispatch의 가장 좋은 솔루션은 Scott Myers 왈, “디자인을 새로해라” 잖아요.

Similar Posts:

Comments 9

  1. 무책 wrote:

    실행 효율이란 면에서는 passion님의 말씀이 일리가 있지만 루비란 언어가 어차피 C++에 비해 실행 효율에서 장점을 보인다기 보다는 다이나미즘에서 앞선다고 한다면 저는 조금 다른 의견을 갖습니다.

    가령 PickAxe p.563에서의 예처럼 Perl의 스트링-숫자 간 자유로운 연산을 루비에서 지원하겠다고 한다면 String 클래스를 오픈해서 String#coerce 메소드만 정의해 주면 Integer, Float, Bignum 뿐만아니라 passion 님의 IntSim 인스턴스와도 덧셈이 되지요.

    반면 보여주신 C++ 예에서와 같이 오퍼레이터 오버로딩을 이용하려면 String 클래스를 오픈하여 (C++에서는 이것 조차 불가능 하지만 가능하다고 가정하고) PickAxe p.564에 나오는 54가지 숫자 관련 연산들에 대해 각각 Integer, Float, Bignum 타입의 오버로딩을 해줘야 합니다. 위에서는 + 연산만 지원하셨지만 Roman을 만드는 사람 입장에서는 나중에 어떤 연산으로 불릴지 모르는 것이니까요.

    Roman, 스트링 뿐만 아니라 앞으로 누가 HangulNum (“일”, “이”, …), HanjaNum (“一”, “二”, …) 등등을 만들지 모른다고 한다면 새로 만들어지는 클래스가 알아서 coerce만 구현해주면 이전의 숫자 관련 클래스와 연산이 되는 것이 더 아름답지 않나요?

    Posted 24 Feb 2006 at 11:25 pm
  2. MKSeo wrote:

    음.. 제가 예를 잘 못 들었는데요.. 요즘 너무 바쁘고 피곤하여;; 루비 블로그에 대해 신경쓰는 것도 그래서 잠시 쉬고 있습니다. 그러니까… 제가 코드를 잘못짰네요;;

    다시 설명을 시도해보겠습니다;; “A의 a를 부르면 a는 B의 b를 불러서 결과를 받아온다”라는 것이 double dispatch 의 핵심입니다. 그리고 그런일을 하는 이유는 A와 B의 타입을 알고 싶기 때문입니다. int+float냐, float+float 냐 하는 것들을요. 이것이 필요한 이유는 양쪽다 숫자인건 아는데 양쪽이 각각 어떤 타입인지에따라 연산을 다르게 하고 싶기 때문입니다.

    예를들어 int + float 라고 하겠습니다. 그리고 이들 둘은 number의 자식 클래스라고 하겠습니다. 그러면 최초의 연산은 number + number입니다. 그 뒤, 좌측의 number의 + 메소드가 호출되고, 이 때 폴리모피즘에 의해 자동적으로 int + 가 찾아집니다. 그러면 결과적으로, int+(number) 가 호출되게 되죠. 하지만 아직 우측은 number인 상태입니다. 따라서, int(number) 내에서는 number.operator+(int)를 호출합니다. 이때, number.operator+ 호출시 이 number가 실제로는 float의 인스턴스임을 폴리모피즘을 통해 알게되므로 최종적으로 float.operator+(int) 가 호출되어 int +float의 연산이 완성됩니다. 이 아이디어를 양쪽언어로 좀 명확하게 표현해야하는데, 마음만 앞서 제대로 하지 못했네요….

    일단 이렇게 아이디어를 정리해 놓고 보면, 루비의 한계는 너무 명백합니다. 노파심이지만, 루비가 나쁘다는 말은 아닙니다.. 사실 ruby나 c++이나 둘다 double dispatch 라는 개념이 없으므로 – CLOS에는 있다고합니다 – 이렇게 메소드를 두번 부르는 트릭을 사용하지만 어쨌든 한계가 있고, 결국 둘다 표현력은 떨어지는 셈이죠..

    아무튼, pickaxe에 나온 코드의 문제는 뭐냐.. 하면 number + number 호출시 최초로 number.operator+(number) 가 호출될때 int.operator+(number)로 resolve는 됩니다. 한데, 여기서 int 를 알아낸다음에 또다시 number.operator+(int)를 해주어야하지만 루비에는 타입이 없으므로 그냥 또다시 number.operator+(number)를 하는 꼴이 됩니다. 이로인해 coerce메소드 안에서는 if Integer === other 와 같은 삽질이 발생하는 것이죠. 사실은 최초로 int.operator+ 까지 찾아간 순간 좌변이 int 인걸 알았지만 그 정보를 coerce에 넘겨주지 않으므로 coerce에서는 좌변이 뭐였지? 라고 if-else 사다리를 타고 있게 됩니다.

    이 상황을 C++에 비교해보면 전혀 다른데요.. C++에서는 좌변을 알고나면 우변의 operator+를 호출할때 좌변이 무엇인지를 확실히 넘겨줍니다. 그렇기 때문에 if-else 사다리가 제거되고 오버로딩으로 해결이 가능한 것입니다.

    if-else 사다리가 OOP의 가장 큰 특징(장점이라고는 단정짓지 못합니다..)인 폴리모피즘이 결여된 코드에서 나는 bad smell 임을 감안한다면 잘못된 코드입니다. 만약 루비 소스가 원래 그렇다면 어처구니 없는데요, 저로선.

    하지만 물론 종합해보면 결국은 54가지 타입에 대해서 if-else 사다리를 타게 할거냐 아니면 오버로딩 할거냐의 얘깁니다. Effective C++의 저자 Scott Myers 는 “그러지 말구 우리 테이블 하나에 변환규칙 넣어놓고 그걸 갖다 쓰자”라는것을 best practice로 제안합니다만, 그 과정에서 보여주는 코드는 그야말로 악몽입니다. (개인적으로 테이블에 넣고 룩업하는 과정에서 발생하는 문법적인 복잡함은, C++이 얼마나 호러블한 언어인가를 역설적으로 증명해주고 있다고 생각합니다..)

    개인적으로 if-else 사다리나 오버로딩 삽질이나 결국은 오십몇가지 삽질이긴 합니다만, 그래도 오버로딩으로 해결 가능한쪽에 한표를 던지고 싶습니다.. 후에 comp.lang.ruby 에 포스팅을 해보겠습니다..

    Posted 25 Feb 2006 at 12:47 am
  3. 공성식 wrote:

    안녕하세요?

    아주 재밌는 글입니다.

    제 생각에 double dispatch와 C++의 operator overloading을 함께 논하는 것은 적절하지 않아보입니다.
    C++의 operator overloading은 컴파일시에 결정되는 것인데 반해, double dispatch는 런타임시에 타입을 결정하여 메소드를 호출하는 것이니까요.
    예로 드신 coercing의 방법이 적절한지에 관해서는 좀더 생각해 봐야겠습니다만…

    http://groups.google.com/group/comp.lang.ruby/msg/7afea029b6519f75?hl=en& 에 가보시면 matz가 예로 든 double dispatch의 예가 있는데 물론 Lisp의 경우를 simulation한 것이라고 봐야겠지만, 이 정도라면 꽤 괜찮지 않을까요?

    계속 생각해 보면서 얘기 나누기로 하죠^^

    Posted 25 Feb 2006 at 1:59 am
  4. 무책 wrote:

    자세하게 다시 설명해 주셔서 감사합니다. ^^ 결국 passion님의 요점은 왜 루비가 polymorphism의 여러 형태 중 하나인 multi-method (parameter 타입에 따른 dispatch)를 지원하지 않느냐로 해석되네요.

    http://groups.google.com/group/comp.lang.ruby/browse_frm/thread/e9e19646e2220667?tvc=1 여기 보시면 thread 중후반부에 이와 연관된 논쟁이 있습니다. 논쟁 자체와 논쟁 중에 나오는 링크 중 읽어볼만한 얘기들이 많아서 제 답변에 대신 하겠습니다.

    한가지만 사족을 달면,
    > 개인적으로 if-else 사다리나 오버로딩 삽질이나 결국은 오십몇가지 삽질이긴 합니다만 […]
    #coerce 내의 if-else는 Integer, Bignum, Float 딱 세 개만 있으면 되는 반면에 overloading은 일반적으로는 Numeric 계열에서 지원되는 모든 연산자 (+,-,*,/,div, …)에 대해 다 해줘야 하죠 (루비에서는 대략 쉰 개 남짓). 그 차이를 말하려 했던 겁니다.

    자꾸 덧글 달다보니 제가 꼭 루비 종교의 열성분자 같아요. ^^; 전혀 그렇지 않고 그냥 뒤늦게 루비라는 언어의 매력에 빠져보고 있을 뿐입니다. 배우다 보니 단점이나 문제점도 눈에 띠지만 그래도 단점보다는 장점이 훨씬 많은 언어인 것 같습니다. Passion님도 저와 비슷한 생각이시지 않을까 생각합니다.

    Passion님 사이트 종종와서 많이 배우고 가곤 합니다. 앞으로도 좋은 글 많이 올려주세요.

    Posted 25 Feb 2006 at 3:56 am
  5. MKSeo wrote:

    감사합니다. ^^ 두분이 링크해주신글도 읽어볼께요..

    “C++의 operator overloading은 컴파일시에 결정되는 것인데 반해, double dispatch는 런타임시에 타입을 결정하여 메소드를 호출하는 것이니까요.
    예로 드신 coercing의 방법이 적절한지에 관해서는 좀더 생각해 봐야겠습니다만…”

    동감합니다.. 오늘아침에 제가 한 생각이란 같아요 ^^

    “#coerce 내의 if-else는 Integer, Bignum, Float 딱 세 개만 있으면 되는 반면에 overloading은 일반적으로는 Numeric 계열에서 지원되는 모든 연산자 (+,-,*,/,div, …)에 대해 다 해줘야 하죠 (루비에서는 대략 쉰 개 남짓). 그 차이를 말하려 했던 겁니다.”

    이건 사실입니다…. 그래서 제가 C++쪽을 잘못짰다고 말씀드린건데.. 똑같이 만들면서 제가 생각한 문제만 부각시키려면 c++도 루비처럼 coerce를 쓰게 짰어야 합니다;;

    그리고 사실은 C++에서 이미 닫혀있는 연산자들에 대해서만 더블 디스패치가 발생하는건 아니고, 여러가지 상황에서 양쪽의 계층구조를 보면서 dispatch하는게 필요하는데 쓰일 수 있는 best practice 가 무엇일까가 궁금했어요;;;; 링크하신글 읽어볼께요..

    Posted 25 Feb 2006 at 9:54 am
  6. 공성식 wrote:

    MKSeo님,

    한가지 제안을 드리고자 합니다.
    지난 번에 말씀하신 것처럼 루비 메타 블로그를 만들고 계시는 걸로 알고 있습니다.
    참 좋은 취지라고 생각합니다.

    이번 글과 커멘트를 보면 루비 사용자들(이라고 해봤자 여태껏 세 명이지만…)이 뉴스그룹을 잘 사용하고 있는 것 같습니다.
    그래서 아예 구글에서 그룹을 만들어서 이용하면 어떨까 합니다.
    com.lang.ruby는 영어로만 진행이 되고 워낙 고수들이 많아 한국의 초보자들이 접하기엔 좀 멀게 느껴집니다.
    한국 사람들은 뉴스그룹을 별로 안 좋아한다는 점이 좀 걱정입니다만, 이번에 MKSeo님과 무책님을 보면서 꼭 그렇지만은 않다는 걸 느꼈습니다.

    물론 MKSeo님의 메타 블로그는 그 나름대로 의미가 있겠구요.
    우리만의 뉴스그룹을 만드는 것은 또 다른 잇점이 있을 것 같아요.

    어떻게 생각하시는지요?

    공성식

    Posted 25 Feb 2006 at 10:15 am
  7. 무책 wrote:

    http://mephle.org/StrongTyping/

    이 RAA 모듈을 써보는 것은 어떨까요?

    class Roman
    require ‘strongtyping’
    include StrongTyping

    def coerce(other)
    overload(other, Integer) { |i| return [other, @value] }
    overload(other, Float) { |f| return [other, Float(@value)] }
    end
    end

    뭐 기본적으론 case..when의 다른 모습이긴 하지만 그래도 readability는 높아지는 것 같네요. Multimethod dispatch를 inherently 지원하지 않는 Ruby, Python, Perl 등의 근본적인 한계인 것 같습니다.

    Posted 25 Feb 2006 at 3:51 pm
  8. MKSeo wrote:

    헉. 정말 길게 두분께 답글을 썼는데 실수로 back버튼 눌렀다가 날라가버렸습니다;;;; 힘이 빠져서 다음에 답글을 달지요ㅠㅠ

    Posted 25 Feb 2006 at 9:16 pm
  9. MKSeo wrote:

    @무책: “Multimethod dispatch를 inherently 지원하지 않는 Ruby, Python, Perl 등의 근본적인 한계인 것 같습니다.” -> 동감입니다. 하지만 타입이 지원되지 않기에 이 부분에선 Java, C++ 보다 더 못하다고 생각됩니다. ‘strongtyping’ 모듈의 경우, 그런 모듈의 존재 자체가 언어의 한계를 정당화 하지는 못한다고 생각되어요.. 제가 좀 언어를 배울 때 그 언어의 표준 라이브러리에 집착하는 측면이 있어서요.. (lazy.rb 는 예외입니다. ㅎㅎ 사실 쓰레드 라이브러리가 너무 빈약해 보여서 만들어 볼만한 것으로 생각한것 두가지가 thread pool과 바로 그 lazy 였으므로..)

    @공성식: 음.. 일단 별도의 그룹을 만드는 것은 form.rubykr.org 가 이미 있고, 이외에도 별도의 포럼이 국내에 있으나 잘 운영되지 못한다는 한계를 극복해야합니다. 개인적인 생각으로는 포럼의 구성도 매우매우 중요하다고 생각하는데요, 메뉴가 적고 초기화면에 최신의 글이 나타나는 등 최소한 javastudy.co.kr 이나 kldp.org 정도의 외양이 나와야한다고 생각합니다. 만약 그런게 아니라 별도의 구글 그룹스를 원하시는 것이라면 han.comp.lang.c++ 이 매우 빈약하게 운영되고 있음을 또 주목할 필요가 있습니다. 루비의 저변이 더 미약하므로 han.comp.lang.c++ 을 넘는 그 무언가가 필요합니다. 강력한 멘터가 자꾸 포스팅을 한다던가 하는 것이 필요할 거 같아요. 루비 메타 블로그의 경우 일단 저라도 자꾸 포스팅하려고 노력하고 있습니다. 역시 별도의 그룹이 만들어진다면 그런 그룹을 유지하기 위한 개인의 노력이 많이 필요할거라 생각됩니다…. 또 분명히 forum.rubykr.org 가 있는데 그것을 개선하지 않느냐는 식의 이야기도 나올 수 있는데 – 저로선 “so what?” 이라고 반응하게 되는 이야기지만 – 그에 대한 정당화(차별점, 더 낫게 운영가능하다는 확실한 대안)이 필요하게 될 수도 있습니다. 그래서 무척 걱정되요. 만약 han.comp.lang.ruby 가 생기는 거라면 그것에 대해서는 찬성입니다. 어쨌든 있어야 한다고 생각하고요. 하지만, 뉴스그룹을 어떻게 만드는지는 모르겠네요;;

    Posted 26 Feb 2006 at 11:31 pm

Post a Comment

Your email is never published nor shared.