wtorek, 26 kwietnia 2016

Nowość radykalna

"Far be from us, Sire, the dangerous novelty of thinking."

W szkole średniej na kierunku "język polski - specjalizacja systemy komputerowe" (pisanie wypracowań na polski zabierało z 3-4 razy więcej czasu niż praca z jakimikolwiek komputerami - z perspektywy czasu trudno mi ocenić czy to dobrze czy źle) często musieliśmy analizować słownie lub pisemnie teksty dawnych twórców.

W programowaniu także mamy "dawnych twórców" i ich teksty, które gdzieś tam czekają na odkrycie (kiedyś mówiło się, że zakurzone pergaminy leżą gdzieś w szufladzie a dziś można napisać, że niezakurzone PDFy czekają na odkrywcę gdzieś na drugiej stronie wyników z googla). I dziś własnie taki bardzo ciekawy moim zdaniem tekst chciałbym pokazać : "On the cruelty of really teaching computing science" - prof. dr. Edsger W. Dijkstra

Oczywiście każdy może sobie coś napisać w necie i podpisać to Dijekstra albo Pałlo Kohelio ale generlanie ów tekst jest wspominany na wielu innych stronach i doczekał się własnych opracowań (chociaż i nie takie rzeczy koledzy od pozycjonowania potrafią zrobić)

mentalna cela metafory

Generalnie artykuł cały nadaje się do zacytowania dlatego zalecam jego samodzielną lekturę - tutaj jedynie chciałbym się skupić na jednym bardzo ciekawym poruszonym tam aspekcie - nowości radykalnej

Jest to o tyle ważne, że dosyć często mamy taką oto groteskową sytuację :

Dużo oprogramowania powstaje w firmach, które zbudowane są w oparciu o tzw. "piramidę władzy" gdzie sformułowanie "N raportuje do M" oznacza, że niejako M ma nadaną odgórnie "moc decyzyjną" i "deleguje" a N w raportach mówi jak te delegacje/targety realizuje. A, że głównym nośnikiem energii w tychże instytucjach jest hajs zwany "budżetem" - toteż mamy sytuację, gdzie przenika się kilka dziedzin wiedzy. A sami z własnych obserwacji możemy pomyśleć ile znamy osób, które opanowały w sposób profesjonalny chociaż by jedną dziedzinę nie mówiąc o kilku...

No i zazwyczaj ci ludzie co mają budżet i stoją wyżej w piramidzie nie mają wiedzy technicznej ale jednocześnie instytucja w której pracują wymaga od nich aby wzięli odpowiedzialność za realizację zadań w poziomach poniżej. Jest to temat rzeka. W każdym razie ze względu na złożoność zagadnienia - jakim jest tworzenie programu - przekaz musi być uproszczony do form znanych temu co ma budżet i pojawiają się zubożone porównania, z których jednym z najbardziej debilnych jest "budowanie systemu to budowanie domu a programiści to murarze"

Nowość radykalna

I tutaj czas aby wspomóc się tekstem Dijekstry, w którym opisuje skąd biora się te metafory i jakie są z nimi problemy.

"The usual way in which we plan today for tomorrow is in yesterday's vocabulary(..)Of course, the words and the concepts don't quite fit because our future differs from our past, but then we stretch them a little bit(...)by means of metaphors and analogies we try to link the new to the old, the novel to the familiar. Under sufficiently slow and gradual change, it works reasonably well; in the case of a sharp discontinuity, however, the method breaks down..."
"the analogies become too shallow, and the metaphors become more misleading than illuminating"

W ramach jakiej metafory pracujemy na co dzień? Słowo klucz to Architektura i nazwa stanowiska Architekt - skąd one się wzięły?. Za wikipedią

"Architektura (łac. architectura, od architector – buduję)– sztuka i technika budowania w celu realizacji praktycznych i artystycznych wymagań ludzi cywilizowanych. Wyróżnia się architekturę miejską i wiejską a także sakralną i świecką (obronną, mieszkalną, budowli gospodarczych, budowli ogrodowych, budowli użyteczności publicznej)"
Możecie sobie sami wyszukać stronkę. O komputerach tam za dużo nie ma.

No dobra idziemy dalej - jak wpiszemy "Architektura komputera" to naszym oczom ukaże się taka definicja :

