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