środa, 11 maja 2016

Typy Danych ale Algebraiczne

Często nie ma co zbyt pochopnie wydawać oceny gdyż wiele zależy od sytuacji i kontekstu :

  • Widzisz kolesia z tasakiem we krwi - krzyczysz "Policja!" - a to tylko rzeźnik wraca z fajki.
  • Widzisz jak koleś wpada do banku z kałachem - krzyczysz "Policja!" - a to tylko kręcą "Nowe przygody Porucznika Borewicza"
  • Widzisz kolesia, który w klasie abstrakcyjnej jawnie odwołuje się do typów, które ją rozszerzają - krzyczysz "Policja Obiektowa !" - a to tylko ktoś sobie implementuje Algebraiczne Typy Danych

"to zależy"

Programiści to ludzie a ludzie lubią jak są jasne zasady, posortowane ,dobro-zło, po przydzielane do danej kategorii i zaszufladkowane. "Czy to jest dobre rozwiązanie? To zależy..." - to niektórych bardzo denerwuje i frustruje bo psuje ścisłe prawa rządzące ich światem.

Ta książka :
Strona 117 (edycja polska, wydawnictwo Helion, copyrajt 2010 itd) :

Jeśli dobrze kojarz Unkel Bob prowadzi firmę "ObjectMentor" (dla młodzieży - Unkel Bob - Robert C.Martin - autor Clean Code) no i jak on sam twierdzi, że nie wszystko jest obiektem i czasem jak coś nie jest obiektem to dobrze, że nie jest obiektem - to może również na pytanie "czy dobre zasady programowania obiektowego są zawsze dobre " - może też zależy? (jak np. "czy bieganie jest zdrowe? No jak stoisz na polu minowym to nie jest- także zależy.")

"Otwarty na rozbudowę / zamknięty na polemikę"

Jest zasada Otwarty na rozbudowę / zamknięty na modyfikację i ogólnie jest to dosyć dobra zasada ale problem polega na tym, że można ją interpretować. Dokładnie tak - programiści ją interpretują i stosują swoją interpretację. Moją interpretacją przez kilka ładnych lat było to, że "trzeba tak napisać klasy i interfejsy, żeby łatwo nowe klasy implementowały te interfejsy a stare klasy były nie ruszane".

Jednak można znaleźć przykłady gdzie zabronienie powyższego procederu tylko wyszło na dobre! Np w uproszczeniu Option w Scali wygląda tak :

sealed trait Option[+A]
final case class Some[+A](get : A) extends Option[A]
case object None extends Option[Nothing]

Sealed to bardzo interesujący mechanizm, którego chyba nie ma w Javie. To takie final ale tylko dla kodu, który nie znajduje się w tym samym pliku - sprawia to, że możemy stworzyć lokalną rodzinę typów zamknięta na dalsza rozbudowę (rozbudowę w sensie dokładania nowych typów bo można o nowe operacje). Nie jestem historykiem-archeologiem ale wydaje mi się, że ten Option jest w Scali od dosyć dawna. W Javie jest Optional, które jest "public final class" i tez ogólnie na rozszerzanie jest zamknięte (i może własnie brak mechanizmu sealed w Javie spowodował, że tam Optional jest zaimplementowany jako taka wolno stojąca pojedyncza klasa ? Nie wiem, być może - to taki strzał z dupy)

Jest jeszcze inny ciekawy przykład...

Jaka cena?

To jest przykład , który z jednej strony ma szanse pokazać, że i czasem przez ponad 150 lat nie ma potrzeby na "rozbudowę typu". I pomimo, że typ nie był rozbudowywany to jest niezwykle użyteczny i praktyczny. Do tego cały czas się ciągnie to słowo Algebraiczne, które brzmi bardzo abstrakcyjnie i matematycznie.

Ów przykład to Algebra Boole'a - i pojawia się tam to słowo Algebra a generalnie może się pojawić bo typy i operacje są jasno określone bo ograniczone . Co by się stało gdyby typ Boolean był otwarty na rozbudowę w rozumieniu "dodawania nowych typów rozszerzających Boolean?"

Jak mamy :

if(costam) {
wariant1
}else{
wariant2
}