"Architektura komputera – sposób organizacji elementów tworzących komputer. Pojęcie to używane jest dosyć luźno. Może ono dzielić systemy komputerowe ze względu na wiele czynników, zazwyczaj jednak pod pojęciem architektury komputera rozumie się organizację połączeń pomiędzy pamięcią, procesorem i urządzeniami wejścia-wyjścia."
"Pojęcie to używane jest dosyć luźno" - dlaczego luźno? Może dlatego, że jest ono zapożyczone z zupełnie innej nie do końca pasującej dziedziny?

No ale to co nas najbardziej interesuje : https://pl.wikipedia.org/wiki/Architektura_oprogramowania

"Architektura oprogramowania – podstawowa organizacja systemu wraz z jego komponentami, wzajemnymi powiązaniami, środowiskiem pracy i regułami ustanawiającymi sposób jej budowy i rozwoju.

Opis architektury oprogramowania (ang. Software Architecture Description) postrzegany jest jako platforma porozumiewania się wszystkich osób zaangażowanych w proces wytwórczy systemów informatycznych.

Architektura oprogramowania jest stosunkowo młodą dziedziną informatyki i że choć w ostatnich dwóch dekadach dość mocno się rozwinęła i nawet osiągnęła poziom dojrzałości, to nadal trwają dyskusje nad jej miejscem w informatyce, a przede wszystkim nie ma zgody szerokiego gremium w zakresie określenia samej definicji architektury oprogramowania."
Jest trochę takiego bełkotu w stylu "platforma porozumiewania się wszystkich osób zaangażowanych w proces wytwórczy systemów informatycznych". Ale najważniejszy jest fragment o tym, że tak naprawdę nie ma definicji czyli nie wiadomo co to tak naprawdę jest!

- Marzena dostałem awans!
- super, kim teraz będziesz?
- architektem IT
- a co taki architekt robi?
- Nie wiadomo bo nikt przez ostatnie 40 lat tego nie zdefiniował!


Oczywiście nie mam tutaj racji bo korpo mają bardzo ładnie zdefiniowaną przez hry ścieżkę kariery (rym! jak się ostatnie zdanie odpowiednio wypowie jest rym!) i sam wdziałem na stronie - takie ładnej z profesjonalnymi stylami - że do obowiązków architekta należy spędzanie 40% na meetingach (autentyk, to jest ku*wa autentyk!!!).

W każdym razie wracając do metafor to podobno jakieś kluczowe rzeczy zadziały się pod koniec lat sześćdziesiątych - były dwie duże konferencje - zdaje się, że sponsorowane przez NATO (ciekawe czy mieli stoisko z napisem "we are hiring") i tam się te terminy jak "Architektura systemu informatycznego" czy "metodyka wodospadu" pojawiły. Ogólnie ciekawe czasy to musiały być. W lekturze często spotyka się termin "software crisis" odnośnie tamtych czasów - jeśli wtedy był kryzys to co my niby teraz mamy!?!

Inne metafory

Ogród - to się pojawia co jakiś czas w różnych artykułach a kolega miał nawet w stopce "Software Gardener". W sumie jak zostawimy budynek tak sam sobie na jakiś czas to zacznie się on rozlatywać - jak zostawimy program tak sam sobie - no to nic się w sumie nie dzieje (no dysk za pinset lat się rozleci ale to już tzw. "life span" przekracza) - no i domu nie można skopiować bezkosztowo!. Natomiast tzw. dług techniczny (który jest kolejną metaforą!) można porównać do chwastów.

Inna ciekawa metafora - Programming as Theory Building. Kawałki programu jako dowody, które wspierają wyprowadzanie nowych dowodów.

Generalnie gdyby tak się temu przyjrzeć co robimy to słowo komputer pochodzi od angielskiego computer i generalnie kiedyś to był tzw. Human Computer.

The term "computer", in use from the early 17th century (the first known written reference dates from 1613), meant "one who computes":
"Who computes" a to compute znaczy "obliczać". I nagle w kontekście obliczeń najpierw wykonywanych ręcznie, później na lampach i w końcu krzemie - gdzieś tutaj nagle pojawiają się analogie do budownictwa... oj musiało być ostre jaranie na tych konferencjach ... albo LSD! w sumie wtedy LSD chyba było legalne - co jeśli korzenie obecnych dogmatów branży IT tkwią w zajebistej imprezie LSD 50 lat temu!!!

"Inne fajne cytaty"

