wtorek, 12 stycznia 2016

Bogaty w Paradygmaty

"Możesz mieć więcej!" - to radosne zdanie możemy usłyszeć w niejednej reklamie atakującej z tv czy internetu. I chociaż sama teza jest dosyć niebezpieczna w świecie programowania bo niekoniecznie chcę mieć więcej linii kodu (a już na pewno pod groźbą odcięcia jajec powinny być zabronione "targety" oparte na ilości linii kodu) to jednak być może dobrze jest mieć więcej "sposobów" pisania tego kodu?

Podobno nasze mózgi są przeładowane informacjami co może tłumaczyc dlaczego proste wyjaśnienia są tak przyjemne i popularne. Był taki moment, że nawet zazdrościłem .NETowcom, że maja tylko jeden powszechnie stosowany framework (nie wiem czy to prawda ale tak słyszałem i tego zazdrościłem) podczas gdy w Javie narzędzi i podejść było "N" co utrudniało życie jeśli chciało się uważać za pragmatycznego bo wtedy musiałeś jakoś uzasadnić dlaczego wybierasz jedno podejście a nie drugie.

W międzyczasie na zupełnie innym poziomie w .NET "wszystko przestało być obiektem" (z tego co słyszałem) podczas gdy programowanie w Javie w wygodny sposób zwalniało nas od myślenia o pewnych sprawach podsuwając "jedyny słuszny paradygmat programowania" (jak lambdy weszły do JAvy8 to ans na grupie .netowcy trolowali, ze w końcu PHP dogoniliśmy).

Jak to zrobić aby było i więcej i lepiej??

Wkrętak uniwersalny?

Powyżej rozkład paradygmatów programowania - jest ich kilka i zazwyczaj nie korzystamy ze wszystkich. Dlaczego?

Z tego co zaobserwowałem większość dyskusji IT o przewadze jednego podejścia nad drugim koncentruje się na pewnych różnicach, które w specyficznej sytuacji ukazują przewagę jednego nad drugim. I tak na przykład ktoś powie ptaki są gorsze od ryb bo średnio sobie radzą na głębokości 10 metrów na to drugi mu przytaknie "że ryby są gorsze bo nie radzą sobie łapaniem planktonu na drzewach". Z drugiej strony każdy z tych gatunków wydaje się mieć pewne zalety, które są wynikiem ewolucji trwającej ileś tam milionów lat (przy sprincie 2 tygodnie policzcie sobie ile musiało być retrospektyw).

Czy podobne zjawiska mogą zajść na gruncie "koncepcyjnym?". Dla przykładu mamy paradygmat1 z ograniczeniem "obiekt ma zmienny stan" i drugi paradygmat2 z ograniczeniem ma być zachowane "referential transparency" (nie wiem jak to łatwo przetłumaczyć ale w skrócie eliminuje to obiekty ze zmiennym stanem i ułatwia bardzo analizę kodu).

I tak co roku na code retreat ćwiczymy podobnie robiąc ograniczenia na zapędy "proceduralne" takie jak "for w forze o tej porze w jeszcze jednym forze i więcej mięsa z dodatkiem if-elsa". I przez to, że ludzie nie mogą robić proceduralnie to w bólach uczą się jak robić obiektowo (jeśli akurat jest Java).

I teraz :

  • Java - od 20 lat robi Code Retreat z ograniczeniem - "robimy programowanie imperatywne" co dla 5% osób, które znam oznacza OOP z dobrymi praktykami a dla 95% tysiące forów zamkniętych w Klasie CosTamManager
  • Haskell - Ileś tam lat rozwija się z ograniczeniem - "ma być referential transparency" - niestety za mało znam ten język i community aby wysilić się na jakiś bardzo śmieszny przykład w tym punkcie.
Czy może teraz jest tak, iż ten pierwszy język dobrze opanował ogarnianie jednego typu sytuacji a drugi ogarnianie innego typu sytuacji? Może... może... może to się przyda w projekcie gdy przyjdzie nam się zmierzyć z obydwoma rodzajami problemów?

Na podparcie tezy cytat z fajnej mądrej książki o programowaniu :

