poniedziałek, 21 kwietnia 2014

Udawane Multiple Dispatch poprzez takie niby niewidzialne wizytatory

Dwie hierarchie

Z nauką nowych rzeczy jest trochę jak z oglądaniem reklam - nagle dowiadujesz się, że masz nowe potrzeby i to co było do tej pory przestaje wystarczać. A Coż może być takie fajne?. Gdy błądzący programista przedziera się przez materiały o obiektówce to zazwyczaj w sąsiedztwie słowa "Polimorfizm" znajduje się jakiś przykład gdzie kot i pies dziedziczą z klasy Animal czy coś w ten deseń.

Okazuje się, że te przykłady przedstawiają coś co nazywa się "single dispatch" i to coś ma ograniczenia, których nie ma bogatszy mechanizm zwany Double Dispatch lub Multi Dispatch. Może to dla większosci oczywiste ale mnie o tym w szkole nie uczyli.

A jeśli ktoś tak jak ja został niedopieszczony przez system edukacji to kod jak zwykle najlepiej pokaże o co chodzi. ( a jeśli ktoś jednak woli wikipedię to :

)

kod

Użyjemy dwóch hierarchii klas.
Pierwsza jest ludzka i zawiera

  • Klasę Human - nie chodzi humanistę tylko ogólnie o człowieka
  • Klasa Hipster dziedziczy z Human - Hipster to ktoś kto deklaruje swoją niezależność od "mainstreamu" głównie poprzez używanie mainstreamowych aplikacji społecznościowych
  • Klasa AntyHipster dziedziczy z Human - Antyhipster to ktoś kto jest takim hipsterem, że aż nie używa fejsbuka - wspomni o tym w przeciągu pierwszych 20 sekund rozmowy

Druga hierarchia to martwa natura i zawiera :

  • Klasę Drink - czyli napój
  • Klasa Vodka dziedziczy z Drink - czyli wóda
  • Klasa OrangeJuice dziedziczy z Drink - Litr soku pomarańczowego zawiera więcej cukru niż litr Coli --> ile cukru w cukrze

 

abstract class Human {
  def consume(drink: Drink):String
}
class Hipster extends Human{
  def consume(drink: Drink) = s"hipster consuming drink : $drink"
  def consume(drink: Vodka) = s"hipster consuming vodka : $drink"
  def consume(drink: OrangeJuice) = s"hipstewr consuming Orange : $drink"
}

class AntyHipster extends Human {
  def consume(drink: Drink) = s"antyhipster consuming drink : $drink"
  def consume(drink: Vodka) = s"antyhipster consuming vodka : $drink"
  def consume(drink: OrangeJuice) = s"antyhipster consuming Orange : $drink"
}


class Drink
class OrangeJuice extends Drink
class Vodka extends Drink

//deklaracje

val hipster = new Hipster
val antyhispter = new AntyHipster
val juice = new OrangeJuice
val vodka = new Vodka

Przypadek jeden Zero dispatch - mała elastyczność

Na samym początku przykład prosty jak cep. Metoda, która przyjmuje konkretną implementacje Human (Hipster) i konkretny napój (Vodka). Gdybyśmy chcieli obsłużyć wszystkie przypadki (Human,Drink) wtedy potrzebujemy 4 różne metody (bardzo niedobrze, bardzo bardzo źle)


def zeroDispatch(hipster:Hipster,vodka:Vodka)=hipster.consume(vodka)

zeroDispatch(hipster, vodka)              //> res0: String = hipster consuming vodka : Vodka

Trochę lepiej - Single dispatch

Co chcę zrobić : przekazać obiekt typu człowiek i ma się wywołać metoda z odpowiedniej implementacji. Czyli poniżej metoda spodziewa się po prostu Human i można tam przekazać i Hipstera i AntyHisptera - czyli klasyczny "Javowy Polimorfizm". Aby obsłużyć wszystkie kombinacje (Human,Drink) potrzebowałbym dwóch metod - po jednej na każdy napój.

def singleDispatch(human:Human,vodka:Vodka)=human.consume(vodka)
val hipster : Human= new Hipster
val antyhispter : Human = new AntyHipster

singleDispatch(hipster, vodka)            //> res0: String = hipster consuming drink : Vodka
singleDispatch(antyhispter, vodka)        //> res1: String = antyhipster consuming drink : Vodka

Multiple Dispatch - pierwsze podejscie z failem

I teraz ważne co chcę zrobić : chcę mieć jedną metodę która ani nie wie co to za Human ani co to za Drink i jak np. przekażę Hipstera i Vodkę to się automatycznie wywoła metoda z Hipstera, która przyjmuje Vodke.
Proba 3..2...1..

val hipster : Human= new Hipster
val antyhispter : Human = new AntyHipster
val vodka : Drink= new Vodka

def notWorkingMultipleDispatch(human:Human,drink:Drink)=human.consume(drink)
notWorkingMultipleDispatch(hipster, vodka)//> res0: String = hipster consuming drink : Vodka
notWorkingMultipleDispatch(antyhispter, vodka)//> res1: String = antyhipster consuming drink : Vodka
                                                  

No i Ch** bo jakby ktoś nie zauważył wywołana jest generyczna metoda "consume(Drink)". W Javie można zrobić takie nieładne "instanceof" albo Vizytatora. W Scali można zrobić Pattern Matching albo Vizytatora.

Ewentualnie można zrobić takiego "niewidzialnego" Vizytatora co zrobimy dla ćwiczeń teraz bo nie mam innego pomysłu na zakończenie tego artykułu

Rzeźbimy

abstract class Human {
  def consume[A](drink: A)(implicit dispatcher: DrinkMultiDispatch[A]): String
}
class Hipster extends Human{
 override def consume[A](drink: A)(implicit dispatcher: DrinkMultiDispatch[A]): String="hipster "+dispatcher.dispatch(drink)
}

trait DrinkMultiDispatch[A] {
  def dispatch(drink: A): String
}

Idea polega na dołożeniu "czegoś" co będzie decydowało o tym jak obsłużyć konkretny napój. Ponieważ mamy implicity to będzie pojawiało się znikąd i user nawet nie zauważy.

implicit val udawaneMultipleDispatch = new DrinkMultiDispatch[Drink]{
  def dispatch(drink: Drink) = drink match {
   case _:Vodka => "drinking vodka"
   case _:OrangeJuice => "drinking juice"
  }
 }

To jest takie oszukiwanie przez pattern matching. Próbowałem dopasować typ Dispatchera poprzez Generyk ale zawsze dostawałem "ambiguous implicit values" jak siedziały obok siebie [Vodka] i [OrangeJuice].

def workingMultipleDispatch(human:Human,drink:Drink)=human.consume(drink)
workingMultipleDispatch(hipster, vodka)   //> res3: String = hipster drinking vodka
workingMultipleDispatch(hipster, juice)   //> res4: String = hipster drinking juice
workingMultipleDispatch(antyhipster, vodka) //> res5: String = antyhipster drinking vodka
workingMultipleDispatch(antyhipster, juice) //> res6: String = antyhipster drinking juice

No i od bidy można powiedzieć, że to rozwiązanie uwalnia klasy "domenowe" od instanceof. Jeśli jest jakieś lepsze rozwiązanie to nie umiem go znaleźć. Zawsze trochę zabawy i edukacji.

A w ogóle skąd ten multi dispatch? Przeczytałem, że Clojure ma to z paczki ale podobno wolne strasznie.

*               *              *

Brak komentarzy:

Prześlij komentarz