"A number of these phenomena have been bundled under the name "Software Engineering". As economics is known as "The Miserable Science", software engineering should be known as "The Doomed Discipline", doomed because it cannot even approach its goal since its goal is self-contradictory. Software engineering, of course, presents itself as another worthy cause, but that is eyewash: if you carefully read its literature and analyse what its devotees actually do, you will discover that software engineering has accepted as its charter "How to program if you cannot."
"one has to approach the radical novelty with a blank mind, consciously refusing to try to link it with what is already familiar, because the familiar is hopelessly inadequate."
The practice is pervaded by the reassuring illusion that programs are just devices like any others, the only difference admitted being that their manufacture might require a new type of craftsmen, viz. programmers. From there it is only a small step to measuring "programmer productivity" in terms of "number of lines of code produced per month". This is a very costly measuring unit because it encourages the writing of insipid code, but today I am less interested in how foolish a unit it is from even a pure business point of view. My point today is that, if we wish to count lines of code, we should not regard them as "lines produced" but as "lines spent": the current conventional wisdom is so foolish as to book that count on the wrong side of the ledger.
Na szczęście ilość linii kodu wyleciała (chyba wszędzie) z targetów zwanych "Kej Pi Ajem (KPI - Key performance coś tam)" ale fakt, że taka patologia miała miejsce doskonale pokazuje skalę niezrozumienia tej dziedziny jaką jest programowanie.
"Again, I have to stress this radical novelty because the true believer in gradual change and incremental improvements is unable to see it. For him, an automatic computer is something like the familiar cash register, only somewhat bigger, faster, and more flexible. But the analogy is ridiculously shallow: it is orders of magnitude worse than comparing, as a means of transportation, the supersonic jet plane with a crawling baby, for that speed ratio is only a thousand."
O różnicy pomiędzy codziennym życiem analogowym i nową formą cyfrową :
"To this I should add that, to the extent that we view ourselves as mechanisms, we view ourselves primarily as analogue devices: if we push a little harder we expect to do a little better." (...) "it has unavoidably the uncomfortable property that the smallest possible perturbations —i.e. changes of a single bit— can have the most drastic consequences"
"also (...) quality control continues to be distorted by the reassuring illusion that what works with other devices works with programs as well. It is now two decades since it was pointed out that program testing may convincingly demonstrate the presence of bugs, but can never demonstrate their absence. After quoting this well-publicized remark devoutly, the software engineer returns to the order of the day and continues to refine his testing strategies, just like the alchemist of yore, who continued to refine his chrysocosmic purifications."
"What is a program? Several answers are possible. We can view the program as what turns the general-purpose computer into a special-purpose symbol manipulator, and does so without the need to change a single wire (This was an enormous improvement over machines with problem-dependent wiring panels.) I prefer to describe it the other way round: the program is an abstract symbol manipulator, which can be turned into a concrete one by supplying a computer to it."
"It really helps to view a program as a formula. (...) it puts the programmer's task in the proper perspective: he has to derive that formula. (...) he has to derive that formula, he has to derive that program. We know of only one reliable way of doing that, viz. by means of symbol manipulation. And now the circle is closed: we construct our mechanical symbol manipulators by means of human symbol manipulation."
"Hence, computing science is (...) the interplay between mechanized and human symbol manipulation, usually referred to as "computing" and "programming" respectively. An immediate benefit of this insight is that it reveals "automatic programming" as a contradiction in terms. A further benefit is that it gives us a clear indication where to locate computing science on the world map of intellectual disciplines: in the direction of formal mathematics and applied logic, but ultimately far beyond where those are now, for computing science is interested in effective use of formal methods and on a much, much, larger scale than we have witnessed so far."

Podsumowanie

"Well, when all is said and done, the only thing computers can do for us is to manipulate symbols and produce results of such manipulations. From our previous observations we should recall that this is a discrete world and, moreover, that both the number of symbols involved and the amount of manipulation performed are many orders of magnitude larger than we can envisage: they totally baffle our imagination and we must therefore not try to imagine them."

No to był jeden z tych trudnych odcinków, gdzie coś trzeba skrytykować by pobudzić czytelnika do myślenia. Mamy zdefiniowany jakiś mainstream - dyskutuje się w ramach mainstreamu ale mało dyskutuje się z mainstreamem. Tworzymy w naszych umysłach model rzeczywistości gdzie jedne tezy opierają się na innych i te na innych i tak dalej aż do momentu gdzie przestajemy drążyć głebiej i przyjmujemy wytłumaczenia takimi jakie są.