"Adding a concept to a computation model introduces new forms of expres-
sion, making some programs simpler, but it also makes reasoning about programs
harder. For example, by adding explicit state (mutable variables) to a functional
programming model we can express the full range of object-oriented programming
techniques. However, reasoning about object-oriented programs is harder than rea-
soning about functional programs"

W sumie już w Effective Java , która chyba ma z 10 lat jest rozdział który optuje za tworzeniem "niemutowalnych" struktur.

Zanim przejdziemy do dalszych rozważań - jedna ciekawa sprawa. Chociaż przy mojej obecnej wiedzy OOP rozwiązuje głównie problemy zarządzania zmiennym stanem to jednocześnie w wielu książkach o OOP czytałem, ze wszystko jest obiektem i zajebiście da się modelowac/projektować w "obiektówce". No na pewno jak możemy za użyć takich słów jak Customer czy Money to łatwiej przekonać ludzi z pieniędzmi bez wiedzy technicznej, że powinni nam trochę tych pieniędzy dać ale naprawdę OOP nie ma monopolu an rzeczowniki domenowe!

Na przykład taki Haskell - podobno czysto funkcyjny - można takie słówka stworzyć i używać.

data Customer= Customer {firstName::String, lastName::String} deriving (Show)

let c=Customer {firstName="Stefan", lastName:: "Zakupowy"}

Prelude > c
Customer {firstName = "Stefan", lastName = "Zakupowy"}

I od bidy jak jakieś rytuały tego wymagają i UMLa pewnie da się wyrysować. Powąchaj, co czujesz? Money czujesz, profit ku*wa czujesz!

Polimorfizm

Kiedyś polimorfizm wywoływał u mnie "odruch Pawłowa" - polimorfizm<--->dziedziczenie . Jakież było moje zdziwienie, gdy okazało się, że ich jest więcej!

I na przykład takie ad hoc polimorphism. Założmy, że mam pomidory i te pomidory to są takie czyste domenowo pomidory :

case class Pomidor(waga:Int)

val pomidory=Seq(Pomidor(1),Pomidor(4),Pomidor(7))

Jak szybko obliczyć wagę wszystkich pomidorów? Mogę "Ad Hoc" nadać pomidorom nature numeryczną i zwyczajnie je zsumować

implicit val numerycznaNaturaPomidora=new Numeric[Pomidor]{
  override def plus(x: Pomidor, y: Pomidor): Pomidor =  Pomidor(x.waga+y.waga)
  override def fromInt(x: Int): Pomidor = Pomidor(x)
  override def toInt(x: Pomidor): Int = x.waga
  override def toDouble(x: Pomidor): Double = ???
  override def toFloat(x: Pomidor): Float = ???
  override def negate(x: Pomidor): Pomidor = ???
  override def toLong(x: Pomidor): Long = ???
  override def times(x: Pomidor, y: Pomidor): Pomidor = ???
  override def minus(x: Pomidor, y: Pomidor): Pomidor = ???
  override def compare(x: Pomidor, y: Pomidor): Int = ???
}

pomidory.sum  
//res0: Pomidor = Pomidor(12)
//def sum[B >: A](implicit num: Numeric[B]): B

W javie trzeba by jaki interfejs numeric zaimplementować i to jest główna różnica w stosunku do dziedziczenia. No i dopóki do Javy się ograniczałem to nie za bardzo kumałem czym to się różni od "Strategii" - trzeba było mi zerknąć w Haskella.

A co do dziedziczenia to jest to realizacja mechanizmu Single Dispatch i jak się od tej strony podejdzie to łatwiej zrozumieć wady i zalety tego mechanizmu anizeli standardowy przykład, że pies i kod dziedziczą ze zwierzęcia.

Information Hiding

Zazwyczaj w parze z OOP idzie Enkapsulacja i jest to dobra praktyka - ale zastanówmy się skąd ta dobra praktyka wynika i czy obowiązuje tylko w OOP czy też może ogólnie? - a co za tym idzie czy musimy robić OOP by mieć enkapsulację czy też być może można mieć enkapsulację bez OOP?

