czwartek, 29 czerwca 2017

Na granicy systemu

Nie wiadomo czy to miejska legenda czy przypadek prawdziwy ale podobno w Indiach mają taką sztuczkę na łapanie małp : wsadzają banan w butelkę a butelkę zakopują. Teraz małpka wyciąga łapkę, łapie banan zaciskając paluszki i już łapka tam zostaje. Małpka banana nie puści i będzie tak czekała zaklinowana aż kłusownicy po nią przyjdą.

Przykład jest tak absurdalny, że doczekał się znaczenia metaforycznego kiedy to ludzie chwytają tak swoje myśli, które to wiążą ich w mentalnych klatkach. I ludzie ci tak będą czekać w tej klatce a myśli nie puszczą. Jaki to ma związek z dzisiejszym artykułem. W zasadzie żaden ale to fajna anegdota a zawsze te pierwsze akapity są najtrudniejsze.

Granice Systemu

Dzisiejszy artykuł sponsorują dwie funkcje :

  • toHex : String => Hex
  • createUser : Json => User
Obie w założeniu działają na granicy systemu czyli zupełnie niekontrolowany 'input' zamieniają w typ domenowy. Coś jak na rysunku

Hex i User... z pozoru mają ze sobą niewiele wspólnego - tak jak i obydwie funkcje toHex oraz createUser. Ale jest pewna wspólna rzecz, jedna niespodziewana i siejąca spustoszenie w systemach IT rzecz. Nawet bez zaglądania do ich definicji można z dużym prawdopodobieństwem rzecz - obydwie te funkcje kłamią!

Program jako funkcja całkowita