To ciekawe czy ludzie żyjący w czasach rewolucji naukowo/kulturalnych wiedzieli, że żyją w czasach rewolucji naukowo/kulturalnych? Czy żyjemy w takich czasach? Jeśli tak to czy nie warto przyjrzeć się bliżej osadzonym wiele lat temu fundamentom obecnych przekonań?

"Because no endeavour is respectable these days without a TLA (= Three-Letter Acronym), I propose that we adopt for computing science FMI (= Formal Methods Initiative)"

poniedziałek, 18 kwietnia 2016

Lista nieoczywista

To jest taki pierwszy z brzegu obrazek procesora ściągnięty z netu. Po co on tutaj jest? Ano aby pokazać, że wszystkie dyskusje o paradygmatach programowania sprowadzają się w ostateczności do tego jakie napięcie pojawi się o tam na tych "nuszkach".

Dla procesora te dyskusje z cyklu, który język lepszy itd są niezrozumiałe bo on widzi i tak tylko 10100010111 czy coś podobnego. Zaś dla ludzi sytuacja jest inna. Dla ludzi poczciwych jak ty i ja być może bardziej abstrakcyjne spojrzenie pomoże ogarnąć szersze przestworze cyfrowych konstrukcji. W końcu po to są metafory aby rozumieć coś nowego w kontekście czegoś starego. A jeśli metafora pomoże zmniejszyć ilość spalonych dolarów na produkcji to tym lepiej. To zobaczmy pierwszą metaforę dla Listy - dla czegoś czego używamy od zawsze i zazwyczaj sie kojarzy z przypadkiem "Lista produktów" albo Lista Userów, a teraz inaczej...

Lista jako alternatywa dla...

Option lub Optional lub Maybe w zależności od języka. Dokładnie tak. Bardzo często spotykam się z porównaniem Option do takiego obiektowego-ifa czy czegoś w tym stylu. Raczej mało osób kojarzy wchodzi rozmyślaniami w system typów i konsekwencje sprowadzenia opcjonalności do wyniku obliczeń

Krótka notka odnośnie nulla i Optional. W przypadku zapoznawania kogoś z koncepcją Optional w Javie8 pojawia się czasem pytanie "no ale co gdy Optional będzie nullem". Wtedy mamy do czynienia z objawem debilizmu lub geniuszu - już tłumaczę. W Javie7 null jest prostym sposobem zasygnalizowania, że czegoś nie ma i ten null może to być częścią interfejsu jak to :"jak nie będzie wyniku to zwracam nulla" - podobno po to ten null powstał. Gdy już mamy do dyspozycji Optional , który jednocześnie jawnie sygnalizuje "możliwośc braku" a umiejętnie użyty oszczędza nam do tego narzutu checked-exception - to wtedy zwracanie nulla gdzie w typie jest Optional jest oznakiem niedowładu mózgu ... albo wyjątkowej wiedzy bo podobno Optional w Javie8 wcale miał nie być takim Optional jako Optional bo nie jest serializable, puste czy nie puste to jedna klasa a architekci z oracla podobno toczą pianę jak ktoś wspomni, że chciałby użyć tego jako typu pola w klasie. Ale mimo wszystko - jak już w sygnaturze metody ktoś dał Optional a ktoś inny zwrócił null - to raczej mamy do czynienia z objawą szaleństwa aniżeli geniuszu.

Natomiast jest to trochę niecodzienna percepcja Optionala widzieć go jako taką małą Listę - albo w drugą stronę - percepcja Listy jako takiego dłuższego Optionala . Po raz pierwszy się z tym spotkałem w kursie FP in Haskell gdzie Erik Meijer wspomniał, że lubi używać List zamiast Maybe (w haskellu jest Maybe a zamiast Some jest Just - dzięki czemu można stosować polski żart Just(5) - ale Gimby pewnie nie znajo...) bo w Maybe może sobie przechować tylko jedną potencjalną wartość - i to jest dosyć ciekawe Lista jako struktura przechowująca n opcjonalnych wartości.

Taki kodzik sobie zobaczmy :

type HTML=String

case class User(login:String, email:String)

val extractEmail:User=>String= _.email
val displayEmail: String => HTML = email =>
      s"""
         |<h1> EMAIL </h1>
         |<span> ${email} </span>
       """.stripMargin

Jest tam i User (byśmy mogli rzec, iż jakoś "biznesowo" jest) jak i dwie banalne pokazowe funkcje aby zbudować mały ciąg przekształceń. I patrz, i patrz jak ten kod jest do siebie podobny :