Otóż jest znowu realizacja bardziej ogólnego konceptu : Information Hiding, który bardziej ogólnie odwołuje się pojęcia modularyzacji. I tak jak np. mamy interfejs List i specyficzną realizację LinkedList to cos sprawia, że to Linked... jest z jakiegoś powodu specyficzne (i błagam kurwa niech się niektórzy zastanowią dlaczego w Javie nie ma IList i ListImpl) analogicznie enkapsulacja OOP ma też w sobie coś specyficznego co drogi czytelniku warto zrozumieć.

Niemniej jednak jeśli chodzi o modularyzacje to Haskell ma moduły, a że to język funkcyjny więc i forma ukrywania informacji w językach funkcyjnych jest!

Jeszcze inna ciekawa sprawa to Pattern Matching. Jest to mechanizm FP (co nie było dla mnie z początku tak oczywiste) i z pozoru rozpościera wnętrzności obiektu po całym programie - ale to jest wizja dosyć błędna wynikająca być może z tego, że np. w Scali Pattern Matching często idzie w parze z "case classes", których nie można do końca utożsamiać z klasami obiektowymi ukrywającymi stan gdyż case class to zwykła forma reprezentacji danych. No a bardziej klasyczną obiektówkę można z "pattern matchingiem" ładnie pożenić.

I tak na przykład mamy taki moduł do gry w karty :

trait Gra[Karta]{
  class Talia (private val karty:List[Karta]){
    def wezZdejmKarte : (Karta,Talia) = (karty.head,Talia(karty.tail:_*))
  }

  object Talia{
    def apply(karty: Karta*)= new Talia(karty.toList)
    def unapply(talia: Talia):Option[Karta] = Some(talia.karty.head)
  }
}

Struktura, która przechowuje karty jest z zewnątrz niewidoczna i pokuszę się o nazwanie tego information hiding. Teraz dokładamy sobie implementację tego modułu gdzie karty to Stringi.

object GraNaStringach extends Gra[String]{
  def maczuj(t:Talia)= t match {
    case Talia("as") => "mam asa"
    case _ => "co innego :("
  }
}

No i jeśli dobrze zrozumiałem całą koncepcję to działa!

import GraNaStringach._
val talia1=Talia("as","walet","dycha")
val (_,talia2)=talia1.wezZdejmKarte

maczuj(talia1) //res2: String = mam asa
maczuj(talia2) //res3: String = co innego :(

Ktoś może zapytać co takiego ma w sobie as, że akurat asa sprawdzamy? Otóż As to jeden z pierwszych polskich Super-Bohaterów!

Abstrakcje

Pojęcie znaczenia abstrakcji jest samo w sobie abstrakcyjne i tutaj jest----> fajna prezentacja o abstrakcjach . Jednak aby za dużo nie odpłynąć to przypomnijmy sobie, że na przykład w Javie częsta dobrą praktyka jest programowanie do abstrakcyjnego interfejsu co daje nam nie raz więcej swobody - i dlatego np. zmienne deklaruje się z typem Collection lub List zamiast od razu obwieszczać "o to mamy LinkedList".

Czasem jednak abstrakcje nie są tak oczywiste i zauważenie ich wymaga skondensowania w nowy sposób z pozoru niezależnych faktów. Być może jest coś wspólnego pomiędzy Optional i CompletableFuture choć z pozoru są od siebie bardzo oddalone? Czy opłaca się czasem zmienić spojrzenie i przekonania na to co "dobre" i "właściwe" w danym kontekście by ujrzeć zupełnie nowe możliwości i odkryć zupełnie nowe podejścia rozwiązywania problemów?

kiedy Dobre jest Niedobre ?

Do tej pory dyskutowaliśmy o tym czy paradygmaty mogą sobie pomóc ale czy mogą sobie przeszkadzać? Czasem zapożyczenie pewnego konceptu z jednego paradygmatu i użyciu go w zupełnie innym kontekście przynosi opłakane skutki co błędnie kończy się skreśleniem konceptu jako takiego.

