niedziela, 10 sierpnia 2014

Funkcje częściowe i uczulenie na instanceof

To będzie wpis kontynuujący zachętę do stosowania do szukania nowych rozwiązań "Game of life" na coderetreat. Będzie także o wyciąganiu pochopnych wniosków.

Kompozycja funkcji częściowych

By nie tracić czasu i się nie potwrzać --->Tutaj<--- jest link do opisu co to jest coderetreat.

A co do dzisiejszego mięska. Funkcje częściowe w Scali i (chyba nie tylko) zwracają jakiś sensowny rezultat tylko dla określonych argumentów ale zamiast samemu pisać ify sprawdzające parametry wywołania - jest to naturą samej funkcji częściowej iż taki "if" istnieje niejako w niej samej.

I tak poniżej mamy dwie funkcje częściowe, jedna dla żywej komórki a druga zaś dla martwej. Magiczny patent dzieje się w linii 69 gdzie obydwie funkcje łączymy w jedną w taki sposób, że jak jedna nie może obsłużyć argumentu to spróbuje zrobić to druga. Jak ktoś lubi wzorce to to jest taki "chain of responsibility" (w wersji dla dresa : "złoty łańcuch zobowiązań")

//tutaj mamy taki mocno naciągany closure ale na potrzeby przykładu styka
    val liveNeighbours=4   
 
 def liveCellEvolution:PartialFunction[Cell,Cell]={
  case cell:LiveCell if (liveNeighbours==2 || liveNeighbours==3) =>LiveCell()
  case cell:LiveCell => DeadCell()
 }                                         

 def deadCellEvolution:PartialFunction[Cell,Cell]={
  case cell:DeadCell if (liveNeighbours==3) => LiveCell()
  case cell:DeadCell => DeadCell()
 }    

//linia numer 69
    val cellEvolution=liveCellEvolution orElse deadCellEvolution

Jeśli będziemy chcieli coś zmienić w logice ewolucji komórek wystarczy zrobić ooo tak :


val newCellEvolution=newLiveCellEvolution orElse cellEvolution
//
Czy coś tam w ten deseń.

I gdzieś tam hen hen jest ogólna niezmienna logika ewolucji :

def evolve(evolution:Cell=>Cell){
    cells.map(evolution)
 } 
Znowu jak ktoś bardzo chce to może powiedzieć, że jest zastosowany wzorzec strategia (I mamy kolejny wzorzec przy wykorzystaniu tylko jednego mechanizmu funkcji)

Płachta na byka

Działa powyższy mechanizm nawet fajnie i ciekawie ale jest i druga strona medalu. Powyżej zastosowałem taki skrócony zapis funkcji częściowej - pełna wersja wyglądałaby tak :


val liveCellEvolution=new PartialFunction[Cell,Cell]{
  def apply(c:Cell)=if(liveNeighbours==2 || liveNeighbours==3) LiveCell() else DeadCell()
  def isDefinedAt(c:Cell)=c.isInstanceOf[LiveCell]
 }  

Ahaaaaa i oto mamy profanację jego dostojności OOP poprzez bluźnierstwo w postaci InstanceOf. w sensie, że moim zdaniem akurat tutaj nie ma w tym nic złego ale jak rok temu jeszcze w Javie pokazałem podobny przykład to poleciały pomidory i na widły mnie chcieli nabić·

Skąd się bierze taka wrogość do "instanceOf". Wydaje mi się, że łatwo to wytłumaczyć bo sam to miałem.

Rekonstrukcja zdarzeń

Wszystko zaczyna się na uczelni gdzie skutkiem kombinacji i przypadków można przejść do 3 czy 4 roku (a nawet skończyć studia) pisząc cały czas programy w main. Istnienie takiej konstrukcji jak Interfejs pozostaje zagadką gdyż studentowi bez odpowiedniego doświadczenia trudno zrozumieć po co jest "klasa", która nie ma metod i w zasadzie nic nie robi?

Potem może być etap 2 kiedy to odkrywa się "polimorfizm w Javie" i odchodzi od metod z setkami ifów w kierunku ukrywania implementacji za interfejsem - o na przykład tak(po staremu) :

for (Invoice invoice : invoices) {
   invoice.process(cosTam)
}
I pewnego dnia wzrok nasz może zaatakować taka konstrukcja :
if(invoice instanceof TakiInvoice){
   //...
  }else if(invoice instanceof SrakiInvoice){
  //.... 
  }
No i wiadomo, ze w pierwszym przypadku jest dobrze bo nie trzeba zmieniać kodu jak dodamy nowe typy a w drugim już jest w tym temacie gorzej bo kod modyfikować by trzeba.

Tutaj można wyciągnąć pochopny wniosek, że istnienie "instanceof" oznacza brak polimorfizmu = a to tak nie do końca bo być może...

Szukamy nie tu gdzie trzeba

Zazwyczaj na coderetreat w rozwiązaniu z nutką obiektowości dochodzi się do rozwiązania gdzie mamy dwie komórki : LiveCell i DeadCell rozszerzajace jakiś wspólny interfejs czy klasę abstrakcyjną. Czyli coś takiego :

W przedstawionym na początku przykładzie nie ma czegoś takiego ale ale.... polimorfizm jest w innym miejscu. Przejawia się on niejako pod postacią operacji na typach a nie na typach samych w sobie.

No i ponownie nie ma się co spinać tylko zaobserwować różnice i wybrać co lepsze w danej sytuacji. A jak ktoś jest uczulony na samo słówko "instanceOf" to może sobie wybrać formę zapisu, która to ukrywa i po kłopocie :)

A no i jest jeszcze efekt krugera dunninga - jak już ktoś przeczyta "head first design patterns" i odkryje polimorfizm to może mu się wydawać, że jest na końcu drogi - kiedy tak naprawdę znajduje się na początku...

Mobilization call for papers

Mobilization 2014 call for papers --> Jest tuaj

2 komentarze:

  1. Paweł wspominałeś, że opcja z instanceof jest rozwiązaniem Expression Problem. Natomiast takie podejście wymaga od nas czujności. Każda dodanie typu bez aktualizacji metody obsługującej może być potencjalnie bugiem. Kompilator Javy (i chyba niczego innego) nas o tym nie ostrzeże, a błąd wyjdzie wiadomo gdzie. Tutaj nawet pozornie dobrze przetestowany kodzik będzie narażony, bo wymaga to pamięci o jakichś handlerach, które gdzieś związane i zależne są od drzewa dziedziczenia. Myślę, że jakimś rozwiązaniem dającym nam trochę więcej bezpieczeństwa mógłby być wizytor (o ile chcemy mieć pewność implementacji dla całego drzewa).

    OdpowiedzUsuń
  2. No i po to mamy te ćwiczenia aby zrozumieć co się przehandlowuje za co :)
    Dostajesz możliwość deklarowania operacji na typie poza samym typem przez co masz więcej elastyczności zmiany ale przy okazji masz więcej możliwości popełnienia błędu.

    OdpowiedzUsuń