Ten akapit będzie streszczeniem innego artykułu : Program jako funkcja całkowita i generalnie chodzi o to, że bug w systemie jest wtedy jak myślimy że nasz program-funkcja jest zdefiniowany dla konkretnych danych wyjściowych a nie jest :( . Na przykład nasz program to aplikacja webowa przetwarzająca request w response czyli Request=>Response no i dla requestu z sqlinjection zachowanie poprawne nie było zdefiniowane - czyli na przykład wyświetlenie błędu - i całość zakończyła się katastrofą.

No i teraz jak mamy "miniprogram" Json => User to o ile dla każdego błędnego jsona nie zwracamy użytkownika Roman to to nie może działać!!!

Null object (czy coś takiego)

Kiedyś pamiętam był na tapecie taki koncept by nie zwracać z funkcji nuli tylko na przykład puste listy ,puste tablice, puste mapy, puste... puste k**wa cokolwiek. Potem pamiętam serię artykułów, że ludzie trochę zaczęli przekombinowywać i zwracać "puste" obiekty nie tylko gdy brakowało danych w bazie czy coś w ten deseń ale gdy np. wystąpił błąd. Jest to zamiatanie problemu pod dywan. Bardzo złe. Bardzo niedobre. Co możemy zwrócić jak jest zły JSON? No jeśli to nie jest na chama robiony CRUD to latający obiekt z "wrong variance" czy jak to się mówi po angielsku - spowoduje tylko kłopoty

Podobnie dla HEX -> "FFAA00" to dobry hex , "DUPA" to zły hex (acz trywialna implementacja String.getBytes obydwa zamieni w poprawny typ z poprawnymi pojedynczymi wartościami ale złym znaczeniem!!!) . I teraz weźmy dwie szkoły. Po pierwsze możemy rozszerzyć rezultat funkcji String => Option[Hex] czy JSON => Option[User]

Druga szkoła to dobrze znane nam wyjątki. Tutaj sztuczka polega na tym, że blok try-catch niejako ogranicza dziedzine funkcji ale w trochę nieintuicyjny sposób bo albo działamy na poprawnym zbiorze wartości i dostaniemy poprawny rezultat albo działamy na niepoprawnym zbiorze i ... zakrzywiamy czasoprzestrzeń lądując "kiedyś" i "gdzieś".

Problem z null

Na nulla możemy także spojrzeć z perspektywy funkcji częściowej i całkowitej. Otóż jeśli np. mam funkcje String => Option[Hex] i jest ona całkowita ze względu na każdy element zbioru String to można szybko zamienić ją na częściową wprowadzając null gdyż wtedy domena zamienia się na "każdy element zbioru string i do tego null" czyli aby znowu uzyskać funkcję całkowita potrzebujemy dodać dedykowaną obsługę nulla.

Czas ma znaczenie

Znak zapytania na ostatnim diagramie to bardzo często jakiś tam ErrorHandlingController. To jedno ale druga sprawa to, że nie za bardzo mamy czas pomyśleć bo to dzieje się teraz!! . Skok w try następuje od razu. W przypadku szkoły pierwszej wspomnianej wcześniej mamy pewien typ. Ten typ mamy tu i teraz i możemy się chwile zastanowić - on nie ucieknie. Innymi słowami try-catch to wykonanie instrukcji a Option[User] czy Option[Hex] to obliczenia w toku. Co więcej obliczenia, które nawet nie muszą być jeszcze wykonane jeśli mamy leniwy język!

No i mając ten Option mogę stworzyć jego dalsze przetwarzania przy pomocy dostępnych kompozycji jak option.orElse(otherOption) . Przy skokach w try oczywiście też mogę dać obsługę wyjątków ale zazwyczaj nie wynika taka operacja z typów co dla RuntimeException zwiększa ryzyko, że o tym zapomnimy faktycznie o obsłudze. No i oczywiście sam skok to nie jest jakaś zwracana wartość tylko no... "skok" także nie można tego za bardzo komponować co może zakończyć się tuzinem zagnieżdżonych klamerek przy bardziej wyszukanej obsłudze.

Szkoła dwa_i_pół

W GO popularne jest inne podejście. Często funkcja zwraca potencjalny rezultat i potencjalny błąd przez co jest całkowita.

func aleBezpieczneDzielenie(a, b float64) (float64, error) {
 if b == 0.0 {
  return 0.0,errors.New("oj neidobrze")
 } else {
  return a/b , nil
 }
} 
Problem z tym przykładem jest taki, że pomimo, iż potencjalne - to jednocześnie wartość i błąd musza być konkretne stad mimo wszystko zwracane jest 0.0 co jest dziwne - no i w sumie to ode mnie zależy czy błąd sprawdzę czy nie także nadal może się popsuć. Oczywiście nie znam tak dobrze jeszcze tego języka i być może jakaś opcja jeszcze gdzieś tam inna istnieje by to inaczej obsłużyć.

Szkoła trzecia - siła typów

Zaczęliśmy od A=>B by dojść do teoretycznie bezpieczniejszego A=>Option[B] czyli bezpiecznie przyjąć dane z zewnątrz systemu

  • intoSystem: String => Option[Hex]
  • intosystem: JSON => Option[User]

Ale co gdy mimo wszystko chcemy zachować funkcję String => Hex ? Cóż wtedy, wtedy cóż? Być może to jakaś funkcja biblioteczna a może po prostu nam pasuje tak jak jest? Zawsze można to zrobić to podejściem ze szkoły pierwszej czyli coś w stylu lift : (A=>B) => (A=>Option[B]) ale można też rozwiazac problem od drugiej strony ograniczając dziedzinę do tylko prawilnych User-JSONów lub Hex-Stringów!

Lub na jeszcze innym rysunku zobaczymy porównanie rozwiązania błędnego z funkcją częściową, funkcję całkowitą poprzez rozbudowanie domeny wyniku, oraz funkcję całkowitą poprzez ograniczenie domeny wejścia do bezpiecznego typu

W kodzie by to wyglądało mniej więcej tak:

val validate: String => Option[SafeHexString] = ...

val toHex : SafeHexString => Hex

validate(input).map(toHex)

Podsumowanie

Artykuł zaczęliśmy od śmiesznej anegdoty o małpce złapanej w butelkę (po która przyjdą kłusownicy - czyli może nie tak śmiesznej). Miało to nie być związane z samym artykułem ale jednak będzie. Metafora banana i trzymania się kurczowo nawyków. Gdy Optional pojawił się w Javie wielu programistów, z którymi miałem styczność traktował go jako takiego "wrapper na nulla". "Wrapper na nulla" należy do domeny Javy, być może dobrze czasem wznieść się na bardziej abstrakcyjny poziom by zobaczyć inną istotę używanych konstrukcji. Jeśli komuś nie pasuje metafora z małpką która będzie zjedzona w Indiach to inna opowieść to "Brzydkie Kaczątko" ale trzeba by ją trochę nagiąć by kaczątko stało się monadą niewykorzystanego potencjału.

niedziela, 4 czerwca 2017

Rozszczepienie obiektu

Jest takie pojęcie - "myślenie lateralne" - stworzone przez E. De Bono, który jest takim specem od ogólnej nauki i przyswajania wiedzy. W tejże książce znajdują się (takie fajne) rysunkowe przykłady graficznego tworzenia jakiegoś rozwiązania gdy dysponujemy ściśle określonym kształtem figur. Zabity na pałę kształt figury symbolizuje sztywne przekonanie a problemy zbudowania rozwiązania symbolizują problemy szablonowego podejścia do rozwiązania problemu gdy mamy tylko sztywne przekonania.

Na kolejnych stronach sztywne kształty są rozbijane na nowe i w końcu budujemy rozwiązanie z nowych przekonań. Jak to ma się do programowwania?

Jest takie powiedzenie "jak programujesz młotkiem to wszystko wygląda jak gwoździe" . Osobiście na tym blogu w ostatnich latach staram się to stwierdzenie odnosić do Javy - z prostego powodu. Sam kiedyś myślałem, że "programowanie" zamyka się w segregacji interfejsów by było bardziej "domenowo", dorzuceniu stosu annotacji by framework przyjął nasz kod i podregulowaniu JVM by było szybciej.

Dlatego też polecam by każdy podszedł do nauki kolejnego języka (nawet bez szybkiej perspektywy zastosowania go w praktyce), który zmusi go do wypróbowania zupełnie nowych podejść i sprawi, że sztywne kształty zostaną rozbite i nowa kompozycja rozwiązania stanie się realna.

Rozszczepienie obiektu

Mamy klasy, które mają pola - pola ustawiamy seterami lub konstruktorem a pobieramy getterami. Inne nazewnictwo tego mechanizmu to "mutatory" i "akcesory" (czy "accessory"). getter są połączone z daną instancją obiektu ... ale czy muszą?

Obiekty w javie to (chyba) częściej będą jakieś bezstanowe kontrolery/repozytoria lub zwykłe DTO/rekordy danych (pole+getter) aniżeli jakaś wyrafinowana enkapsulacja zmiennego/mutowalnego stanu. Wynikać to poniekąd może z faktu, że dane i tak trzeba zapisać w bazie toteż "życie" takiego obiektu jest bardzo krótkie. Jeden z niewielu przykładów gdzie z mojego doświadczenia takie prawo nie obowiązywało to akka-persistance gdzie aktor może kumulować prywatny stan mając zabezpieczenie w postaci "event journala" - nie wykluczone, że podobna sytuacja występuje w domenach i narzędziach, z którymi przyjemności nie miałem pracować ale chyba w CRUDach produkowanych w Łódzkich szwalniach raczej przeważa model "wyciąg z bazy, zmien i wciśnij na powrót".

I taka "obiektowa" reprezentacja rekordu danych np "User" nam do dalszych rozważań spokojnie wystarczy.

No bo teraz zobaczmy taką konstrukcję.

class Street(name:String)
class Address(s:Street,city:String)
class User(name:String, address:Address)
Mając cały czas ten pierwotny sztywny kształt "getter w instancji" próbując dostać się do nazwy ulicy otrzymamy .
street.getName
address.getStreet.getName
user.getAddress.getStreet.getName

Wiele osób sie skręci na ten widok bo tyle kropek w jednej linii uważane jest (poniekąd prawidłowo) za antywzorzec i nazwane "train wreck". Oburzenie jednocześnie byłoby i nie byłoby uzasadnione. Otóż spierdolina niewątpliwie może się pojawić gdy np. w obiekcie mamy listę produktów i staramy się pobrać najdroższy z nich

Można to zrobić źle

class Klasa{
private List<Product> products = ...
getProducts... 
}

klasa.getProducts.sort(desc).takeFirst

lub trochę lepiej :

class Klasa{
private List<Product> products = ...
findMostExpensive = products.sort(desc).takeFirst 
}
klasa.findMostExpensive

I to dla mnie wydawało się jasne i oczywiste gdy byłem w epoce javy ale nic nie stoi na przeszkodzie by cały czas mieć enkapsulację danych a wynieść metodę do innego obiektu, który może działa w zakresie prywatnym danej instancji. Brzmi trochę dziwnie ale np. scala umożliwia taką konstrukcje przy pomocy companion object

class Klasa{
private List<Product> products = ... 
}

object Klasa{
def findMostExpensive(instance:Klasa) = instance.products.sort(desc).takeFirst 
}


//in a different package
import Klasa._
val k=new Klasa
findMostExpensive(k)
i to może zadziałać tylko jeśli na liście będą metody, które sobie wymyśliłem. A jeśli mamy jakieś "superSpecjalistyczneBiznesoweSortowanie" ? Wtedy można wywołać ją "explicit"
superSpecjalistyczneBiznesoweSortowanie(instance.products).takeFirst
Ale jeśli spojrzeć na superSpecjalistyczneBiznesoweSortowanie nie jak na metodę a jak na funkcję "List[Products] => List[Products]" wtedy może mieć miejsce zgrabna kompozycja
val findMostExpensive : Klasa => Product = 
getProducts andThen superSpecjalistyczneBiznesoweSortowanie andThen getFirst 

Czym jest getProducts? To getter w postaci funkcji, która przyjmuje daną instancje. Normalnie jak mamy "getCos" w klasie to tak "na pierwszy rzut oka" nie ma parametrów ale na rzut oka drugi w bajtkodzie będzie przekazany parametr "this" czyli mamy taki "coupling" gettera do danej konkretnej instancji. Możemy zrobić "decoupling" oraz odpowiednio sterować zakresem dostępu by getter wyciągał bebech w określonych miejscach.

ok, rozbijając pierwotny kształt dochodzimy do trochę innego sposobu budowania rozwiązania -> podążajmy dalej tym tropem

Optyka

Jeśli już mamy getName, getStreet i getAddress wtedy bez problemu możemy całość skomponować w jednego gettera

val getStreetName : User => String =  getAddress andThen getStreet andThen getName
no fajnie ale co nam to daje? Jest to klasyczny przyklad user, ktory ma adres. Firma może mieć adres i zakup moze miec adres. no i można getter "reużyć" w kompozycji.
val getStreet : Address => String = getStreet andThen getName
val companyStreet: Company => String = companyAddress andThen getStreet
val purchaseStreet: Purchase => String = purchaseAddress andThen getStreet
No i jest rejuse poprzez ładną kompozycję. A to dopiero gettery. Magia zaczyna się gdy poskładamy tak sobie settery i może skromna wizualizacja poniżej.

Wspomniany mechanizm nie został odkryty tu i teraz. To jest popularne podejście FP zwane Lens i realizowany przez biblioteki Monocle(https://github.com/julien-truffaut/Monocle) lub rodzime Quicklens(https://github.com/adamw/quicklens). A monocle dlatego, że lens jest częścią ogólniejszego mechanizmu zwanego optics i jest tam kilka innych ciekawych rzeczy.

Iso

Co jeśli w jednym kontekście numer ulicy to String ale gdzie indziej operujemy "bardziej domenowym" StreetNumber. Można łatwo dokomponować do gettera/settera konwersję "w tę i na zad".

Optional

Nulle to chuje. Kosztują wiele błędów, czasu i pieniędzy. Dlatego brak wartości należy modelować przy pomocy Option/Optional/Maybe . No i możemy Optional wkomponować w nasze gettery tak, że dalsze soczewki będa komponowane w kontekście potencjalnego braku wartości i na koniec dostajemy np. Option[StreetNumber]

List

Jeśli Option to zero lub jeden element - tak lista to zero lub n elementów. No i teraz taka opcja. Powiedzmy, że mamy jebany javowy moloch wygenerowany z SOAP.

getUser composeTraversal getPurchases.map(_.price) composeFold reduce  // to pseudokod
I to może nam dać szybki raport o sumie zakupów

Functor

No to jak jest Option i jest List to pewnie będzie i generalizacja w postaci funktora. I np. w monocle mamy:

  /** modify polymorphically the target of a [[PLens]] using Functor function */
  def modifyF[F[_]: Functor](f: A => F[B])(s: S): F[T]

Pryzmat

Ta koncepcja raczej będzie opcja w świecie JAvy. Pryzmat jest w stanie rozszczepić abstrakcyjny typ ADT do konkretnego podtypu. Klasycznym przykladem jest

sealed trait Json
case class JStr(s:String) extends Json
case class JNum(i:Int) extends Json

//i to tutaj skopiowane prost z dokumentacji
val jStr = Prism[Json, String]{
  case JStr(v) => Some(v)
  case _       => None
}(JStr)

Warsztaty

Temat poruszony w tym artykule został zainspirowany książką "[TYTUL]" która stanowi kręgosłup nowego cyklu warsztatów na łódzkim JUGu (JUG - Just User Group) "Modelowanie Domeny z FP". Pierwsze spotkanie już się odbyło i kilkanaście osób na tak wyspecjalizowany temat interpretuję jako zainteresowanie duże.

Na

pierwszym spotkaniu uczyliśmy się jak wykorzystać monady do modelowania efektów (tak padło słowo monada i nie rozumiem tego podniecenia tu i tam - "nie wypowiem słowa na M"- po to kurwa ma ta konstrukcja nazwę by jej używać) oraz jak komponować operacje biznesowe będące czystymi funkcjami z efektami. Kolejna część po wakacjach bo jak jest lato i 30 stopni to nikomu się nie chce przychodzić i jest dla mnie strasznie frustrujące jak zapisuje się 30 osób a przychodzi 5. Także wrzesień - manipulacja złożonymi typami przy pomocy "optyki" a później pewnie użycie Monad Reader jako repozytoriów.

No a artykuł był o tym by zburzyć dogmaty na których zbudowane są nasze przekonania - w tym przypadku przekonanie, że zawsze getter musi być związany z dana instancją - a jak już to się stanie to możemy budować rozwiązania z nowych "kształtów"

Post SKRYPTum

I tutaj jeszcze link z mojej próby podejścia do tego tematu dwa lata temu : http://pawelwlodarski.blogspot.com/2015/08/soczewkiobiektywylenses.html

niedziela, 7 maja 2017

Jak w Javie można podkradać fajki czyli private nie takie prywatne

We wstępie do takiej jednej książki autorstwa Roberta Cialdiniego o obronie przed manipulacją jest przykład pewnego Indora (nie chodzi o popularne na siłowniach "Indor Cycling" ale o zwykłego żywego ptoka) i tamże naukowcy podrzucili temu prawdziwemu Indorowi sztucznego Indora, a w zasadzie nie indora tylko coś co wydawało indorowate dziwięki i to wystarczyło aby prawdziwy indor zaczął traktować odtwarzacz kasetowy jako swoje własne indorze piskle.

Innymi popularnymi przykładami bezrefleksyjnego zachowania bodziec-reakcja w świecie przyrody będzie oczywiście czerwona płachta na byka, wydzielanie śliny po usłyszeniu dzwonka przez psa Pawłowa oraz oburzenie niektórych programistów Java gdy przed polami w klasie nie pojawia się słowo private.

Pola (Niepotrzebnie)Prywatne

Jak kończy się bardzo często w praktyce dodawanie modyfikatora private do pól? Gdy chcemy stworzyć po prostu reprezentacje danych a język, w którym programujemy ma tylko klasy - wtedy tworzymy klasę, która ma być po prostu danymi - dodajemy pola i generujemy getter/settery.

Ewentualnie jeśli jesteśmy w tej branży odpowiednio długo to jednak nie dodajemy setterów. Można poczytać choćby w już 10 letniej Effective Java o zaletach (z angielska) "klas niemutowalnych" i stworzyć konstruktor, który będzie dbał o to by w naszym obiekcie nie pojawiły się śmieci w stylu sto-dwudziesty-ósmy dzień tygodnia, dajemy final przed polami i jest elegancko. A jak już jest obiekt i uda nam się uniknąć jałowej dyskusji czy to ma być rich/fat/poor/flat/anemic bo potrzebujesz rekord danych - a twój język zarówno technologicznie jak i ideologicznie nie dopuszcza możliwości stworzenia czegoś takiego - wtedy trzeba na końcu nazwy dodać "DTO" i następąpi kolejny cykl bodziec-reakcja gdzie na niektórych ludzi zadziała to jak mellissa i przestaną ci podsyłać posty Martina Fowlera z przed 15 lat.

Jak już mamy poprawnie stworzony obiekt (pamiętaj pamiętaj pamiętaj : "Data Transfer Object" a nie żaden "Record" - Wszystko jest obiektem a Record to takie nieobiektowe) wtedy trzeba dać możliwość dostępu do danych. No i jest jakaś teoria za geterami, która nazywa je (z angielska) "Akcesorami" i daje jakieś tam uzasadnienie że coś tam można prywatnie zmieniać w implementacji klasy bez zmiany jej interfejsu. O i tutaj zaraz na prawo jest taki post gdzie na obrazku w nagłówku jakaś ładna pani czyta wsadowy plik DOSa -> Accessors and Mutators. No i jak to w tego typu postach mamy trochę z dupy naciąganą sytuacje gdzie w Rekordzie obiekcie Person trzeba było zmienić sposób w jaki przechowuje się middle name ze Stringa na Tablice. No niezwykle praktyczny przykład.

Oczywiście może pojawić się krytyka, że przecież muszą być gettery bo pola sa prywatne a przecież musza być prywatne bo jak nie będą to każdy tam wepchnie byle syf. Nie nie muszą. Mamy coś takiego jak juz wspomniane final i jak pole jest final to ustawiasz sobie raz to pole public final String name w konstruktorze. Na pewno się to nie uda gdy utrzymujemy projekt z przed 150 lat gdzie standard JSP wręcz wymusza by dostęp do danych był przez "getCosTam". To się nazywa konwencja i taką sobie przyjęli - być może inne Zręby frameworki też wymuszają taką konwencję. Cóż jak chcesz ich używać to nie masz wyjścia ale takie uzasadnienie "bo framework wymaga" ma niewiele wspólnego z perspektywą projektowania/modelowania systemu.

Apropo frameworków to możemy czasem wpaść na pomysł, że w sumie chcemy gdzieś zapisywać dane a później np. chcemy wyświetlić listę "Personów" na jednej ze stron naszego CRUDa. No i jest ten fajny framework, który mi wyciąga dane z bazy i pakuje do klas i w ogóle. I wystarczy tylko dać annotacje tu i tam. A no i trzeba dodać bezparametrowy konstruktor -> no bo frejmłork stworzy sobie pustą klasę i bedzie tam wpychał dane jak mu wygodnie, być może przez refleksję będzie je wpychał, być może z totalnym wyjebaniem na takie słówka jak private. No i wtedy nie ma wyjścia - jak masz konstruktor bezparametrowy to już pola nie mogą być final i musza być private. Chyba nie ma wyjścia --> z tej sytuacji bo taki standard jest. No cóż.

¯\_(ツ)_/¯

Pola (potrzebnie)Prywatne

Kiedy tworzenie pola prywatnego ma sens? Zawsze kiedy masz stan, który musisz zmieniać. To czy musisz mieć ten stan i czy to dobrze czy nie dobrze, że go zmieniasz to już inna historia. Obiekt może przechowywać stan, który na skutek interakcji z resztą systemu może ulegać zmianie i zależy ci aby przede wszystkim uniemożliwić te zmiany z zewnątrz. Z racji wydajnościowych lub innych ważnych powodów możesz chcieć operować efektywnymi acz umożliwiającymi machloje strukturami danych i a na zewnątrz chcesz operować bezpiecznymi niezmiennymi strukturami. I tutaj na przykład obiekt może manipulować tablicą bajtów czy StringBuilderem a z zewnątrz będzie można dane tylko jak string odczytywać. Plus jak sobie włączymy 2,3,4 wątek to bardzo bardzo nam powinno zależeć aby stan zmieniać tylko z jednego zabezpieczonego miejsca.

No i dla przypomnienia raz jeszcze. Jak sobie napiszesz klasę Person i tam jest getName, które zwraca prywatne pole typu String to nic nie ukrywasz - taka sztuka dla sztuki ten geter, a jak jeszcze masz seter bo jakiś framework tego chce to ani nie ma żadnego ukrywania ani żadnej kontroli. Cześtym argumentem pojawiającym się tutaj jest - "bo jak mam settera to walidację sobie mogę zrobić" - spójrz w lustro , spójrz sobie w oczy w tym lustrze i odpowiedz sam przed sobą ile razy twoja interakcja z setterem to było coś więcej niż IDE->GENERUJ.

W każdy razie po tym wstępie (tak to był wstęp, długi ale wstęp) przejdziemy do zasadniczej części artykułu. Jeśli już masz taki uzasadniony prywatny stan - to niespodzianka - on wcale taki prywatny w tej Javie nie jest.

Pola (Nawet nie wiesz, że nie)Prywatne

Obiektem - a w zasadzie dwoma obiektami - naszych badań będą dwie instancje klasy Osoba. Klasa ta prezentuje koncept biednego człowieka relaksującego się przy pomocy używek.

class Osoba{
    private int fajki;
    public final String name;

    public Osoba(int fajki, String name) {
        this.fajki = fajki;
        this.name = name;
    }

    public String relax(){
            if(fajki==0)
                return name + ": o kurde gdzie moje fajki!";
            else{
                fajki=fajki-1;
                return name + ": zostało mi "+fajki+" fajek";
            }
    }

    void podpierdolFajki(Osoba ktosInny){
        this.fajki=ktosInny.fajki;
        ktosInny.fajki=0;
    }
}
Palenie szkodzi zdrowiu. Poniższy przykład służy tylko edukacji bo prościej sobie wyobrazić, że ktoś podprowadzi fajki a nie witaminę B6.

I co my tam mamy? Jest publiczne pole name czyli już będzie (David) lincz na forach javowywch. Ale jest też prywatne pole fajki i nie ma metody getFajki. Nie powinno nikogo interesować jak ktoś chce sobie puścić dymka. No i w zależności od stanu prywatnego fajek w kieszeni tenże relaks się uda albo nie uda.

Mamy także i jeszcze jedną niepokojąca metodę. Metoda ta ma zakres pakietowy (o czym wielu zapomina, że taki jest bo jest niewidzialny) czyli możemy założyć, że to metoda prywatna dla danej paczki co jest także dosyć wygodne by podzielić logike na kilka klas w pakiecie bez pokazywania ich na świat.

No i w tej metodzie tak trochę wygląda jakby jedna instancja mogła zmienić stan w innej. czy to się kompiluje? Niestety albo stety tak

Osoba stefan=new Osoba(0, "Stefan");
Osoba babkaStefana=new Osoba(10, "Babka Stefana");

stefan.podpierdolFajki(babkaStefana);

System.out.println(stefan.relax());
System.out.println(babkaStefana.relax());
I chociaż Stefan nie ma fajek to jednak w wyniku szfindlu i kompilacji.
Stefan: zostało mi 9 fajek
Babka Stefana: o kurde gdzie moje fajki!
Dlaczego tak się stało? Otóż private w Javie nie jest dla obiektów ale dla klas! Dlatego też dwie instancje tego samego obiektu mogą sobie te prywatne pola czytać. Może to się przydać przy tzw "konstruktorach kopiujących" kiedy tworzymy nowy obiekt na podstawie starego. No i oczywiście coś takiego nie przeszkadza kiedy mamy udawane-obiekto-dane gdzie można sobie ustawić cokolwiek skądkolwiek przy pomocy settera.

Kiedy takie podkradanie danych przez instancje tej samej klasy zaczyna być zauważalne? Kiedy w deklaracji klasy pojawia się generyk oraz zależność pomiędzy klasą a typem generyka jest Covariant (więcej może tu : kowariantne-konsekwencje.html")

Pola (Naprawdę)Prywatne

Do eksperymentu wystarczy nam taka deklaracja :
class Opakowanie[+A](private var mutable:ListBuffer[A])
No i co sie teraz stanie gdy będziemy tworzyć instancję?
val o=new Opakowanie[String](ListBuffer[String]())
Błąd !
Error:(14, 34) covariant type A occurs in invariant position in type => scala.collection.mutable.ListBuffer[A] of variable mutable
class Opakowanie[+A](private var mutable:ListBuffer[A])

o so chozi? Otóż z racji, że Opakowanie[String] jest podtypem Opakowania[Object] możliwe jest szachrajstwo gdzie Opakowanie[Int] podszywając się za Opakowanie[Object] podrzuci Inta do Listy Stringów - a tego nie chcemy. Nie chcemy usuwac tego +A bo chcemy miec relację Covariant - co zrobić? Otóż rozwiązaniem jest prywatny zakres instancji

class Opakowanie[+A](private[this] var mutable:ListBuffer[A])
Jest coś takiego w Scali - Zakres prywatny Instancji .No i teraz żodyn tu int anie podrzuci Żodyn!!!

Podsumowanie

O czym dokładnie był ten artykuł? Na poziomie mechanicznym o tym, że niektóre mechanizmy języków nie zawsze działają tak jak nam się wydaje. Na poziomie ideologicznym był to apel by jednak do pewnych rzeczy nie podchodzić jak maszynka bo przemysł nas zaprogramował by lubić annotacje i "Beany". Czasem warto zajrzeć jak coś działa w innych językach a już na pewno uważać z używaniem cytatów Fowlera czy Unkla Boba jako argumentów - każdy przez to przechodzi i ja też przechodziłem - to, że konsultant, który oferuje swoje usługi płatnie wydał kilkanaście lat temu opinie jeszcze nie jest argumentem

poniedziałek, 17 kwietnia 2017

Nauka Modelowania Domeny z Programowaniem Funkcyjnym

Co będzie

  • Będzie warsztat FP + modelowanie domeny-> Modelowanie Domeny z FP - część 1 - Oddzielenie biznesu od efektów ubocznych. Najprawdopodobniej 10 maja ale wiele rzeczy się dzieje to i termin może się przesunąć.
  • na podstawie książki
  • w Scali
  • przykłady praktyczne z wykorzystaniem czasem zaawansowanego FP
  • nie będzie hajbernejta ani żadnych frameworków (nazywanych przez ludzi pozbawionych praktyki programistycznej "zrębami"), które gloryfikują kod gdzie jest więcej annotacji niż samego kodu -> czyli czegoś w stylu :
    @ManyToMany
    @JoinTable(
          name="EMP_PROJ",
          joinColumns=@JoinColumn(name="EMP_ID", referencedColumnName="ID"),
          inverseJoinColumns=@JoinColumn(name="PROJ_ID", referencedColumnName="ID"))
    @WhatDoYouMeanByLearnSQL?
    @FuckThoseRelationsOOPUberAlles
    @IJustWantToHaveAListAndCallGetterOnIt
    @IDontCare
    @PleaseDontForceMeToThink
    @MagicalAnnotationToMakeAllProblemsDissapear
    @AndAlsoPleaseHandleTransactionsForMe
    private List projects;
    
    (co w moich oczach przypomina bardziej średniowieczną alchemię gdzie dąży się do takiego dobrania składników by zamienić metal w złoto - w tym przypadku zamienić zwykłą deklarację listy w jakiś twór, który spełni niebanalne wymagania domenowe)

A dokładniej

Traktujac ksiązkę jako kręgosłup nauki pojawia się następujący plan:

  1. Tworzenie danych/aggregatów i oddzielenie operacji domenowych od operacji na efektach infrastruktory - Option,Try,Future,Validation -> i jak map i flatMap pomagają utworzyć przepływ danych
  2. Lenses i kompozycja przekształceń na agregacie danych
  3. Kompozycja operacji na repozytorium przy pomocy Monad Readera
  4. Type classes i monoid - to da wiedzę jak uzyskąć znacznie lepszą kompozycję poprzez definicję symbolicznych operacji dla danego typu
  5. Funktor,Applicative i Monady - jeszcze wiecej wiedzy jak uzyskać lepszą kompozycje funkcji biznesowych poprzez separację efektów ubocznych systemu. Tutja moga sie pojawiać zastosowania dla bardziej specjalistycznych typów jak StateMonad
  6. No i jedziemy dalej z coraz silniejsza kompozycją - tutja będzie tzw. “Kleisli arrow” , która to konstrukcja pozwala komponować typy jak M[A] => M[B] prz pomocy funkcji z efektami A=>M[B]
  7. Wykrywanie naruszeń zasad biznesowych w trakcie kompilacji - typy Fantomowe
  8. Modularyzacja/Bounded Context przy pomocy FreeMonad

Plan można zmieniać wedle upodobań. Formuła to zapewne dwie części z ćwiczeniami. Część pierwsze zajmie się bardziej mechanika używanych konstrukcji FP a druga bardziej zastosowaniem domenowym. No i formułę tez oczywiście można zmienić wedle upodobań. Także (najprawdopodobniej) 10 maja zapraszam kto chętny : Modelowanie Domeny z FP - część 1 - Oddzielenie biznesu od efektów ubocznych

niedziela, 26 lutego 2017

Closed/Closed Principle

(...)The short way of saying this is, OpenClosedPrinciple cannot work well without a good crystal ball. It is sort of like saying, "Stocks are easy, all you have to do is pick the right stock and you can be rich." It's a "tease" principle without teeth(...)

Wstęp

Jest kilka zasad w programowaniu, których się po prostu "nie rusza". Nie podważa, nie analizuje, nie szuka alternatyw.A jeśli tylko spróbujesz to zostaniesz wykluczony z kręgu towarzyskiego "programistów, którzy rozumieją jak się robi dobre programy". I teraz spróbuj nie dostać kamieniem gdy wejdziesz z opinią, że open/close principle (po naszemu "ino otworte na rozszerzonie ać z domykiem na modyfikacjom!") może w niektórych miejscach przynieść więcej szkody niż pożytku.

Co jakiś czas można się nadziać na artykuł o zasadzie otwarte/zamknięte gdzie logika argumentacji trochę przebiega jak w przykładzie "a teraz pokaże wam, że opony zimowe są lepsze od letnich. Na potrzeby artykułu załóżmy, że wszędzie leży śnieg". Czyli "o tutaj mam taki problem, który dobrze rozwiązuje dziedziczenie w javie. Dla potrzeb dyskusji przyjmijmy, że każdy problem tak wygląda". Mam nadzieję, że w tym artykule uda nam się zobaczyć, że rzeczywistość jest bardziej "różnorodna".

Ale zaczniemy od tego, że zazwyczaj przy okazji takich "praw" programowania gdzieś tam w tle pojawia się temat zmiany logiki/wymagań/funkcjonalności. I, że na zmianę trzeba się przygotować. Przygotowanie się na zmianę w miejscu gdzie ona nigdy nie nadejdzie może nas tak naprawdę sporo kosztować i wtedy zastosowanie "o/c" staje się zwykłym antywzorcem - a powiązany kawałek kodu zwykłą fuszerką. No ale jak to zmiana nie nadejdzie? Przecież w chaotycznie prowadzonym projekcie IT zmienia się wszystko! Nie. Nawet w tysiącach maksymalnie totalnie chujowo prowadzonych projektach jest kawałek logiki, który nigdy się nie zmienił i pewnie nie zmieni nigdy - i to od niego zaczniemy dalsze rozważania.

170 lat bez zmiany

(Albo 680 łokresów kwartalnych bez Rekłestu dla czendża w tejże logice)

Jeśli wierzyć internetowi (a dlaczego by nie wierzyć) Algebra Boola was introduced by George Boole in his first book The Mathematical Analysis of Logic (1847). Czyli dawno. Bardzo. I nic się nie zmieniło.

Wyobraźmy sobie teraz na chwilę, że nie ma algebry Boolea (bóla). Ni ma. Ni ma true. Ni ma false. I w tym świecie ktoś rzuca speckę by taki typ zrobić. Potencjalnie można stworzyć popularną javową konstrukcję z "interfejsem, implementacjami , polimorfizm i w ogóle"

interface Bool{
    Bool or(Bool o);
    Bool and(Bool o);
}

class True implements Bool{

    @Override
    public Bool or(Bool o) {
        return new True();
    }

    @Override
    public Bool and(Bool o) {
        return o;
    }
}

class False implements Bool{

    @Override
    public Bool or(Bool o) {
        return o;
    }

    @Override
    public Bool and(Bool o) {
        return new False();
    }
}

I chociaż technicznie taka implementacja powinna działać to jest ona bez sensu. Z bardzo prostego powodu. Chociaż dodanie nowego typu jest możliwe od strony mechaniki języka to od strony funkcjonalności to nie ma szans zadziałać. No bo jak byśmy dodali typ AlmostFalse implements Bool to teraz jak ma zmienić się zachowanie metod and i or?

Ponieważ typ jako taki
jest częścią API
to dając nowy
czeka nas zawrót głowy

W skrócie nie będzie działać. Dlatego tez if w javie od 20 wygląda tak samo. Jest albo true albo false. Nie ma nic innego i nie ma w planach niczego innego. If - i wszystko co bazuje na boolean jako boolean - jest zamknięte na rozbudowę i zamknięte na rozszerzanie. I całe szczęście. Inaczej mało co by działało i reaktory by wybuchały.

W jednym z popularniejszych przykładów Open/Closed jest chyba ten gdzie dodaje się nowe figury geometryczne i tak mając interfejs "Figura" (Katarzyna) z metodą pole , mamy trójkąt i możemy sobie dodać kółko. Przykład jest wygodny i ma tę właściwość, że typy nie oddziałują same ze sobą czyli nie ma na tej płaszczyźnie pomiędzy nimi relacji. Relacji w rozumieniu abstrakcyjnym. I właśnie wokół tego słowa i ogólnie pojętej abstrakcji na chwile przejdziemy do mniej praktycznych rozważań.

Nazwy

Jak tam wyżej wspominaliśmy Boole'a" to tam był jeszcze chyba mianownik "kto?co? -> Algebra" . Algebra jest słowem niezwykle abstrakcyjnym i chyba nie atrakcyjnym dla szerokiej masy ludzi, którzy mówiąc "w życiu trzeba spróbować wszystkiego" mają zazwyczaj na myśli narkotyki i skoki z samolotu a nie zrozumienie Analizy Matematycznej (czy jakoś tak szedł ten dowcip).

Dlatego zamiast o abstrakcyjnych pojęciach matematycznych ruszymy łatwiejszą drogą, wkleję zdjęcie czaszki i porozmawiamy o grze.

Gra ciekawa, wydana ze 20 lat temu o tytule "Planescape torment" - główny bohater nie dość, że nie żyje to nie ma imienia i to właśnie temat "znaczenia imienia/nazwy" jest tam jednym z głównych wątków. Z tego co pamiętam w pewnym momencie spotykało się typka co to przeklinał dzień kiedy odzyskał imię bo od tego dnia właśnie jego wrogowie mogli rzucać w niego klątwami czy coś w tym stylu.

Podobnie możemy podejść do kwestii nazewnictwa przy rozważaniu praw programowania. Być może lepiej zahaczać o pewne pojęcia bez definiowania konkretnej nazwy. Ominiemy w ten sposób pole minowe nafaszerowane subtelnymi niuansami znaczeniowymi i unikniemy jałowych dyskusji w stylu "czy Try to Monada" i takie tam.

ADT po raz pierwszy

Jak już pojawia się słowo Algebra niedaleko padają od jabłoni Algebraiczne Typy Danych i - czego się spodziewaliśmy - tuzin różnych definicji. Można sobie o tym poczytać na wikipedi https://en.wikipedia.org/wiki/Algebraic_data_type . Sam również kiedyś próbowałem to opisać w sposób prawidłowy i zabawny tutaj -> http://pawelwlodarski.blogspot.com/2016/05/typy-danych-ale-algebraiczne.html. Nie wiem czy udało mi się jedno albo drugie.

Skacząc dalej po linkach ciekawe zdanie jest umieszczone na haskellowej wiki : https://wiki.haskell.org/Algebraic_data_type

This is a type where we specify the shape of each of the elements(...)

Czyli rozumiejąc tak jak jest mi na tę chwilę wygodnie będzie to typ, gdzie wszystkie elementy są jasno określone. Czyli dla typu Boolean jasno określone są True i False. Tylko, że znowu może to znaczy to a może coś innego. Jaki jest rozpierdziel z definicjami doczytamy w dalszych akapitach zacytowanej strony.

Algebraic Data Type is not to be confused with *Abstract* Data Type, which (ironically) is its opposite, in some sense. The initialism "ADT" usually means *Abstract* Data Type, but GADT usually means Generalized *Algebraic* Data Type.
Także tego...

ADT po raz drugi (i PDT)

Link do opracowania : https://www.cs.utexas.edu/users/wcook/papers/OOPvsADT/CookOOPvsADT90.pdf

Wspomniana tutaj praca zestawia ze sobą reprezentacje typów programowania obiektowego zwaną dalej PDT - Procedural Data Types z innym ADT - Abstract Data Types. Przyswajania informacji wcale nie ułatwia fakt, że cytowana w poprzednim punkcie wiki Haskella twierdzi, że Abstract Data Types to coś przeciwnego do Algebraic Data Types a autorzy tego opracowania twierdzą, że jest "complementary" do programowania obiektowego.

Praca ma stron 20 dlatego wybierzemy sobie z niej jeden interesujący aspekt odnośnie dwóch różnych podejść do modelowania danych.

ADTs are organized around the observations. Each observation is implemented as an operation upon a concrete representation derived from the constructors. The constructors are also implemented as operations that create values in the representation type. The representation is shared among the operations, but hidden from clients of the ADT.

PDA is organized around the constructors of the data abstraction. The observations become the attributes, or methods, of the procedural data values. Thus a procedural data value is simply defined by the combination of all possible observations upon it.

Definicje znowu sa bardzo abstrakcyjne ale na szczęście w artykule mamy przykłady konkretnych deklaracji listy zarówno w zgodzie z PDT jak i ADT. Lista koncepcyjnie jako lista będzie też czymś co może zaskoczyć programistów "tylko javy" bo będzie ona reprezentowana jako dwa pod typy "ELEMENT_LISTY" oraz "KONIEC_LISTY"

PDT

Nil = recursive self = record
  null? = true
  head = error;
  tail = error;
  cons = fun(y) Cell(y, self);
  equal = fun(m) m.null?
end
Cell(x, l) = recursive self = record
  null? = false
  head = x;
  tail = l;
  cons = fun(y) Cell(y, self);
  equal = fun(m) (not m.null?)
    and (x = m.head)
    and l.equal(m.tail) 
end

Nie mogłem namierzyć info czy to jakiś konkret język czy taki edukacyjny pseudokod ale nie ma to znaczenia bo PDF i tak go nie skompiluje. Ten kawałek tutaj przypomina podejście "polimorfizm ala Java" ino trzeba tam jakiś interface List dodać. Mamy pod-typy i każdy po swojemu przeciąża metodę. Czyli tak jak napisaliśmy wcześniej : "Thus a procedural data value is simply defined by the combination of all possible observations upon it." A teraz zobaczmy to samo inaczej

ADT

adt IntList
representation
  list = NIL | CELL of integer * list
operations
  nil = NIL
  adjoin(x : integer, l : list) =
    CELL(x, l)
  null?(l : list) = case l of
    NIL ⇒ true
    CELL(x, l) ⇒ false
  head(l : list) = case l of
    NILerror
    CELL(x, l ) ⇒ x
  tail(l : list) = case l of
    NILerror
    CELL(x, l ) ⇒ l
  equal(l : list, m : list) = case l of
    NILnull?(m)
    CELL(x, l ) ⇒ not null?(m)
      and x = head(m)
      and equal(l , tail(m))

Tym razem bardziej to przypomina prosty zestaw typów z jasno zdefiniowanym zbiorem operacji, który można nań wykonać.Zwróć uwagę, że każda z operacji jest w pełni świadoma jakiego typu dane może dostać i stosuje nań standardowy pattern matching. Czyli tak jak napisalismy wcześniej : "Each observation is implemented as an operation upon a concrete representation derived from the constructors(...)The representation is shared among the operations, but hidden from clients of the ADT."

Praktycznie

Po tej teoretycznej wycieczce po abstrakcjach i ciekawych artykułach skupimy się na słowie, które już wcześniej padło odnośnie ADT i PDT a daje nadzieje na "win-win" czyli complementary - znaczy, że uzupełniający się. Będę starał się pokazać, że to nie jest żadne vs ale, że nasze standardowe podejście javowe może łatwo współpracować z czymś co łamie dobitnie zasadę open/closed.

Na początek zacznijmy ze standardową sytuacją, gdzie faktycznie chcemy dać sobie furtkę do rozszerzania danej logiki przez dodanie kolejnych implementacji "Biznesowej Walidacji". Będzie to mechanizm w zgodzie z O/C principle.

object BusinessLogic{
  trait BusinessData
  trait BusinessValidationPDT{
    def isValid(bd:BusinessData):Boolean
  }

  def validateProcess(data:BusinessData,checks:Iterable[BusinessValidationPDT]):Boolean =
    checks.forall(check => check.isValid(data))
}

Czyli tak jak z tymi figurami w oryginalnym przykładzie tak i tutaj mogę sobie dorzucić kolejną implementację. Mamy w tej sytuacji także druga rodzinę typów totalnie zamkniętych na wszelkie rozszerzania czyli już wcześniej wspomniany boolean. Teraz aby to rozróżnienie bardziej unaocznić zastąpmy boolean własnym typem, który po za tym, że coś się zepsuło niesie informacje co się zepsuło.

object CheckADT{
  sealed trait CheckResult
  final case object CheckOK extends CheckResult
  final case class CheckFailure(info:List[Throwable]) extends CheckResult

  def combine[A](checks:Iterable[CheckResult]):CheckResult = checks.foldLeft[CheckResult](CheckOK){
    case (CheckOK,CheckOK) => CheckOK
    case (CheckOK,f : CheckFailure) => f
    case (f : CheckFailure, CheckOK) => f
    case (CheckFailure(info1),CheckFailure(info2)) => CheckFailure(info1 ++ info2)
  }
}

Tym razem przy definicji traitu mamy slówko sealed czyli nie możemy dodawać nowych rozszerzeń poza plikiem gdzie ów trait jest zdefiniowany. Oznacza to w praktyce, że mamy dwa możliwe podtypy CheckResult i nic więcej. Teraz pytanie czy to ADT i jeśli ADT to Abstract czy Algebraic? Z punktu praktycznego nie ma to żadnego znaczenia i możemy go również nazwać "Andrzeja Dom Tonie" czy coś takiego. To co nas najbardziej interesuje to, że mamy jasno zdefiniowaną operacje na jasno zdefiniowanym zestawie typów : combine.

W zasadzie nic nie stoi na przeszkodzie by dodac kolejne operacje tak jak do boolean można dodac xora czy inne takie. Czyli cały czas pod pewnym aspektami jesteśmy przygotowani na rozbudowę ale nie kurwa tak, że fanatycznie wszystko polimorfizm i abstrakcje i polimorfizm i abstrakcje. Co więcej jest to ten moment kiedy można użyć Property Based Testing. Zazwyczaj przy okazji jakiejś prezentacji czy warsztatów ze ScalaCheck pada pytanie "no ale jak mam na przykład taką klasę User to jak do tego użyć PBT?". Źle zadane pytanie. Jak masz ADT i operacje na nich operujące wtedy możesz użyć PDT. (masa masa skrótów trzyliterowych, dobre na prezentacje marketingową, jeszcze tylko jakieś zdjęcia ludzi w garniakach ze stocka).

object prop extends Properties("CheckResult"){
  val gen:Gen[List[CheckResult]] = ???

    property("combine") =forAll(gen){checks=>
      val initialErrors=checks.collect{case  CheckFailure(infos) => infos.length}.reduce(_+_)
      val reducedErrors=CheckADT.combine(checks) match {
        case CheckOK => 0
        case CheckFailure(infos) =>  infos.length
      }
      initialErrors == reducedErrors
    }
  }

Czyli kolejna zaleta NIEstosowania o/c principle -> otwiera się przed nami potężny paradygmat testowania kodu jako zestawu twierdzeń.

No i na koniec jak to zastosować :

object BusinessLogic2{
  trait BusinessData
  trait BusinessValidationPDT{
    def isValid(bd:BusinessData):CheckResult
  }

  def validateProcess(data:BusinessData,checks:Iterable[BusinessValidationPDT]):CheckResult =
    CheckADT.combine(checks.map(_.isValid(data)))
}

Na razie chyba najlepsza publikacją omawiająca praktyczne podejście do tego co tutaj opisałem znalazłem w:

I jeszcze link do ciekawej prezentacji, żeby nie było że nikt inny nie jedzie po SOLID : why-every-element-of-solid-is-wrong

Podsumowanie

Wyobraź sobie, że to działa tak:

  1. Najpierw w ogóle nie wiesz, że jest open/close principle...
  2. Potem wiesz, że jest i tego powyżej uważasz za niekompetentnego programistę ale jeszcze nie wiesz, że jest kolejne stadium kiedy to...
  3. Rozumiesz, że open/close nie jest prawem programowania a jedynie zbiorem wygodnych uproszczeń w komunikacji i nauce
No i teoretycznie teraz wchodząc na poziom drugi myślisz, ze to ostatni poziom i nie odróżniasz tych co jeszcze nie ogarnęli o/c (czy innej prostej zasady) od tych którzy wiedzą, że to już nie działa. Oczywiście to może iść dalej bo skąd wiadomo czy nie ma czwartego poziomu gdzie jednak w jakimś kontekście to działa, a potem piątego który ponownie całą koncepcje odrzuca... Także słowo na koniec to : taki system informatyczny to inżynieria wielu zmiennych a nie zestaw prostych zasad gdzie każdy problem da się rozwiązań skończonym zestawem annotacji i hajbernejtem. Cały czas szukaj nowych źródeł wiedzy i podważaj to co już wiesz.

niedziela, 29 stycznia 2017

Kowariantne Konsekwencje

Mroźnym popołudniem standardowego dnia pracy wiedzeni drzemiącą w nas naturą podróżnika możemy wylądować gdzieś w pośród deklaracji metod bibliotecznych, które w javie8 wielce prawdopodobne będa przyjmować funkcje. I wtedy to naszym oczom może ukazać się coś takiego :

Function<? super V, ? extends T> declarationName

I tak w zasadzie to podobne rzeczy będą się ukazywać nam co chwilę... coraz częściej... w zasadzie w przypadku funkcji inna deklaracja(bez tego super i extends) nie ma sensu. Omówimy sobie skąd się to wzięło, dlaczego tak zostało, czemu inne deklaracje nie mają sensu i dlaczego pomimo, że to jedyna deklaracja z sensem to trzeba z palca zawsze to extends i super dodawać oraz, że istnieje alternatywa którą (znowu) ma w zasadzie wiele języków a nie ma java.

Problemy z typem typu

Dawno dawno temu wszystko w Javie było faktycznie obiektem. Nawet jak nie chciałeś. Jeśli mieliśmy Listę to mieliśmy listę obiektów. Były to czasy gdy monitory CRT wypalały oczy a aplikacja startowała 15 minut po to by zaraz po starcie rzucić wyjątek "class case exception" - no bo trzeba było zgadywać co za typ kryje się za obiektem. Nie było generyków.

Gdy generyki w końcu zawitały do Javy5 te problemy stały się znikome ale fani zbyt uproszczonych rozwiązań dostali nowe tematy do hejtowania bo oto pojawiły się pewne zachowania z punktu widzenia bazującego na "wszystko jest obiektem" - trochę nieintuicyjne.

No bo mamy takie coś, że -> String jest Obiektem -> mamy listę stringów -> List -> lista stringów jest obiektem ale czy stringi w liście są na tyle obiektami, że to także lista obiektów? Okazuje się, że w tym miejscu model się załamuje - bo nie jest!

Gdyby była to dałoby się zrobić coś takiego
List<String> stringi=new LinkedList<>();
List<Object> objjjekty = stringi; 
objjjekty.add(1); //kupa

A ponieważ doprowadziłoby to do eksplozji to takie niecne programowanie jest zabronione. No ale jeśli dobrze nam z oczu patrzy i obiecamy, że nie będziemy tam nic wkładać to kompilator może nam pójść na pewne ustępstwo :

List<? extends Object> probaDruga=stringi;
I jest teraz zabezpieczenie w postaci dziwnego typu co wystarczy by nas powstrzymać przed wrzucaniem tam śmieci

Czy można było inaczej to zrobić? Można było, co zobaczymy później ale patrząc na ten kawałek kodu z listą wydaje się, że mechanizm jest w porządku, nieprzekombnowany i ratuje nas przed ClassCastEx. To był rok 2004. Mija kolejnych 10 lat. Cała dekada. Wychodzi Java8 a w niej Funkcje . I to właśnie ten nowy mechanizm pokaże słabe strony wyborów z przeszłości...

Use Site Variance

Jeśli wejdziesz w definicję java.util.function.Function to zobaczysz do czego te zabawy z generykami doprowadziły

  default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }

Pytania czy te "extends" i "super" musza tam być? To co teraz zrobię być może ma jakąś nazwę w nauce o wnioskowaniu (a może nie) ale generalnie one tam być muszą bo... nie ma żadnych argumentów za tym aby w przypadku funkcji ich tam nie było! (ewentualnie ktoś może mnie tutaj do-edukować).

Extends

Jaka jest różnica pomiędzy dwoma poniższymi funkcjami bibliotecznymi? (taka funkcja "map" dla ubogich)

  public static <A,B> Collection<B> libraryMethod(Collection<A> c, Function<A,B> f){
        List<B> l =new ArrayList<>();
        for(A a: c){
            l.add(f.apply(a));
        }

        return l;
    }


    public static <A,B> Collection<B> libraryMethod2(Collection<A> c, Function<? super A,? extends B> f){
        List<B> l =new ArrayList<>();
        for(A a: c){
            l.add(f.apply(a));
        }

        return l;
    }

Różnica pojawi się w wywołaniu bo mając klasę

class User{
    private String name;
    private Integer age;

    public User(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public Integer getAge() {
        return age;
    }
}

Chciałbym teraz wykonać poniższe przekształcenie:

Collection<User>  users=new LinkedList<>();
Function<User,String> display=o->o.toString();
Collection<Object> res1=libraryMethod(users,display); // error
Collection<Object> res2=libraryMethod2(users,display);

Ale nie można go wykonać :( Wiem, że w tych akapitach zbytnio się nie rozpisałem ale próbki kodu chyba lepiej pokazują o co chodzi. Bez dodatkowego wysiłku i dodatkowych deklaracji wprowadzamy niczym nie uzasadnione ograniczenie. Nie ma naprawdę żadnego argumentu za ograniczeniem przyjmowanych funkcja do stricte typów biorących udział w równaniu gdyż nic przez to nie zyskujemy a tracimy wszystkie przypadki wywołania gdy typy nie zgadzają się "jeden do jednego". I w zasadzie tak będzie wszędzie - w sensie każdej funkcji bibliotecznej. Nie potrafię znaleźć kontr-przykładu.

Super

Z tym super na początku sprawa jest trudniejsza bo to szalenie nieintuicyjne co tu się dzieje. Starałem się to kiedyś wytłumaczyć tutaj -> http://pawelwlodarski.blogspot.com/2013/08/covariance-i-contravariance-dla.html. Tutaj spróbujmy taki prosty przykład. Niech User ma podklasę jakaś taką specjalną :

class SpecialUser extends User{

    private String somethingSpecial;

    public SpecialUser(String name, Integer age, String somethingSpecial) {
        super(name, age);
        this.somethingSpecial = somethingSpecial;
    }
}

Mamy znowu inna funkcję biblioteczną, która tym razem filtruje nasze obiekty :

 public static <A> Collection<A> filter1(Collection<A> c, Function<A,Boolean> f){
        List<A> l =new ArrayList<>();
        for(A a: c){
            if(f.apply(a)) l.add(a);
        }

        return l;
    }


    public static <A> Collection<A> filter2(Collection<A> c, Function<? super A,Boolean> f){
        List<A> l =new ArrayList<>();
        for(A a: c){
            if(f.apply(a)) l.add(a);
        }

        return l;
    }

I znowu nie ma żadnego powodu by nie pozwolić tej metodzie przyjmować funkcje, które działają na podtypach bo przecież jest to całe is-A zapewnione - w sensie, że w podtypie na pewno znajdą się te metody których wymaga metoda działająca na rodzicu i całość się skompiluje.

Function<User,Boolean> isAdult=user->user.getAge()>= 18;

Collection<SpecialUser>  specialUsers=new LinkedList<>();
// filter1(specialUsers,isAdult); //error
filter2(specialUsers,isAdult);

No i oczywiście jak zajrzysz w implementacje java.util.stream.Stream to i tam też map oraz filter również mają te super i extends -> bo zwyczajnie inna deklaracja nie ma sensu! Ale skoro nie ma sensu to czy dałoby się zrobić to jakoś inaczej by tyle tego nie pisać i by uprościć typy? Oczywiście, gdyż to już jest, to już jest, jest już to
zdrowo i wesoło
we wszystkich językach w koło

Declaration-Site Variance

Otóż w językach innych możemy zamiast pisać po tysiąc razy to "extends" zwyczajnie napisać jeden raz przy deklaracji, domyślnie zawsze będzie "extends", że to jest po prostu natura danej konstrukcji.

C#
interface IProducer<out T> // Covariant
{
    T produce();
}
 
interface IConsumer<in T> // Contravariant
{
    void consume(T t);
}
Kotlin
abstract class Source<out T> {
    abstract fun nextT(): T
}

Scala
trait Function1[-T1,+R] extends AnyRef 

To dlaczego nie tak?

Dlaczego w javie jest to zrobione ala "use-site variance?" . Najprawdopodobniej gdy ten mechanizm powstawał wydawał się doskonałym pomysłem bo java miała jedynie mutowalne kolekcje do których ta maszynka wystarczała. Przypomnijmy - były to lata 2002-2005 -> IE miało 90% udziałów w rynku, ratunku dla branży widziano w tonach xmli a o funkcjach nikt nie myślał. No i w Javie musi być kompatybilność wstecz to jak już zrobili to tak zostało... Jeśli ktoś szuka przykładu długu technicznego to wydaje mi się, że tutaj właśnie go znaleźliśmy.

O nielicznych zaletach i licznej krytyce "Use-Site variance" przeczytacie tutaj -> https://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)#Comparing_declaration-site_and_use-site_annotations. Generalnie jeśli chodzi o funkcje to przewaga "declaration-site variance" wydaje mi się bezdyskusyjna. Bardziej problematyczne są kolekcje. O ile chyba Kotlin ma obydwa typy podejść o tyle np. (znowu z tego co mi wiadomo) scala zrezygnowała z "use-site" na jakimś tam początkowym etapie rozwoju gdyż niewiele to dawało a utrudniało mocno koncepcję auto wykrywania typów dzięki któremu programy w scali mają tone mniej tekstu niż to co reprezentuje sobą java. Zaś co do samych kolekcji należałoby przenieść dyskusje na zupełnie inną płaszczyznę zatytułowaną "algorytmy z wykorzystaniem kolekcji niezmiennych" a tu już jesteśmy o krok od FP vs OOP co może nas doprowadzić do ogólnego wniosku, że problemy z generykami w Javie są pochodną wybranego paradygmatu programowania.

Pocieszenie

Jeśli pracujesz w dobrym ogromnym korpo gdzie kastą rządząca są ludzie biegający z plikiem exceli pod pachą (tzw. "jego EXCELencja") to nie masz się czym martwić bo i tak javy8 pewnie nie zobaczysz przez następne 10 lat. Także głowa do góry!

linki