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ę!




8 komentarzy:

  1. TEST ZADZIAŁAŁ!!!! :D:D

    ,;``',
    ; |
    ;;. ;;,'
    `"-'7'. IT'S TIME TO CELEBRATE!! _ /
    |' >. .'` |\ -
    | / >. _\ / / | -
    '/ / ,`. __..----. .' .' / _
    ; / /_.-' \ /_.-`_.-' \
    ;' .' '` | - `-.-'
    |_/ .' / \_\_
    _| |_ .____.-'` / __)`\
    ( ` /\`'-...__.' \ | '\(_.'|
    `-\ \ `-'-'-'| `. -. \(_./
    \ \.-.-. \ \___ / >-'\_\
    \ \ \ \ `/\ |_ '` /
    _./\ \ ' | / /_\ .-`
    .' _.\'. '.__.' /`\`'
    .-.---'\_/ `.`'-..____ ; \
    / / .--. |, `'-._ /`'.| |
    `| /-' / / \ `.' \ _/
    '-' '-' \ `-._ _,-' |
    \ `'''----'''' /
    > _.'
    / /`'-.._____..--'\ \
    < \ / /
    \ `. .' |___ mx
    ___\_ `. /__.-' ``--..
    ..--''` `-.\ (___/`'--.._____)
    (_____...--'`\___)

    OdpowiedzUsuń
  2. "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?"
    TAK, tight-coupling" pomiędzy obiektami jest zły ALE nie ma czegoś takiego jak "tight-coupling" pomiędzy danymi i operacjami(W OOP).
    Jeżeli ktoś uczył Cię obiektowości, to uczył Cię tego że obiekt to dane + zachowanie, uczył dziedziczenia i polimorfizmu - bo to fundamenty OOP. Nie możesz mówić oddzielnie o danych i oddzielnie o operacjach w kontekście OOP.

    To co Ty napisałeś to tak naprawdę znaczy to, że w obiektowych językach programowaniach obiekty są złe, bo tworzą mocne powiązanie pomiędzy danymi i operacjami. No kuźwa takie jest OOP. Za x lat kiedy na popularności zyska np. prolog, to pewnie będziesz pisał jakie to FP jest złe bo ma funkcje:).

    "Ano dlatego bo na diagramach UML były kwadraty a w nich napisy "Klient,Faktura i Money" i to się sprzedawało!
    - czy mógłbyś rozwinąć tę myśl? czy rozwój i popularność języków obiektowych zawdzięczamy według Ciebie UML'owi?

    OdpowiedzUsuń
  3. Cześć,
    Chcę uniknąć dyskusji OOP vs FP ponieważ uważam, że obydwa rozwiązania są dobre ... tam gdzie są dobre.

    W świecie symetrycznym mielibyśmy taką sytuację :

    1) Mam problem
    2) Mam podejście OOP z danymi praktykami, które rozwiążą tę część problemu.
    3) Mam podejście FP z innymi praktykami, które rozwiąże inną cześć problemu.

    Natomiast wydaje mi się, że mamy niekoniecznie dobry świat asymetryczny gdzie mamy podejście

    Mam OOP, już mówiłem, że mam OOP i praktyki OOP i wszystko ale to wszystko i zawsze jest obiektem
    - no dobra a jaki jest problem?

    I generalnie np. Scala jest językiem bardziej złożonym od Javy bo próbuje pokazać że można obydwa podejścia połączyć i co najważniejsze, ze dwa różne podejścia mogą istnieć obok siebie.


    Także tworząc w artykule porównania - niekiedy jak widać budzące emocje - staram się pokazać alternatywy i walczyć z podejściem "jedynego słusznego rozwiązania".

    Zgadza się, że obiekt to dane+zachowanie. Być może czasem dane+zachowanie związane w jednym miejscu to więcej kłopotu niż pożytku? Być może to wygoda nie zawsze musieć stosować OOP?

    Co do UML to był taki żart na koniec ale mam taką niepopartą teorię, że mógł pomóc trochę w tej części Europy bo zazwyczaj ludzie z hajsem raczej mają inny zestaw umiejętności niż "dogłębna wiedza techniczna" i mogli skusić się wizją "w centrali ktoś mądry mówiący dobrym angielskim zaprojektuje a później wyślemy diagramy do taniej siły roboczej to zaimplementowania" wpasowuje się w taki model "ktoś mądry zaprojektuje linie montażową a za praca pilnikiem będzie ofszore" ale to takie moje gdybania i to miał być taki żarcik, ze inwestuje się zawsze w buzzwordy. Także żarcik. Na pewno było dużo zmiennych który wpłynęły an obecny stan rzeczy , teoria chaosu i takie tam.

    pzdr i dzięki za komentarz.

    OdpowiedzUsuń
  4. Hej, być może czegoś nie czaje ale napisałeś: "by w Javie przeczytać kolejność typów trzeba czytać od końca bo najpierw mamy typ zwracany z później dopiero typ przekazywany."
    A w specu (https://docs.oracle.com/javase/8/docs/api/java/util/function/Function.html) jest:

    interface Function

    Type Parameters:
    T - the type of the input to the function
    R - the type of the result of the function

    Czy może po prostu coś pogmatwałem?

    OdpowiedzUsuń
  5. Cześć, w tym przypadku chodziło mi o kolejność w metodach - tam jak czytasz od lewej to najpierw masz co metoda zwraca a dopiero później co metoda przyjmuje. Do tego obydwa typy przedzielone są nazwą metody przez co troszeczkę trudniej śledzić transformacje danych. {wyjście}{nazwa}{wejście}

    Zwróć uwagę na ten rysunek ze strzałeczkami (ostatnie zdjęcie przed końcowa fotką przyrodniczą) i porównaj z kodem Haskella, który jest zaraz nad nim - tam masz {nazwa}::{wejście} -> {wyjście}

    typ Function jest w Javie OK :)

    pzdr,

    OdpowiedzUsuń
  6. A no i ważna rzecz, że w Javie jak już zadeklarujesz typ Function jako wartość to on nie może mieć generyków.
    Dlatego też w przykładzie "map" jest standardową metodą z generykami, która przyjmuje funkcje i zwraca funkcje i ponieważ jest to metoda to własnie am ten schemat {rezultat}{nazwa}{wejście} .
    W scali ta metoda miałaby schemat {nazwa}{wejście}{rezultat}. Przy bardziej złożonych sygnaturach mi osobiście bardzo ten drugi schemat pomaga w czytaniu co konkretnie przechodzi w co. pzdr

    OdpowiedzUsuń
  7. Tak sobie zerknąłem na kilka nowych języków :
    - rust - http://rustbyexample.com/fn/methods.html {nazwa}{wejscie}{wyjscie}
    - kotlin -https://kotlinlang.org/docs/reference/functions.html {nazwa}{wejscie}{wyjscie}
    - pony - http://tutorial.ponylang.org/expressions/methods.html {nazwa}{wejscie}{wyjscie}

    Zodyn nie zapożycza składni z Javy. W sumie jakiś spec od kompilatorów mógłby się wypowiedzieć czy ta odwrócona kolejność z Javy to nie jest jakiś anachronizm składniowy, który zgubił swoje uzasadnienie gdzieś w połowie lat osiemdziesiątych czy coś takiego. Przyznam, ze nie znam się na tym a jedynie gdzieś czytałem, że jak wszystkie kwalifikatory (jak final public itd) daje się z lewej a dane idą na prawą stronę to coś tam podobno łatwiej temu kompilatorowi idzie. Ale to ktoś od kompilatorów musiałby się wypowiedzieć. Ja jedynie widzę subiektywny efekt uboczny, że i łatwiej się to czyta.

    OdpowiedzUsuń