To wiadomo, że albo "costam" to albo false albo true (poza językami, gdzie pusta tablica to też false ale to inna historia). I przez to, że jest tak ubogo to jest prosto, a jak jest prosto - to jest bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo bardzo dobrze.

Jak jest prosto to jest bardzo dobrze i tworzenie abstrakcji gotowej na rozbudowę może mieć swoje uzasadnienie ale ma tez swoją cenę!. I jak np. napiszemy kod bardzo abstrakcyjny to nie jest tak, że "dobre bo solid kurde solid" tylko trzeba zadać tez sobie pytanie czy jesteśmy świadomi ceny i czy potrzeba tę cenę uzasadnia.

Czyli to że Boolean to tylko True i False to dobrze - uwaga! "To zależy" . Tak na codzień to chyba dobrze ale już w poniższych linkach wygląda tak trochę jakby nie wystarczyło

Koniec filozofowania

Dobra pisanie tekstu zamiast kodu ma tę wadę, ze muszę potem sprawdzić czy nie ma w nim błędów bo spellcheckerowi daleko do kompilatora. Także tego...

W Scali jakbyśmy chcieli takiego Booleana rezem z operacjami zaimplementować to tak :

sealed trait Bul{
  def and(other:Bul)= this match {
    case Fols => Fols
    case Tru  => other
  }
}
case object Tru extends Bul
case object Fols extends Bul

I teraz mamy rzecz niesłychaną!!! Oto w klasie (a właściwie traicie) abstrakcyjnej mamy Pattern Matching z konkretnymi typami. Ale słuchaj, słuchaj, wdech-wydech, posłuchaj. Wyłącz na chwile reakcję automatyczną ciała migdałowatego i pomyśl - i tak nie dorzucimy żadnego nowego typu, wszystko jest jasno określone tu i teraz więc czemu by tej wiedzy nie wykorzystać? Ano wykorzystujemy.

I w scali fajne jest to, co jeśli źle użyte będzie przeklęte - można czasem olać kropki i nawiasy i wtedy mamy :

