wtorek, 26 sierpnia 2014

Expression problem nie tylko dla orzełów

Zagadnienie, które będzie poniżej plastycznie opisane kwiecistym językiem ma się tak do codziennego programowania jak zawody formuły 1 do jazdy osobówką po mieście. Niby nie widzimy na ulicach formułek a jednak stanowią one ciekawy poligon dla rozwiązań technicznych, które po pewnym czasie lądują w normalnych samochodach

Z drugiej jednak strony fakt, że nie mam pojęcia o zawodach formuły 1 może sprawić, że powyższe porównanie jest nieprawdziwe i bez sensu. Tak czy inaczej najtrudniejszą część - czyli wstęp - mam już za sobą i można jechać dalej...

Więcej niż "polimorfizm"

Przez lata moje rozumienie słowa "polimorfizm" sprowadzało się do "jest interfejs i jego kilka implementacji" i było swoistym uwiecznieniem dobrego projektu obiektowego. Pierwsza ciekawa nowinka(dla mnie) - można to samo uzyskać bez żadnego interfejsu zwykłymi generykami - a dwa - nie ma się czym podniecać bo jak wszystko w życiu ma to swoje wady i zalety.

Na wzmiankę o wadach takiego rozwiązania natrafimy w miejscu, którego się nie do końca spodziewamy - czyli w rozdziale zdaje się szóstym "Clean Kodu" o Obiektach i Strukturach danych. Właśnie tam opisany jest ciekawy przypadek kiedy to użycie niesławnego instanceof może nam bardziej pomóc niż zaszkodzić. Gdy mamy sytuację gdzie dochodzą dochodzą nam nowe typy to rozwiązanie z interfejsem ładnie w to wchodzi ale już przy nowych operacjach, na typach jednak wbrew intuicji i powszechnej poprawności projektowej - instance of sprawdza się lepiej.

I to jest moment na wzmiankę o ...

Expression problem

Genezę problemu można sobie znaleźć tutaj -> http://en.wikipedia.org/wiki/Expression_problem lub tutaj : Expression Problem. Co więcej możemy legalnie ściągnąć profesjonalną pracę naukową samego Oderskiego -> PDF Oderskiego

Niestety albo stety prace naukowe mają do siebie, że brak tam rysunków i animacji rodem z "head first" albo też multimedialnych książek kucharskich. Aby więc było łatwiej zamiast używać oryginalnego problemu z rozwijaniem języka wyrażeń naukowych skupimy się na przykładzie z "Clean Code" - kółka i kwadraty (bo "trójkąty i kwadraty" to już chyba będzie plagiat).

  • M1 - tworzymy interfejs kształt i klasę kółko
  • M2 - dodajemy nowy typ kwadrat - tu nie ma schodów
  • M3 - dodajemy nową operację na kółku - tu są schody
  • M4 - a tutaj tę nową operację dodajemy też do kwadratu

M1

//1
object ExpressionsBlog extends M1{
  //2
 val circle:Shape=new Circle(5)            //> circle  : com.wlodar.expressions.poligon.ExpressionsBlog.Shape = com.wlodar.
                                                  //| expressions.poligon.nablog.M1$Circle@72067564
}

package nablog{
 //3
 trait M1{
  
  trait Shape{
   def area:Double
  }
  //4
  class Circle(r:Int) extends Shape{
   def area=Math.PI*r*r
  }
  //5
  def sum(s1:Shape,s2:Shape)=s1.area+s2.area
 }
}

  1. Traity w Scali najłatwiej wytłumaczyć porównując je do interfejsów w Javie ale tutaj widać także ich trochę inną naturę - w tym miejscu trait M1 wzbogaca moduł/obiekt ExpressionsBlog w nowe zachowanie i nowe typy(tutaj Shape i przykładowa implementacja Circle)
  2. Ponieważ kompilator Scali sam zgaduje typ zmiennej, więc aby uogólnić ów typ do Shape musimy jawnie zadeklarować kółko jako właśnie Shape
  3. Mamy dwa traity : M1 i Shape - widzimy (mam nadzieję) także dwoistą naturę tego konceptu - "Interfejs" i "Moduł". Możemy sobie wewnątrz deklarować klasy itd itp
  4. Klasa jak klasa - standardowe rozszerzenie Shape z implementacją metody
  5. A tutaj mamy metodę sum, która będzie służyła do ilustracji kilku ciekawych faktów i eksperymentów okołopolimorfizmowych.