//option
def findUser(login:String):Option[User]=Some(User("Roman","donjuan17@wp.pl"))
findUser("Roman").map(extractEmail).map(displayEmail).getOrElse("""<h1>Nie ma takiego zioma</h1>""")

//list
def findUserList(login:String):List[User] = List(User("Roman","donjuan17@wp.pl"))
findUserList("Roman")
.map(extractEmail).map(displayEmail).headOption.getOrElse("""<h1>Nie ma takiego zioma</h1>""")

taka nowa ciekawa perspektywa co nie? Lista jako 0..n opcjonalnych elementów. Ale można na Listę spojrzeć jeszcze z innej strony.

Efekt - ale czego ?!?

Mamy takie dwie proste funkcje :

val addThree:Double=>Double= _ +3
val timesTwo:Double=>Double = _*2

Możemy je zastosować :

W kontekście efektu opcjonalności :

Option(1.0).map(addThree).map(timesTwo)

W kontekście efektu błędu :

Try(1.0).map(addThree).map(timesTwo)

W kontekście efektu czasu :

Future(1.0).map(addThree).map(timesTwo)

I mogę sobie coś takiego zastosować na liście :

List(1.0).map(addThree).map(timesTwo)

Składnia podobno ale czy to jest jakiś efekt ta lista? A jeśli jest to efekt to efekt ku*wa czego?

A wybierz se co chcesz

y=f(x)

Co jest tak niesamowitego w tej linijce powyżej, że ma własne formatowanie i italica? Otóż funkcja aby była funkcją musi mieć jeden wynik. No i zazwyczaj ma - findUser i jest user. albo 2+2=4 ale jaki jest wynik czegos takiego : 0=x^2-4 ?

Math.pow(2,2)-4
Math.pow(-2,2)-4

Nie ma jako takiego "jednego rozwiązania" - gdybyśmy chcieli znaleźć wszystkie możliwe rozwiązania (nie licząc krzywych wymiarów zapętlonie-zespolonych) to kod moglibyśmy napisać tak :

def allSqrts(v:Int)=List(-Math.sqrt(4),Math.sqrt(4))
allSqrts(4) //res12: List[Double] = List(-2.0, 2.0)

I cały dowcip polega na tym, że tam gdzieś jest dobra odpowiedź ale ja nie wiem która - i w sumie przez chwilę nie chcę wiedzieć.

allSqrts(4).map(addThree).map(timesTwo) //List[Double] = List(2.0, 10.0)

Może wezmę pierwszy, może większy, może mniejszy - to zależy od kontekstu (jak zwykle). A te wszystkie metody z rodziny fold czy reduce, które np. liczą sume zysku to tak abstrakcyjnie patrząc wszystkie polegają na tym, aby przejść od stanu nie-determinizmu do pewnej określonej wartości.

Czyli patrząc na coś takiego :

sealed trait SchrodingerCat
case object Alive extends SchrodingerCat
case object Dead extends SchrodingerCat

val catStates=List(Alive,Dead)

Może warto przeredagować paragraf z poprzedniego punktu =>

W kontekście efektu braku determinizmu :

List(1.0,2.0,3.0).map(addThree).map(timesTwo)

niedziela, 3 kwietnia 2016

Patrz Panie jakie fajne wstrzykiwanie

To co widzicie poniżej to stos.

Zanim stos został użyty w programowaniu do przechowywania argumentów funkcji służył jako ostateczne narzędzie do wygrywania sporów. Np. jak ktoś zadawał niewygodne pytania :

- ziemia jest płaska i leży na 4 słoniach!
- a na czym stoją te słonie?
- na ogromnym żółwiu
- a ten żółw na czym stoi?
- a tam już jest piekło!
- no ale jak ten żółw stoi tam na tym piekle?
- a co ciebie interesuje tak to piekło?!?! Diabeł ludzie diabeł! na stos z nim !!

I w ten sposób kolejna dyskusja na forum została wygrana a mainstream nie zmieniał się przez kilkaset lat.

Niestety w programowaniu mamy także zestaw niepodważalnych reguł w których kontekście się dyskutuje ale "o których się nie dyskutuje". I tak generalnie tezą, za którą można stracić miejsce w lokalnym klubiku szachowo-obiektowym byłoby "Bo czasem jak ustawisz pole jako 'public final' - no wiesz tak bez getera to też będzie dobrze a czasem nawet lepiej".