I tak na przykład często omawiając spierdoliny projektowe dochodzi się do momentu rodem z obiektowego Gmocha "hehe i pojawiły się hehe Utils hehe". I z jednej strony coś w tym jest bo często Utils w projekcie który miał być obiektowy jest świadectwem, ze coś jednak nie wyszło - bo oto mamy wór na luźno powiązane metody, które często jeszcze lubią zmienić sobie jakiś stan w aplikacji i już w ogóle nie wiadomo o co chodzi.

Ale jest taki cierń w oku każdego obiektowego fanatyka, który nazywa się org.apache.commons.lang.StringUtils i działa fenomenalnie? Co tutaj poszło dobrze co nam zazwyczaj nie wychodzi? Wydaje mi się, że w tym umysłowym ograniczeniu "wszystko musi być obiektem" skupiliśmy się na pojęciu spójność u ujęciu takich metryk jak LCOM4, które mierzą spójność obiektu ze względu na interakcje z jego stanem ale jednocześnie nie wiemy dobrze jak zmierzyć specjalizację bezstanowego modułu.

Ukryty Paradygmat

Istnieje jeszcze jeden paradygmat słabo opisany w literaturze nazywany "paradygmat zbiorowego nieświadomym pisaniem chujowego kodu" , który jest skomplikowanym zjawiskiem społecznym, a w którym udział biorą ludzie z całego wachlarza stanowisk korporacyjnych - od góry do dołu. Paradygmat ów stanowi swego rodzaju bramkę, wrota do innych paradygmatów i dopóki nie zostanie okiełznany to w zasadzie wszelkie dyskusje o innych paradygmatach stanowią tylko fantastykę naukową (ewentualnie ów paradygmat można porównać do świątyni na początku fallouta 2 - najpierw trzeba zajebać wszystkie skorpiony a później dopiero można myśleć w którą stronę iść.)

Dyskusje na zakończenie

Wyobraźmy sobie, że mamy dwa paradygmaty poruszania się bieganie i chodzenie. Można by dyskutować, który jest lepszy "jak biegasz to uciekniesz bandytom, - a jak cicho chodzisz to może cie nie zauważą". Ten przykład jest dla nas oczywiście głupi gdyż dotyczy bardzo intuicyjnej codziennej czynności - i dlatego wiemy, że jest głupi!

Programowanie nie jest na tyle intuicyjne dlatego warto najpierw nauczyć się i chodzić i biegać by zrozumieć, że jest bardziej abstrakcyjna forma poruszania się a jej realizacja "zelaży,zależy,zależy od" - i na tym polega bycie inżynierem by wiedzieć od czego zależy. (z drugiej można chyba robić dosyć duży hajs będąc tzw. Evangelistą jakiejś jednej konkretnej rzeczy, także znowu mamy abstrakcyjny koncept "tożsamość w IT" i chociaż bardzo nie chcieliśmy to znowu wpadliśmy w pułapkę "wiedzenia lepiej" - dop. autora)

I jeszcze taka ciekawostka : ponieważ w świecie JVM mamy pewne skrzywienie polegające na tym, że przez ponad dekadę jedynym uznawanym paradygmatem było "chodzenie" (chociaż wiele osób tka naprawdę robiło fikołki - patrz sekcja "ukryty paradygmat") to tak dla równowagi można trochę tak w zdrowych ramach pochejtować i potrolowac to jedyne słuszne "chodzenie" - wiecie tak tylko dla równowagi ale nie można za bardzo odlecieć.

A i jeszcze taki fajny cytat z książki, która jako bazę wiedzy traktuje FP i przechodzi do OOP jako czegoś dodatkowego- tak na łapanie perspektywy : "(...)A function with internal memory is usually called an object(...)"

***