M2

//1
trait M2 extends M1{
  class Square(a:Int) extends Shape{
   def area=a*a
  }
 }

//2
trait M2Rectangle extends M2{
  class Rectangle(a:Int,b:Int) extends Shape {
   def area=a*b
  }
 }
  1. Dodanie nowego typu jest naturalne i nie wymaga żadnego "wzorca" - co podkreślam by czytelnik dopuścił do siebie fakt, iż wzorzec pomimo "efektu aureoli" cudnego rozwiązania może być zwykłym nadmiarowym szumem
  2. No i Traity można rozwijać niezależnie
//1
object ExpressionsBlog extends M2 with M2Rectangle{  
 val circle:Shape=new Circle(5)            //> circle  : com.wlodar.expressions.poligon.ExpressionsBlog.Shape = com.wlodar.
                                                  //| expressions.poligon.nablog.M1$Circle@57a220c2
   //2                      
 val square=new Square(3)                  //> square  : com.wlodar.expressions.poligon.ExpressionsBlog.Square = com.wlodar
                                                  //| .expressions.poligon.nablog.M2$Square@23557525
 //2
 val rectangle=new Rectangle(3,4)          //> rectangle  : com.wlodar.expressions.poligon.ExpressionsBlog.Rectangle = com.
                                                  //| wlodar.expressions.poligon.nablog.M2Rectangle$Rectangle@164af41d
 
}
  1. Przykład dodania dwóch traitów do modułu
  2. Dzięki temu mamy dostęp zarówno do kwadratu jak i prostokąta

M3

//1
trait M3 extends M1{
//2
  trait Shape extends super.Shape{
   def perimeter:Double
  }
//3
  class Circle(r:Int) extends super.Circle(r) with Shape{
   def perimeter=2*Math.PI*r
  }
 }
  1. Przy dodawaniu nowej operacji zaczynają się schody - nie da się wykonać tego tak naturalnie jak w przypadku dodawania nowych typów
  2. Aby zrodzić iluzję dodanej metody przesłaniamy trait Shape z M1
  3. To samo robimy z klasą ale tutaj jeszcze dodatkowo implementujemy przesłonięty Shape
//1
object ExpressionsBlog extends M3 with M2Rectangle{
  
 val circle:Shape=new Circle(5)            //> circle  : com.wlodar.expressions.poligon.ExpressionsBlog.Shape = com.wlodar.
                                                  //| expressions.poligon.nablog.M3$Circle@331f9cee
   //2
   circle.perimeter                               //> res0: Double = 31.41592653589793
   circle.area                                    //> res1: Double = 78.53981633974483
 
}
  1. Cały czas implementujemy dwa niezależne rozszerzenia M1 - za chwilę stanie się z tym coś ciekawego.
  2. Kółko dostało obydwie metody area i perimeter

M4

//1
trait M4 extends M2 with M3{
//2
  class Square(a:Int) extends super.Square(a) with Shape{
   def perimeter=4*a
  }
 }
  1. Scalamy oba kierunki w jeden moduł - tego chyba nie da się zrobić w Javie - nawet ósemce z "defaultowymi metodami" bo jest konflikt nazw
  2. Standardowe Przesłoniećie klasy - klasę bazową bierzemy z jednego modułu a nową operację z drugiego
Niby problem się rozwiązał ale teraz zaczną się dopiero ciekaw rzeczy....

Niebezpieczeństwo

W opracowaniu Oderskiego wykorzystany jest dodatkowo typ abstrakcyjny- na początku zupełnie nie rozumiałem po co skoro wszystko działało i bez tego. No i to był problem bo ... działało za bardzo. Zerknijmy na to co jest w M1

 trait M1{
  //....
  def sum(s1:Shape,s2:Shape)=s1.area+s2.area
 }

