2010-05-09 12 views
56

Mówi się, że gdy mamy klasę Point i wie jak wykonać point * 3 jak następuje:W języku Ruby, jak działa funkcja Coerce()?

class Point 
    def initialize(x,y) 
    @x, @y = x, y 
    end 

    def *(c) 
    Point.new(@x * c, @y * c) 
    end 
end 

point = Point.new(1,2) 
p point 
p point * 3 

wyjściowa:

#<Point:0x336094 @x=1, @y=2> 
#<Point:0x335fa4 @x=3, @y=6> 

ale wtedy

3 * point 

nie jest rozumiane:

Point nie może być zmuszany do Fixnum (TypeError)

Więc musimy dodatkowo określić metodę instancji coerce:

class Point 
    def coerce(something) 
    [self, something] 
    end 
end 

p 3 * point 

wyjściowa:

#<Point:0x3c45a88 @x=3, @y=6> 

Tak jest powiedział, że 3 * point jest taki sam jak 3.*(point). Oznacza to, że metoda instancji * przyjmuje argument point i wywołuje obiekt 3.

Teraz, ponieważ ta metoda * nie wie, jak pomnożyć punkt, więc

point.coerce(3) 

będzie nazwany, i wrócić tablicy:

[point, 3] 

a następnie * jest raz ponownie zastosowany, czy to prawda?

Teraz, to jest zrozumiałe, a teraz mamy nową Point obiekt, jako wykonywane metodą instancji * klasy Point.

Pytanie brzmi:

  1. Kto powołuje point.coerce(3)? Czy to Ruby automatycznie, czy jest to jakiś kod w metodzie * z Fixnum przez wychwycenie wyjątku? Czy to przez oświadczenie case, że jeśli nie zna jednego ze znanych typów, to zadzwoń coerce?

  2. Czy zawsze trzeba zwrócić tablicę 2 elementów? Czy to nie może być tablica? Czy może to być zestaw 3 elementów?

  3. I czy reguła, że ​​oryginalny operator (lub metoda) * zostanie wywołany na elemencie 0, z argumentem elementu 1? (Element 0 i element 1 to dwa elementy w tej tablicy zwrócone przez coerce.) Kto to robi? Czy robi to Ruby, czy jest to zrobione przez kod w Fixnum? Jeśli robi się to za pomocą kodu w Fixnum, to jest to "konwencja", którą wszyscy przestrzegają podczas przymusu?

    Więc może to być kod w * z Fixnum robi coś takiego:

    class Fixnum 
        def *(something) 
        if (something.is_a? ...) 
        else if ... # other type/class 
        else if ... # other type/class 
        else 
        # it is not a type/class I know 
         array = something.coerce(self) 
         return array[0].*(array[1]) # or just return array[0] * array[1] 
        end 
        end 
    end 
    
  4. Tak naprawdę trudno coś dodać do metody Fixnum „s instancji coerce? To już ma dużo kodu w nim i nie możemy po prostu dodać kilka wierszy w celu zwiększenia go (ale będziemy kiedykolwiek chcesz?)

  5. coerce w klasie Point jest dość ogólny i współpracuje z * lub +, ponieważ są przechodnie. A jeśli to nie jest przechodnia, takich jak definiujemy minus Fixnum być:

    point = Point.new(100,100) 
    point - 20 #=> (80,80) 
    20 - point #=> (-80,-80) 
    
+1

To jest doskonałe pytanie! Jestem bardzo szczęśliwy, że go znalazłem, ponieważ przeszkadzało mi to i aż do teraz nie sądziłem, że można go rozwiązać! – sandstrom

+0

Dobre pytanie. Dzięki za umieszczenie go. Dzięki temu zaoszczędzę wiele czasu na pomieszanie inżynierów. – VaidAbhishek

Odpowiedz

39

Krótka odpowiedź: sprawdzeniu how Matrix is doing it.

Chodzi o to, że coerce powraca [equivalent_something, equivalent_self], gdzie equivalent_something jest obiektem w zasadzie równoważne something jednak, że wie jak to zrobić operacje na klasy Point. W bibliotece Matrix konstruujemy obiekt Matrix::Scalar z dowolnego obiektu Numeric i ta klasa wie, jak wykonywać operacje na Matrix i Vector.

Aby rozwiązać swoje punkty:

  1. Tak, to jest Ruby bezpośrednio (sprawdzić połączenia do rb_num_coerce_bin in the source), chociaż własne typy powinien robić zbyt jeśli chcesz, aby Twój kod będzie rozszerzalny przez innych. Na przykład, jeśli Twój Point#* jest przekazywany jako argument, którego nie rozpoznajesz, sam byś zapytał ten argument do coerce do Point przez wywołanie arg.coerce(self).

  2. Tak, to musi być tablicą z 2 elementów, tak że b_equiv, a_equiv = a.coerce(b)

  3. Tak. Ruby robi dla wbudowanych typów, a należy też na własnych typów niestandardowych jeśli chcesz być rozszerzalny:

    def *(arg) 
        if (arg is not recognized) 
        self_equiv, arg_equiv = arg.coerce(self) 
        self_equiv * arg_equiv 
        end 
    end 
    
  4. Chodzi o to, że nie należy modyfikować Fixnum#*. Jeśli nie wie, co zrobić, na przykład, ponieważ argumentem jest Point, zapyta cię, dzwoniąc pod numer Point#coerce.

  5. Przechodnie (a właściwie przemienność) nie jest konieczna, ponieważ operator jest zawsze wywoływany we właściwej kolejności. To tylko połączenie z coerce, które tymczasowo przywraca odebrane i argumenty. Nie ma wbudowany mechanizm, który gwarantuje przemienności operatorów jak +, ==, etc ...

Jeśli ktoś może wymyślić zwięzły, precyzyjny i jasny opis poprawić oficjalnej dokumentacji, komentarza!

+0

hm, czy przechodzenie nie ma większego znaczenia? Na przykład zobacz http://stackoverflow.com/questions/2801241/inrub-how-how-to-implement-20-point-and-point20-using-coerce –

+0

Nie, przechodniość nie odgrywa żadnej roli a Ruby nie zakłada, że ​​'a - b' jest tym samym, co' - (b - a) 'lub coś podobnego, nawet' a + b == b + a'. Co sprawia, że ​​wierzysz, że się mylę? Czy sprawdziłeś źródło MRI? Dlaczego nie spróbować podążać za wskazanym przeze mnie kierunkiem? –

+0

Myślę, że OP oznaczał "symetryczny", a nie "przechodni". W każdym razie ** ja ** chcę wiedzieć, jak piszesz 'coerce' tak, że niesymetryczne operatory, takie jak' -', mogą być zaimplementowane tylko w jednym kierunku, przy jednoczesnym zachowaniu operatorów symetrycznych działających w obie strony. Innymi słowy, 'a + 3 == 3 + a' i' 3 + a - 3 == a' ale '3 - a' powoduje błąd. –

2

łapię się często pisania kodu wzdłuż tego wzoru w kontaktach z przemienności:

class Foo 
    def initiate(some_state) 
    #... 
    end 
    def /(n) 
    # code that handles Foo/n 
    end 

    def *(n) 
    # code that handles Foo * n 
    end 

    def coerce(n) 
     [ReverseFoo.new(some_state),n] 
    end 

end 

class ReverseFoo < Foo 
    def /(n) 
    # code that handles n/Foo 
    end 
    # * commutes, and can be inherited from Foo 
end