6 komentarzy:

  1. Referential transparency to "przejrzystosc referencyjna" https://pl.wikibooks.org/wiki/Programowanie/Programowanie_funkcyjne

    Takich artykulow dokladnie potrzeba w tej chwili zamiast typowych flejmow w stylu wujka Boba "All state is evil. Fuck OO"
    Nalezy sklaniac ludzi w strone kompozycji roznych paradygmatow a nie wywolywac u nich obrzydzenie jednym i bezwzgledna milosc wobec drugiego.
    Bede szerowal.
    Pozdro!

    OdpowiedzUsuń
  2. Na początku chciałem się tu rozpisać i o utilsach i o dziedziczeniu, ale daruje sobie i napiszę to co jest najważniejsze w tym artykule: łączenie paradygmatów.

    Generalnie idea, która przyświeca temu jest taka, że zapożyczamy to co fajne, nawet jeśli nie do końca pasuje. I z jednej strony sam lubię programowanie funkcyjne, ale z drugiej strony podniesie się wiele głosów, że wdrażanie rozwiązań ze Scali do Javy to zaśmiecanie języka imperatywnego.
    Przekładając to na inne dziedziny życia bądź ogólnie na wszechświat: ogony są fajne - spójrzcie ile dają one kotom! A chcielibyście biegać z ogonami na co dzień, tylko dlatego że przydają się przy spadaniu?

    Na koniec taki tweet od Michaela Fogusa: "The best programmers I've ever met had an odd balance of an ability to cope with current tech and a fervent unwillingness to do so."

    OdpowiedzUsuń
    Odpowiedzi
    1. Niestety nie za bardzo zczaiłem co chcesz powiedzieć :)

      Usuń
    2. Chciałem powiedzieć, że mam obiekcje co do mieszania paradygmatów, które jest dość modne. Z drugiej strony, sam chętnie z tej opcji korzystam. Czy czyni mnie to hipokrytą? A może po prostu leniem? (chyba bardziej to drugie).

      Nawiązując do Twojego zakończenia: czy lepiej chodzić czy biegać? Odpowiedź brzmi: chodź po cichu, ale jak już Cię zauważą, to biegaj! Problem w tym, że w programowaniu nie zawsze tak to działa. Jako przykład weźmy monady.
      Każdy z nas, zwłaszcza na początku kariery, gdy nie znał zbyt wielu opcji na pewno naciął się na następujący problem: coś napisałem, teraz walnę w środku wywołanie funkcji, która coś mi zwróci i gotowe. I wtedy okazuje się, że zrobienie tej funkcji w środku, aby zwróciła to co byśmy chcieli nie jest proste ani ładne.
      I nagle przychodzi nowa Java i mówi nam: macie tu monady. My zapoznajemy się z tematem, ogarniamy z grubsza o co chodzi i wrzucamy monadę, bo "to takie fajne" i "rozwiązuje nasz problem". Ale za chwilę okazuje się, że nasza monada (np. CompletableFuture) wymusza albo powrót do blokowania wątku (bo musimy poczekać na wynik), albo ponowną zmianę klasy obiektu zwracanego przez funkcję (z klasy 'A' do 'CompleatableFuture\') co skutkuje zaśmieceniem monadami całego stacka.
      Oczywiście nie neguję wszystkich monad z paczki, czy też rozwiązań w stylu lambda, bo są po prostu przydatne, choć np. debugowanie lambdy - kto robił wie co jest w tym nie fajnego.

      Podsumowując, i w kontrze do tego co napisał Julian: Kompozycja paradygmatów mi się nie podoba. Jeśli chcesz użyć różnych rzeczy, bo Twoja aplikacja robi różne rzeczy może lepiej ją rozbić na mikroserwisy i napisać je w różnych językach? Programowanie imperatywne (np. w Javie) opiera się o kluczową zasadę "pull > push", zaś inne paradygmaty mogą się z tą zasadą nie zgadzać. Wtedy zacznie się nam heca z debugowaniem, monitorowaniem czy profilowaniem.

      Usuń
    3. Jeśli piszesz na bazie swoich doświadczeń to można założyć dwie rzeczy - albo masz racje albo twoja baza doświadczeń jest zbyt mała - trudne do zweryfikowania ;)

      Usuń
    4. Piszę głównie na bazie zapoznawania się z tematem na prezentacjach lub opinii innych. Własne doświadczenia w tym temacie też mam, ale mało, jak słusznie zauważyłeś.
      Temat jednak uważam za bardzo interesujący i warty dyskutowania.

      Usuń