Tydzień temu starałem się pokazać ( Tutaj jest wpis sprzed tygodnia) , że metody można nie tylko wywoływać na instancji przekazując argument - co jest żelaznym ograniczeniem w klasycznej Javie - ale również można wywołać akcję wpierw określając argumenty a później decydując się jakiej instancji ma to wywołanie dotyczyć. Taka operacja otwiera przed nami szereg nowych możliwości, które jeśli ktoś ciekaw są opisane w tamtym artykule.

Dzisiaj z kolei zobaczymy co się stanie jeśli złamiemy założenie najpierw wstrzykuję zależność a później ją wykorzystuję. Dokładnie - najpierw wykorzystamy zależność a na samym końcu określimy czym ta zależność jest. Mam nadzieję, że już trochę ryje czaszkę i zachęca zdanie poprzednie do dalszej lektury.

Klasycznie

W "klasycznym" wstrzykiwaniu będziemy mieli jakiś serwis (być może z 10 adnotacjami) do którego będzie wstrzyknięte repozytorium (być może też z 10 adnotacjami). I zrobimy sobie jakąś taką małą symboliczną domenkę w kontekście, które jest użytkownik i on może mieć status

I własnie taką akcją mała i symboliczną na potrzebę tego artykułu będzie zmiana statusu z NEW na CONFIRMED

No i standardowo serwis przyjmie żądanie, wyciągnie usera, coś w nim zmieni i zapisze. Rezultatem zaś będzie id nowej wersji usera .Kodzik - po raz kolejny podkreślę, że na potrzeby edukacji symboliczny - może wyglądac tak:

case class UserId(id:Int) extends AnyVal
case class User(login:String,status:String)

trait Repository{
    def find(id:UserId):User
    def save(u:User):UserId  
}

class Service(repository:Repository){
    def confirUser(id:UserId):UserId={
      val user = repository.find(id)
      val confirmed = user.copy(status = "CONFIRMED")
      repository.save(confirmed)
    }
}

Teraz tak chwilę zastanówmy się czym charakteryzowała będzie się praca z tym kodem i czy pewne aspekty tej pracy nie będą nam przeszkadzać :

  • Test będzie wymagał zmockowania Repozytorium, ustawienia, że ma zwracać takiego a takiego usera i będziemy musieli jakoś przechwycić argument, który poszedł do save by sprawdzić, że status faktycznie jest zmieniony na "CONFIRMED"
  • Chociaż możemy kawałek ze zmianą na "CONFIRMED" wynieść do osobnej metody, która przetestujemy (w sumie dobry pomysł) - to jednocześnie nie da się łatwo sparametryzować tego szkieletu :

    1) repository.find(id)
    2) Przekształć Usera <- ten kawałek chcę sobie skonfigurować
    3) repository.save(newUser)
  • Nawet można by pójść z ta "Kastomizacją" krok dalej

    1) Zrob se cos na repository i dostaniesz usera <- a co dokładnie to pomyśle później
    2) Przekształć Usera <- a co dokładnie to pomyśle później
    3) I zrób se cos z tym nowym Userem na Repozytorium <- iiiiii zgadnij, tak przekładam tę decyzję w kodzie na chwile gdy będę miał więcej informacji

Dobra popili, pojedli to teraz ...

Funkcyjnie ale nada klasycznie

Gdybyśmy chcieli zapisać wspomniane obliczenia jako typ funkcji to było by coś w stylu Repository => UserId => User czyli najpierw ustawiam repozytorium i dostaje funkcję, która userId przekształci w Usera.

  //KLASYKA
val findUserClassic:Repository => UserId => User = repository => id => repository.find(id)
val injectedRepository = findUserClassic(MockRepository)
val user = injectedRepository(UserId(1))

No i fajnie. Na tym można by skończyć pisanie posta z wnioskiem "w FP też można DI!" i w sumie pogoda jest ładna a siłka też otwarta to lecimy. ALE... ale teraz dopiero zaczną się dziać ciekawe rzeczy...

Złamać Intuicje

Odwróćmy ciąg przyczynowo skutkowy :

val findUser : UserId=>Repository=>User = id=>repository=>repository.find(id)

Czy dokładnie widać co się stało? Na wszelki wypadek jeszcze rysunek.

OTÓŻ - Wstrzyknęliśmy argument zanim jeszcze wstrzyknęliśmy repozytorium! Oczywiście powstaje pytanie - "ale po co?"