I teoretycznie mogę tutaj przekazać coś co implementuje Shape z M1 jak i Shape z M3. Ale co jeśli to jest niedobre, bardzo bardzo złe i nie chcę tego? No to teraz czas włąśnie na...

Typ Abstrakcyjny

Aby pokazać pełną abstrakcję typu abstrakcyjnego nazwiemy go pomidor
trait M1 {
    //...
    //1
    type pomidor <: Shape
    //2
    def sum(s1: pomidor, s2: pomidor) = s1.area + s2.area
  }
  1. Najtrudniejszym elementem w tym miejscu jest chyba ten znaczek : "<:" wzięty prosto z gry PACMAN - a oznacza on zwyczajnie, że pomidor będzie podtypem kształtu.
  2. Nasza metoda nie przyjmuje teraz kształtów ale dwa pomidory. Wydaje się, że za wiele zmian nie zobaczymy w porównaniu do zwykłego dziedziczenia ale teraz tak naprawdę mogę dokładnie sterować czym jest pomidor - i jak będę chciał to nie będzie miał nic wspólnego z Shape z M1
rait M3 extends M1 {
    //1
    trait Shape extends super.Shape {
      def perimeter: Double
    }
    //2
    type pomidor = Shape
    //....
  }
  1. Pamiętamy, że Shape z M1 jest przesłonięty przez nowy Shape
  2. I ustalamy, że pomidor to właśnie nasz nowy Shape czyli to co nie implementuje Shape z M3 nie ma wstępu do metody
Czyli to się nie skompiluje :
object Testy extends M4 with M2Rectangle {
  def test = {
    val circle: Shape = new Circle(5)
    val rec = new Rectangle(3, 4)
//rec i circle to dwa różne szejpy!
    sum(circle, rec)
  }
}

Polimorfizm inaczej

Metoda sum zdefiniowana w M1 dodaje pola dwóch figur. Stwórzmy sobie sztuczny problem i załóżmy, że po dodaniu nowej metody w M3 chcielibyśmy mieć kontrolę nad tym jak to sum jest liczone :

trait M1 {
   //....
    type pomidor <: Shape
//2
    trait Summarizer{
     def add(s1:pomidor,s2:pomidor):Double
    }
    object AreaSummarizer extends Summarizer{
     def add(s1:pomidor,s2:pomidor)=s1.area+s2.area
    }
//1
    def sum(s1: pomidor, s2: pomidor)(s:Summarizer) = s.add(s1,s2)
  }
  1. Definiujemy taką niby strategię - sposób dodawania pomidorów jest zdefiniowany poza metodą sum
  2. Summarizer to zwykły interfejs
I teraz w M3 mogę sobie zdefiniować sumowanie po obwodzie :
 trait M3 extends M1 {
//................................
    type pomidor = Shape   
    object PerimeterSummarizer extends Summarizer{
     def add(s1:pomidor,s2:pomidor)=s1.perimeter+s2.perimeter
    }
  }

sum(circle, sq)(PerimeterSummarizer)
Jest jeden mały problem - API troszeczkę ssie pałkę. Zobaczmy co można z tym zrobić :
//1
def sum(s1: pomidor, s2: pomidor)(implicit s:Summarizer) = s.add(s1,s2)
//2
implicit val summarizer=PerimeterSummarizer
//3
sum(circle, sq)
  1. Dodajemy słówko "implicit" co oznacza, że summarizer będzie wyszukiwany przez kompilator i jak go znajdzie to sam go wstawi jako parametr (a jak nie to będzie błąd). Uwagę czytelnika niech zwróci także druga para nawiasów.
  2. Gdzies tam w module deklarujemy domyślnego summarizera
  3. API jest znowu czyste - jak chcemy to cały czas możemy przekazać kolejny argument, który będzie różny od implicit
