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