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