I uwaga można z tym iść jeszcze dalej :
trait M1 {
    //....
    type pomidor <: Shape
    
//1
    trait Summarizer[A]{
     def add(s1:A,s2:A):Double
    }
//2
    object AreaSummarizer extends Summarizer[pomidor]{
     def add(s1:pomidor,s2:pomidor)=s1.area+s2.area
    }
//3
    def sum[A](s1: A, s2: A)(implicit s:Summarizer[A]) = s.add(s1,s2)
  }
  1. Summarizer ma generyka A
  2. AreaSummarizer określa generyka A jako pomidor
  3. I sum ma tez generyka A
Ej i teraz obadajcie to :
trait M3 extends M1 {
//....
    type pomidor = Shape
//1
    implicit object PerimeterSummarizer extends Summarizer[pomidor]{
     def add(s1:pomidor,s2:pomidor)={println("pomidor");s1.perimeter+s2.perimeter}
    }
  }


 trait M2Rectangle extends M2 {
//2
  implicit val summarizer=new Summarizer[Rectangle]{
     def add(s1:Rectangle,s2:Rectangle)={println("rectangle");s1.area+s2.area}
    }
  
    class Rectangle(a: Int, b: Int) extends Shape {
      def area = a * b
    }
  }

def test = {
    val circle = new Circle(5)
    val rec = new Rectangle(3, 4)
    val sq = new Square(6)
    
    sum(circle, sq) // pomidor
    sum(rec, rec) // rectangle
  }

Normalnie kompilator sam wstrzykuje to co trzeba na podstawie typu! W "1" mamy obsługę Shape w "2" prostokąta ze starym shape z M1 i wio. Ale jest jeszcze bardziej pojechany mechanizm...

Typy Fantomowe

Najpierw ogólna koncepcja :
//1
package phantoms1{
 sealed trait NotValidated
 sealed trait Validated
 sealed trait Normal
 sealed trait Admin
 
 case class User[A,B]
}
//2
def validate[B](u:User[NotValidated,B])=user.copy[Validated,B]
def secret[B](u:User[Validated,B])=println("sekrety")

//3
val user=new User[NotValidated,Normal]
val validatedUser=validate(user)

//4
secret(validatedUser)                           //> sekrety
//5
secret(user)

  1. Deklarujemy serię traitów, która tak naprawdę nigdzie nie będzie deklarowana - dlatego własnie fantomy
  2. Tutaj jest istota przykładu - mamy taką niby walidację w czasie kompilacji poprzez sprawdzenie czy user ma odpowiednie generyki a może je mieć tylko wtedy jeśli przeszedł metodę "validate"
  3. Na początku user miał generyka NotValidated
  4. Jeśli przeszedł walidację i generyk zmienił się na Validated to wyswietli sekret
  5. A jak nie to wywali sie kompilacja
Jak to wykorzystać w głównym przykładzie?
trait Summarizer[A,B]{
     def add(s1:A,s2:A):Double
    }
    trait ZwyklySummarizer
    implicit object AreaSummarizer extends Summarizer[pomidor,ZwyklySummarizer]{
     def add(s1:pomidor,s2:pomidor)={println("shape z M1");s1.area+s2.area}
    }
    def sum[A](s1: A, s2: A)(implicit s:Summarizer[A,ZwyklySummarizer]) = s.add(s1,s2)

//////////
trait NiezwyklySummarizer
    implicit object PerimeterSummarizer extends Summarizer[pomidor,NiezwyklySummarizer]{
     def add(s1:pomidor,s2:pomidor)={println("pomidor z M3");s1.perimeter+s2.perimeter}
    }
////
implicit val summarizer=new Summarizer[Rectangle,ZwyklySummarizer]{
     def add(s1:Rectangle,s2:Rectangle)={println("rectangle");s1.area+s2.area}
    }

//
   sum(circle, sq) //shape z M1
    sum(rec, rec) //rectangle
No i "NiezwykłySummarizer" z M3 został olany i w jego miejsce wszedł "Zwykly" z M1. Przykład trochę na siłę ale stworzony w celach czysto edukacyjnych.

Podsumowanie

  • W programowaniu czeka na nas wiele ciekawych rzeczy
  • Warto czasem czytać referaty i opracowania naukowe i zwalczyć naturalną niechęć do tego typu prac wykształcony w nas przez system szkolnictwa polskiego.
  • a tu jest cały kod
*   *   *

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