println(Tru and Tru) //Tru
println(Tru and Fols) //Fols
No dla mnie wygląda to fajnie. Ale można jeszcze inaczej :
object BulAlgebra{
  def and(left:Bul,right:Bul):Bul=(left,right) match{
    case (Fols,Tru) => Fols
    case (Tru,Tru) => Tru
  }
}
I jest tutaj specjalnie zrobiony błąd bo nie wszystkie opcje są rozpatrzone :
Warning:(29, 35) match may not be exhaustive.
It would fail on the following inputs: (Fols, Fols), (Tru, Fols)
  def and(left:Bul,right:Bul):Bul=(left,right) match{
                                  ^

I zobaczcie co zyskaliśmy przez to, że nie tolerujemy open/close principle po obiektowemu - wszystkie typy są w jednym miejscu i kompilator może mi zwrócić pożyteczną uwagę "o czymś żeś zapomniał i patrz wyjebie się - nie ma za co"

To była Scala a teraz coś z zupełnie innej beczki

Idris

Idris to język - który wygląda jak Haskell plus ma bonusy - taki Super Sajan Haskellz rodziny "bardzo dobrze zaprojektowane ale mało kto umie używać" (tego typu języki napotykają dziwną barierę w organizacjach gdzie "wzrost" jest mierzony "hed kantem"). I generalnie tutaj to normalnie jest Baaaaaaaaaaajka (nie mylić z tym klubem w Mielnie).

data Bul = Tru | Fols

and : (Bul,Bul) -> Bul
and (Tru, Fols) = Fols
and (Tru,Tru) = Tru

I znowu jest specjalny błąd ale tutaj do problemu podejdziemy trochę od innej strony. W Idris możemy sprawdzić czy funkcja jest całkowita czy częściowa gdyż kompilator ma wszystkie informacje o typie - fachowo to się nazywa totality checker.

I po uzupełnieniu :

and : (Bul,Bul) -> Bul
and (_, Fols) = Fols
and (Fols, _) = Fols
and (Tru,Tru) = Tru

I bardzo dobrze. I naprawdę podoba mi się ta nazwa "Totality Checker" - jak taki cyfrowy superbohater.

Kotlin

Kotlin jest o tyle ciekawym językiem, że został stworzony przez tę samą firmę, która stworzyła jedno z lepszych (a być może najlepsze) IDE dla programistów Javy - JetBrains. I to co jest niezwykle ciekawe,to że firma z kilkunastoletnim doświadczeniem z parsowania na lewo i prawo składni Javy stworzyła dla swojego języka składnię podobną bardziej...do Scali. Dokładnie tak - Kotlin jest podobny bardzo do Scali niż Javy - a do tego JetBrains reklamuje go jako język bardzo praktyczny i ma wszelkie "kwalifikacje" aby twierdzić, że wie jak powinien wyglądać język praktyczny. Poniżej przykład ADT w Kotlinie - co moim zdaniem jest bardzo dobre to fakt, że jeśli ten ichniejszy pattern matching z "when" ma brakujące przypadki to kod zwyczajnie się nie kompiluje.

sealed class Bul{
    object Tru : Bul()
    object Fols : Bul()

    fun and(other:Bul):Bul=when(this){
        is Fols -> Fols
        is Tru -> other
    }
}

Także może należy przenieść dyskusje "składnia Javy i składnia Scali" na nowy poziom - "Składnia języków z lat 90tych i składnia nowych języków?". Generalnie kto zna Scalę powinien szybko załapać Kotlina a kto zna Javę -cóż... będzie miał okazję poznać wiele mechanizmów, o których istnieniu nie miał pojęcia.

wtorek, 3 maja 2016

For comprehension ale że w Javie

Pytanie czy w sytuacji gdy chcę napisać po prostu kilka kawałków kodu potrzebny jest jakiś rozbudowany wstęp? Sam sobie odpowiem, że nie i dlatego tylko wspomnę, że będzie tutaj o konstrukcji, która może znacznie poprawić czytelność kodu gdy w naszym kodzie Javowym (tak Javowym) pojawi się koło siebie kilka Optionów czy Futureów.

W Javie8 i są już i lamby i są już reprezentacje "Efektów". I chyba każdy język, który ma lambdy i efekty ma coś co nazywa się "[coś-tam] comprehension" - no tylko Java nie ma... ale w sumie to trochę ma... ale nie do końca. Zresztą zaraz zobaczymy.

Po co w ogóle to comprehension

Nie będziemy się bawić w tworzenie jakiejś sztucznej domeny tylko po prostu jedziemy z trzema optionami.

val option1=Some(1)
val option2=Some(2)
val option3=Some(3)

Jak mamy taki option to możemy kombinatorem/metodą map przetwarzać potencjalną wartość, która weń siedzi przy pomocy takich "czystych" normalnych funkcji.

option1.map(i=>s"result is $i").getOrElse("there is no result")
//res1: String = result is 1

To było proste ale bardziej skomplikowane konstrukcje pojawią się gdy więcej Option-ów musi ze sobą współpracować.I jeśli chce dodać dwa inty z option1 i option2 to będzie to wyglądać tak :

option1.flatMap{i1 =>
        option2.map(i2=>i1+i2)
}

I analogicznie dla trzech :
val result: Option[String] = 
 option1.flatMap { i1 =>
        option2.flatMap { i2 =>
                option3.map(i3 => s"result is  ${i1 + i2 + i3}")
        }
 }
I tak dalej...

Ponieważ im więcej tego będzie tym większy arrow code się wygeneruje dlatego też w Scali mamy do dyspozycji taką oto konstrukcję :

val forResult: Option[String] =for{
        i1 <- option1
        i2 <- option2
        i3 <- option3
} yield s"result is ${i1+i2+i3}"

result.getOrElse("there is no result")
Wynik ten sam a jak się zna ten piękny pokazywany tutaj język to czyta się o niebo łatwiej.

A co jest w Javie? A w Javie...

Mroczne widmo

Przy pomocy standardowych bibliotek w Javie to "se" można takie coś najwyżej zrobić :

Optional<Integer> optional1 = Optional.of(1);
Optional<Integer> optional2 = Optional.of(2);
Optional<Integer> optional3 = Optional.of(3);


Optional<String> result =
        optional1.flatMap(i1 ->
            optional2.flatMap(i2 ->
                optional3.map(i3 -> "result is : " + (i1 + i2 + i3))
            )
        );

System.out.println(result.orElse("there is no result"));

// result is : 6

Ale nie jest jeszcze tak źle bo ten... epizod sżósty... i tak dalej

Nowa nadzieja

Jacyś bardzo dobrzy ludzie - niech im szczęście w życiu sprzyja - stworzyli bibliotekę JAVASLANG , która po pierwsze primo daje nam typ Option, który nie jest tka upośledzony jak Optional z Javy8 (z 5 razy więcej przydatnych nań jest metod). No i bardzo wygodnie się pomiędzy tymi dwoma mapuje.

Option<Integer> dobryOption = Option.ofOptional(Optional.of(1));
Optional<Integer> dziwnyOption = dobryOption.toJavaOptional();

Ale najważniejsze jest to teraz, patrz na to , ej no skup się teraz, przypomnij sobie te flaMapy z poprzedniego punktu i patrz na to, patrz teraz na to :

 API.For(Option.of(1),Option.of(2),Option.of(3))
                .yield((i1,i2,i3)->i1+i2+i3)
                .map(sum -> "result is : "+sum)
                .forEach(System.out::println);
//result is : 6

To jest chyba nówka sztuka : http://static.javadoc.io/io.javaslang/javaslang/2.0.2/javaslang/API.html

Labola.. Rabor... Rabol ... Labolatorium

Scala - wszystkie Optiony Some

for {
      i1 <- Option(1)
      i2 <- Option(2)
      i3 <- Option(3)
    } yield i1+i2+i3

//Some(6)

Javaslang - wszystkie Optiony Some

API.For(Option.of(1), Option.of(2), Option.of(3))
                .yield((i1, i2, i3) -> i1 + i2 + i3).headOption()

//Some(6)

Scala - jeden Option None

val result: Option[Int] =for {
      i1 <- Option(1)
      i2 <- Option.empty[Int]
      i3 <- Option(3)
    } yield i1+i2+i3

//None

JavaSlang - jeden Option None

Option<Integer> result = API.For(Option.of(1), Option.<Integer>none(), Option.of(3))
                .yield((i1, i2, i3) -> i1 + i2 + i3).headOption();

//None

Scala - Interakcja pomiędzy Optionami

 val result: Option[Int] =for {
      i1 <- Option(1)
      i2 <- Option(2 + i1)
      i3 <- Option(3 + i2)
    } yield i1+i2+i3

//Some(10)

Javaslang - Interakcja pomiędzy Optionami

Tutaj trzeba zwrócić uwagę, że składnia jest ciutkę inna ale z kompilatorem Javy chyba nic lepszego się nie zdziała :(

Option<Integer> result = 
        For(Option.of(1), i1 ->
                For(Option.of(2 + i1), i2 ->
                        For(Option.of(3 + i2)).yield(i3 -> i1 + i2 + i3)
                )
        ).headOption();

//Some(10)

No to już tak średnio wygląda ale to nie największy problem...

"Ej a ten headOption?" - czyli nie ma róży bez kolców a ślimak pokaż rogi gdzie kucharek sześć

Trochę tam oszukiwaliśmy w tym Javaslang bo zawsze po wszystkim wołaliśmy "headOption". A stosowaliśmy to gdyż oryginalny typ to :

 Iterator<Integer>
Czyli dostajemy osobny typ zdefiniowany przez Javaslang
package javaslang.collection;

public interface Iterator<T> extends java.util.Iterator<T>, Traversable<T> {...}

Zaś sama deklaracja For wygląda tak :
 public static <T1, T2> For2<T1, T2> For(Iterable<T1> ts1, Iterable<T2> ts2)
Dlatego możemy zapomnieć o użyciu tutaj Optional czy CompletableFuture z Java8 (ale an szczęście są konwersje do javaslang i jest jakiś Future w Javaslang ale nie stosowałem)

Jakie to ma konsekwencje? Np w Scali gdy zrobię obliczenia na typie , który niesie ze sobą więcej informacji niż jest/nie ma to raz, że nie trace informacji o typie a dwa to, że nawet w przypadku failu wiem co się stało.

val result: Try[Int] = for {
      i1 <- Try(1)
      i2 <- Try(1 / 0)
      i3 <- Try(3)
    } yield i1 + i2 + i3

//Failure(java.lang.ArithmeticException: / by zero)
A Javie niestety w kontekście obydwu wymienionych punktów możemy rozłożyć ręce...
Iterator<Integer> result = For(
                Try.of(() -> 1),
                Try.of(() -> 1 / 0), 
                Try.of(() -> 3))
                .yield((i1, i2, i3) -> i1 + i2 + i3);

//Iterator()
Ale i tak to co możemy zrobić z biblioteką Javaslang jest o niebo lepsze niż z samą biblioteką standardową. A pod tym linkiem jest więcej ciekawych inicjatyw : 5 Essentials for a Java 8 Tech Stack

11 Maja będzie warsztat o efektach ubocznych w Javie to będzie można sobie poćwiczyć : Warsztaty Java8 - Funkcje i Efekty Uboczne

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..."

poniedziałek, 28 marca 2016

O Rany odwrotna kolejność Danych

A w zasadzie to chodzi o kolejność deklaracji typów w funkcji ale to się nie rymuje tak łatwo bo jak znaleźć rym do "funkcji"? Albo do słowa "Typy"? Do "Typy" można napisać "z Dypy" ale jest to naciągane i podobno niekulturalne. Także ten tego...

Prawdziwa natura metod i "obiektowa klatka"

Zacznijmy od takiego prostego (z pozoru) kawałka kodu, który ejst zwykłym wywołaniem metody na obiekcie :

List(1,2,3).map(e=>e+1)
I chociaż nie wygląda to na cos magicznego to jednak dzieje się tutaj ciekawa rzecz : wywołujemy metodę na konkretnej instancji a nie na dowolnej i to jest dosyć ważne . Wywołanie to ma jeden parametr - funkcję mapującą

Ciekawą sprawa jest to, że tak naprawdę (i w uproszczeniu) rozpatrywana metoda ma dwa argumenty bo dochodzi jeszcze niejawny argument this - referencja do instancji (Przynajmniej tak to działa na pewno w Javie to chyba w Scali też : http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-3.html#jvms-3.7)

Wobec tego powyższe można by zapisać koncepcyjnie jako :

//symboliczna równowartość wołania metody na instancji obiektu
def map[A,B](instance:List[A])(f:A=>B):List[B] = instance.map(f)

I w zasadzie można teraz tak pomajstrować w kodzie, że bez problemu zasymulujemy obiektowe wywołanie metody

val listInstance=List(1,2,3)
val function:Int=>Int = e => e+1

// można od razu wywołać z argumentem
map(listInstance)(function) // List(2,3,4)

//A można sobie zasymulować ustawienie instancji
val mapForInstance: (Int=>Int) => List[Int]=map(listInstance) _

//i później wywołanie
mapForInstance(function)

No niby taka ciekawostka ale czy coś praktycznego z tego dla nas wynika? Pierwsza rzecz to spójrzmy na ten typ :
val mapForInstance: (Int=>Int) => List[Int]

To nic innego jak metoda, która jako parametr przyjmuje funkcje Int=>Int i zwraca List[Int]. Ten typ jest dosyć dziwny i trochę nienaturalny ale z tą dziwnością żyliśmy przez lata w Javie i może dlatego dopiero po takich ćwiczeniach składniowych pojawia się tutaj "coś dyskusyjnego". Cóż, nasza nowa forma nie jest scalona z instancją co daje nam nowe pole do eksperymwntów...

A gdyby tak na odwrót - czyli opuszczenie mentalnej klatki

Odwróćmy kolejność argumentów i nazwijmy metodę fmap tak dla odróżnienia.

def fmap[A,B](f:A=>B)(l:List[A]):List[B] = l.map(f)

I powtarzamy ćwiczenie aplikując jedynie pierwszy argument :

val addOneToAll: (List[Int] => List[Int]) = fmap(function)
val allToString: (List[Int] => List[String])=fmap(i=>i.toString)

I uzyskujemy dosyć ciekawy typ List[Int] => List[Int]. Jest to spójniejszy aniżeli to co było w poprzednim punkcie bloga czyli ten typ, który funkcje zamieniał w listę. Aby zobaczyć to jeszcze lepiej przenieśmy drugi argument do sygnatury zwracanej funkcji :

def fmap[A,B](f:A=>B): List[A] => List[B] = l=>l.map(f)

I teraz widać to dokładnie! Jaka piękna kompozycja normalnie! Zamieniamy A=>B w List[A] => List[B]. No dobra ale po co i skąd ten entuzjazm? Czas na rysunek....

Czyli generalnie możemy sobie łatwo skomponować ciąg obliczeń na poziomie pewnych struktur korzystając z prostych funkcji jako bloków budulcowych. A teraz kod...

val addOneToAll: (List[Int] => List[Int]) = fmap(function)
val allToString: (List[Int] => List[String])=fmap(i=>i.toString)


//obliczenia
val computation: (List[Int]) => List[String] =
  ((l:List[Int]) => l.filter(_ > 0) ) andThen addOneToAll andThen allToString

//albo i tak
val computation2: (List[Int]) => List[String] =
  ((l:List[Int]) => l.filter(_ > 0) ) andThen fmap(function) andThen fmap(_.toString)

//i wtedy

computation(List(-1,-2,0,1,2)) // List[String] = List(2, 3)
computation2(List(-1,-2,0,1,2)) // List[String] = List(2, 3)

Dodatkowa parametryzacja

Korzystanie z prostych funkcji ma tę ogromną zaletę, że łatwo da się je parametryzować i nie musimy zaszywać na stałe wartości do której porównujemy w filtrze itd.

//podnoszenie funkcji filtrującej do poziomu list
def filter[A](f:A=>Boolean):List[A]=>List[A]= l=> l.filter(f)

//konfiguracja tego tam warunku po którym filtrujemy
val curriedPredicate: Int => Int => Boolean = toCompare => elem => elem > toCompare

// a tutaj sobie konfigurujemy dodawanie
val curriedAdd : Int=>Int=>Int = a => b =>a+b


// I HEJA!
val computation3: (List[Int]) => List[String] =
  filter(curriedPredicate(-1)) andThen fmap(curriedAdd(2)) andThen fmap(_.toString)


computation3(List(-1,-2,0,1,2)) //List(2, 3, 4)

I rysunkowo :

Marketing i edukacja

A ja się pytam dlaczego w trakcie edukacji finansowanej z podatków moich rodziców i później moich nie uczyli takich rzeczy tylko w ząbek czesane jakieś pieski i kotki dziedziczące z Animala ? I jednocześnie bombarduje się ludzi zasadami, że "tight-coupling" pomiędzy obiektami jest zły ALE (niechajże gęś to kopnie) jednocześnie wmawia się, że obiekty jako takie są dobre chociaż obiekt to nic innego jak - motyla noga - "tight-coupling" pomiędzy danymi i operacjami? (udało się udało się, zero bluzg udało się nie wierzę, że się udało kurwa udał... a niech to dunder świśnie)

"Ano dlatego bo na diagramach UML były kwadraty a w nich napisy "Klient,Faktura i Money" i to się sprzedawało!

Chyba rozumiem, czyli aby zmaterializować hajs metodę map trzeba zapisać tak :

type InternetOfThings[A]=List[A]
def zysk[Klient,Faktura](bigData: Klient=>Faktura)(microservices: InternetOfThings[Klient])
  = microservices.map(bigData)

Teraz dobrze?

Inne języki

W Haskellu w map funkcja występuje na pierwszym miejscu przez co możemy zastosować wszystkie patenty z tego artykułu :

:type map
map :: (a -> b) -> [a] -> [b]

:type map (+1)
map (+1) :: Num b => [b] -> [b]

A w Javie :

Aby w Javie przeczytać kolejność typów trzeba czytać od końca bo najpierw mamy typ zwracany z później dopiero typ przekazywany. Czyli Java to język, który czyta się od prawej do lewej i tutaj można by umieścić jakiś żart o czytaniu z prawej do lewej ale w dzisiejszych czasach to już polityka. Także udanego programowania życzę!