Za chwile zobaczymy jak niezwykle ciekawe możliwości otwiera przed nami ten zabieg ale jeszcze dla kwestii edukacyjnych wprowadźmy taki wygodny alias dla funkcji Repository => A

type Reader[A] = Repository=>A
//albo bardziej "javowo"  type Reader[A] = Function[Repository,A]

Przyjść na gotowe

No dobra pierwsza obserwacja - kiedy najpierw wstrzykiwaliśmy repozytorium to w testach niejako musieliśmy tworzyć Serwis od nowa z Mockami. Teraz w zasadzie wszystkie parametry obliczenia są określone poza jedynym który może się wywalić czyli Repozytorium. Dlaczego "jedynym, który może się wywalić" ? Ponieważ repozytorium wygląda na jedyny moment kodu gdzie łamane jest "Referential Transparency" czyli gdzie wynik obliczeń jest zależny od jakiegoś stanu zewnętrznego.

A ponieważ dostaliśmy formę Repository=>A to potencjalna zjebka jest odłożona w czasie do momentu decyzji jak ten efekt uboczny najlepiej ogarnąć.

//wczesniejszy kawąłek kodu - tutaj raczej wszystko jest pod kontrolą
val findUser1: (Repository) => User = findUser(UserId(1))

//późniejszy kawałek kodu, tutaj postanawiamy robić test to wstzrykniemy sobie Mock
findUser1(MockRepository)

//ale równie dobrze już działac produkcyjnie
findUser1(ProductionRepository)

No i fajnie ale nadal to wygląda jak taki trochę bajer tylko. Dobra to idziemy dalej...

Operacje domenowe

Mamy taką sobie skromną operację domenową, o której wspominaliśmy wcześniej czyli ustawienie usera na "CONFIRMED"

val confirmUser:User=>User= u => u.copy(status = "CONFIRMED")


(moglibyśmy napisać "mamy taką małą łatwą w użyciu funkcje" ale jak odmieniamy słowo "Domena" i "Biznesowe" to jest tak no bardziej profeszional)

No i teraz możemy podejść do problemu, który poruszyliśmy przy okazji implementacji "klasycznej" - czyli kompozycji dwóch niezależnych od siebie kawałków kodu. Ponieważ pierwsza funkcja ma typ Repository=>User a druga User => User to można by zrobić standardowe andThen lub compose jednakże tutaj przygotujemy pewną funkcję "mapującą", która przyda się później.

def mapR[B,C](reader:Reader[B])(f:B=>C): Reader[C] =  reader andThen f
def mapf[A,B,C](base:A=>B)(f:B=>C): A=>C =  base andThen f


  • mapR[B,C] - to jest sygnatura funkcji, która wykorzystuje nasz alias type Reader[A] = Repository=>A
  • mapf[A,B,C] - to dla porównania bardziej generyczna funkcja, która nie jest ograniczona do Readera i może zmapować dowolną funkcję A=>B do A=>C

Poniższa funkcja confirmUser1 już nie tylko czyta usera o id=1 z repozytorium, które jeszcze nie jest określone ale już ustawia status na "CONFIRMED".

val confirmUser1: (Repository) => User = mapf(findUser1)(confirmUser)

Możemy testować niezależnie :
  • funkcję czytającą Usera
  • funkcję ustawiajaca status "CONFIRMED"
  • funkcję wyższego rzędu mapującą jedną funkcję prostą przy pomocy innej funkcji prostej
I w zasadzie testując te trzy składniki mamy pewność, że ich kompozycja również działa : mapf(findUser(UserId(1)))(confirmUser). Ale to cały czas jest naciągane bo ten sam efekt można również uzyskać wstrzykując klasycznie najpierw repo a później argument. Otóż siła tej konstrukcji pojawi się w sytuacji gdy ponownie będziemy chcieli użyć Repozytorium do zapisu.

FLAT MAP

Będziemy chcieli zrobić coś takiego :

I tutaj może być taki trochę MINDFUCK moment. Generalnie flatMap ma sygnaturę A=>F[B] gdzie F to będzie jakiś typ wyższego rzędu np A=>List[B] albo A=>Option[B]. Więc w naszym przypadku to będzie A=>Reader[B] a ponieważ Reader[B] to funkcja Repository=>B to flatMap w tym przypadku (teraz będzie ten moment) A=>Repository=>B

def flatMapR[B,C,D](reader:Reader[B])(f:B=>Reader[C]) : Reader[C] = a=> f(reader(a))(a)
def flatMapf[A,B,C,D](base:A=>B)(f:B=>(A=>C)) : A=>C = a=> f(base(a))(a)

I znowu mamy dwie funkcje do porównania. Generyki w wersji z Readerem oznaczyłem od B aby zachować podobieństwo. Jaka funkcja spełnia ten warunek : f:B=>Reader[C]. Jedną już widzieliśmy :

val findUser : UserId=>Repository=>User = id=>repository=>repository.find(id)

To teraz czas na kolejną :

val persistUser : User=>Repository=>UserId = user => repository => repository.save(user)

I teraz możemy sobie skomponować caluśką operacyję :

val databaseOperation: (Repository) => UserId = flatMapf(mapf(findUser(UserId(1)))(confirmUser))(persistUser)
databaseOperation(MockRepository) //nowe Id zapisanego usera

Ktoś może powiedzieć, że to wygląda trochę nieczytelnie. Będzie on w błędzie bo to wygląda bardzo nieczytelnie ale mamy na to patent!

Pora na FORa

Teraz zaznamy słodyczy języka Scala. Najpierw w postaci niejawnej konwersji :

implicit class FunctionTransformation[A,B](base:A=>B){
    def map[C](f:B=>C):A=>C = base andThen f
    def flatMap[C](f:B=>A=>C):A=>C = a=>f(base(a))(a)
}

która umożliwi taki zapis :
val program: (Repository) => UserId = findUser(UserId(1)).map(confirmUser).flatMap(persistUser)
program(MockRepository)

Przy minimalnej znajomości składni scali ten kod już jest czytelny ale można jeszcze (być może) lepiej.

val forResultProgram: (Repository) => UserId = for{
      user <-findUser(UserId(1))
      confirmed=confirmUser(user)
      newId <- persistUser(confirmed)
} yield newId

forResultProgram(MockRepository)

I znowu przy odpowiedniej znajomości składni scali wiadomo co tam się dzieje. Przy braku znajomości składni scali to nic nie będzie czytelne ale to już nie problem języka...

Wyważanie otwartych drzwi

Nie trzeba samemu tworzyć tej niejawnej (implicit) konwersji dodającej (map i flatMap) do funkcji. Można się wspomóc biblioteką :

import cats.std.all._
import cats.syntax.functor._
import cats.syntax.flatMap._

val catsResult: (Repository) => UserId = for{
      user <-findUser(UserId(1))
      confirmed=confirmUser(user)
      newId <- persistUser(confirmed)
} yield newId

Separacja definicji od wykonania

Na to co do tej pory robiliśmy, można także spojrzeć z zupełnie innej strony. W sumie zbudowaliśmy działającą kompozycje funkcji, która jest wyzwalana poprzez podanie parametru repozytorium. Czyli w jednym miejscu zdefiniowaliśmy "co" ma się stać (logika przekształceń) a w innym "jak" to ma się stać (produkcyjne repo/ testowe repo).

Funkcje częściowe w przebraniu - źródło wszelkich bugów

Jets taki fajny cytat, że bugi/błędy pojawiają się wtedy gdy funkcję częściową traktujemy jak całkowitą. No bo jakby dziwnie to nie zabrzmiało program z błędami to funkcja częściowa, która jest określona poza wszystkimi miejscami w których się wypie***la.

Pytanie co zrobić z sytuacją gdy chcemy jakoś zaznaczyć, że czytanie/zapisywanie w repo może się popsuć. Cóż pamiętajmy, że to zajebistego w funkcjach jest, że to jeden ujednolicony interfejs i bardzo łatwo go komponować w bardziej wyrafinowane konstrukcje.

Dlatego też prostym i szybkim rozwiązaniem ale nie koniecznie najlepszym może być :

def hasSideEffect[A,B](f:A=>B): A=>Option[B]= a=> try{
     Some(f(a))
}catch{
     case e:Throwable => None
}

val programWithEffects: (Repository) => Option[UserId] = hasSideEffect(forResultProgram)

Jak mówiłem szybkie ale nie koniecznie najlepsze. Tak czy inaczej w kontekście całego artykułu - być może warto się czasem zastanowić, które założenia w naszym rozumieniu świata to faktycznie "stałe", a które to "leniwe zmienne", do których się przyzwyczailiśmy i nie chce nam się pomyśleć "a co by było gdyby..."