Code Retreat - a zwłaszcza GLOBAL - to doskonała okazja do nauki programowania, zerwania z nieskutecznymi nawykami i odnalezienie nowych podejść do rozwiązywania problemów. A, iż to jest to spotkanie cykliczne to akurat w tym miejscu wykorzystam starą metodę DRY i a TY będziesz dispatcherem/dispatcherką wywołania. Także wybierz sobie wstęp z jednego z poniższych artykułów.
- Motywacja do GDCR 2013
- Wnioski po GDCR 2013
- Kilka przykładów użycia mechanizmów FP ze scali do Game of Life
- Po GDCR 2014
- przed GDCR 2015 - tutaj w ogóle jakiś Haskell jest :D
- Po GDCR 2016 - przymiarki do funkcyjnego CR
- Wrażenia po GDCR 2016, którego nie było i tym razem trochę lepszy Haskell
Skoro już trochę historii za nami to wróćmy na chwilkę do ograniczeń. Jednym z popularniejszych na CR jest ograniczenie zwane object calisthenics, które jest nakierowane na poprawienie umiejętności pisania poprawnego obiektowego kodu. I od razu musimy sobie wyjaśnić jedną rzecz - jeżeli z jakiegoś powodu nie podoba ci się programowanie obiektowe to masz jak najbardziej prawo je krytykować ale najpierw musisz zrozumieć o co w tym wszystkim chodzi. jeżeli dzień za dniem rzeźbisz klasy, które nazywają się ManagerCosTamImpl albo CosTamHelper to nie masz problemu z programowaniem obiektowym ale zwyczajnie piszesz nad wyraz złozony kod proceduralny w języku, który wymusza by ten kod proceduralny był w klasach - stąd te kolosy po 8000 linijek.
A wracając do ograniczeń to object calisthenics mają np. takie ćwiczenie "jedna kropka na linie kodu" i to ma pomóc pisać kod, który lepiej enkapsuluje dane w klasach bo trudniej o tzw. train wreck obj.get()A.get()B.setC(c). No i jako, iż programowanie funkcyjne również zaczyna grać istotną role w twoim ulubionym języku to warto by podobne zasady wprowadzić dla FP.
W 2016 mieliśmy przerwę ale już w tym roku Code Retreat wraca na JUG Łódź. Spotkanie odbędzie się 18 listopada a zapisac można się tutaj -> Global Day of CR łódź Meetup. W tym roku będzie także zapewniona opieka nad dziećmi także można wpaść z rodziną.
No i by do tematu nauki podejść całościowo i rozszerzyć ćwiczenia o FP to tym razem najprawdopodobniej kilka ćwiczeń będzie zainspirowanych nowym zestawem functional calisthenics -> https://codurance.com/2017/10/12/functional-calisthenics/
No to jedziemy z koksem i obczajamy te zasady. Tym razem użyjemy języka, który zdobywa coraz większą popularność na JVM, ma dobre wsparcie w IDE bo robi go firma, która sama robi IDE a do tego jest wciąż bezpiecznie ubogi tak, ze korporacje nie powinny nim sobie zrobić krzywdy - mowa o Kotlinie.
Zasada 1 : Nazywaj wszystko i naucz się rozpoznawać wzorce w kodzie.
"All functions need to be named. Which means that you shouldn't use lambdas or anonymous functions. Also, name all your variables as per standard clean code rules."
Jeśli musisz dać czemuś nazwę to istnieje duża szansa, ze wiesz co ten fragment musi robić. No i teraz jeżeli zamiast używać anonimowych funkcji zaczniesz je definiować i nazywać to może zauważysz szansę na parametryzacje i kompozycje. Ale aby to miało ręce i nogi najpierw musisz w ogóle zastosować podejście funkcyjne a tutaj moga pomóc bardziej fundamentalne ograniczenia jak np. brak zmiennych.
Co do implementacji to tutaj bardzo może się przydać nowy ficzer z kotlina 1.1 - aliasy typów.
//aliases since Kotlin 1.1 typealias Cell = Boolean typealias Neighbours = Int val LIVE_CELL:Cell = true val DEAD_CELL:Cell = false fun cellEvolution(c:Cell,n:Neighbours): Cell = when(c){ LIVE_CELL -> if(n==2 || n==3) LIVE_CELL else DEAD_CELL DEAD_CELL -> if(n==3) LIVE_CELL else DEAD_CELL }
Powyżej pierwsza próba, w trakcie której próbowałem zadeklarować dwie stałe okreslające stan komórki DEAD oraz LIVE jakkolwiek kompilator (a przynajmniej plugin do Intellij) nie był w stanie wykryć iż wszystkie warunki w wywołaniu when zostały ogarnięte. Dlatego tez powstało drugie podejście - sealed class
sealed class Cell object LiveCell :Cell() object DeadCell :Cell() fun cellEvolution(c:Cell,n:Neighbours): Cell = when(c){ LiveCell -> if(n==2 || n==3) LiveCell else DeadCell DeadCell -> if(n==3) LiveCell else DeadCell }
Zasada 2 : Brak zmiennego stanu.
Dosłownie : "You shouldn't use any variable of any type that can mutate."
Najważniejsze w tym ćwiczeniu jest "zablokowanie" uczestnikom drogi w kierunku (nawet i poprawnego) programowania obiektowego gdyż w tym wypadku nie można owinąć zmiennego stanu instancją obiektu.
//THIS IS FORBIDDEN class MutableCell(private var state:Boolean){ fun evolve(n:Neighbours){ //set new state here } }
Pewne trudności moga sie pojawić kiedy będziemy chcieli zmienić stan planszy - no ale przecież zmiany nie są dozwolone!. Kotlin dosyć ładnie oddziela api służące do deklarowania i używania niemutowalnych kolekcji od mutowalnych odpowiedników. Aby transformować niemutowalne listy tworząc nowe niezależne wersje używamy funkcyjny wyższego rzędu jak map czy filter obecnych teraz choćby w Javie.
typealias Coordinates=Pair<Int,Int> typealias CellPosition=Pair<Coordinates,Cell> typealias Board = kotlin.collections.List<CellPosition> val nextRound : (Board) -> Board = {board -> board.map { ??? } }
No i zobacz czytelniku/czytelniczko jakim wygodnym pomysłem są aliasy w przykładzie powyżej.
Zasada 3 : Każdy argument na wejściu zwraca wynik
"There can not be an if without an else. Switches and pattern matching should always have all paths considered"
To jest dosyć interesująca zasada bo dobrze oddaje naturę code retreat. Tak więc mamy proste założenie, którego uczestnicy mają się trzymać i obserwować wyniki. Jest to na początku znacznie prostsze aniżeli wchodzenie w rozważania teoretyczne o funkcjach częściowych. Każdy argument musi generować konkretny wynik - bez wyjątków - dosłownie!
Zasada 4 : Nie używaj zmiennych tymczasowych
Zasada 5 : Expressions, not statements
Te dwie zasady postanowiłem połączyć bo w moim odczuciu dotyczą podobnego tematu. Dodatkowo druga zostawiam po engliszu by nie popełnić błędów w tłumaczeniu.
"There shouldn't be any variable declared in the body of a function." oraz "All lines should be expressions. That's it, all lines should return a value."
Tutaj języki takie jak Kotlin czy Scala mogą pomóc bardzo, bardzo, bardzo bo tam wszystko zdaje się coś zwracać! A szczególnie if jest wygodny jak coś zwraca. W Javie zwykle trzeba coś tam manipulować po bokach.
fun badEvolve(cell:Cell,neighbours:Int):Cell{ var newCell:Cell?=null //val newCell:Cell would also work here but var shows problem better if(cell==LiveCell && neighbours in (2..3)) { newCell = LiveCell } else if(cell==LiveCell) { newCell = DeadCell } else if(cell==DeadCell && neighbours==3) { newCell=LiveCell } else { newCell=DeadCell } return newCell }
W Javie często deklarowane są zmienne tymczasowe (pamiętacie te czasy gdy 90% zmiennych w waszym godzinie nazywało się temp?) tylko po to by coś w nich na chwilę przypisać i od razu dać return. Wyjście? Niech if coś zwraca i niech to będzie wynik metody. No i w Kotlinie to dzieje sie naturalnie. W Javie niby jest tzw. "ternary operator" ale on chyba dlatego nazywa się ternary bo nie ma "if-else". Trochę peszek.
fun betterEvolve(cell:Cell,neighbours:Int):Cell{ return if(cell==LiveCell && neighbours in (2..3)) LiveCell else if(cell==LiveCell) DeadCell else if(cell==DeadCell && neighbours==3) LiveCell else DeadCell }
No i teraz najlepsze. Ponieważ jest to jedyna instrukcja funkcji to możemy wyrażenie uprościć jeszcze bardziej.
fun bestEvolve(cell:Cell,neighbours:Int):Cell = if(cell==LiveCell && neighbours in (2..3)) LiveCell else if(cell==LiveCell) DeadCell else if(cell==DeadCell && neighbours==3) LiveCell else DeadCell
Zasada 6 : Brak jawnej rekurencji
Przyznam, iż tego punktu do nie rozumiem a to z faktu, iż autorzy odnoszą się do jakichś konstrukcji z Clojure. Coś tam kiedyś czytałem o "loop/recur" ale nie będę udawał, iż pamiętam. Tak czy inaczej w tym punkcie znajdziemy poradę by tę "jawną rekurencję" zastąpić map/reduce . No to tak będziemy starali się zrobić.
Zasada 7 : Generyczne bloki
"Try to use a much as possible generic types for your functions, outside of the boundaries of your application"
To jets dosyć interesujący i abstrakcyjny sam w sobie punk. Z jednej strony ile razy deklarowałeś listę kiedy kiedy Collecion lub Iterable w zupełności wystarczało? Z tym ograniczeniem będziesz musiał dokładnie uzasadnić czy typ, którego używasz nie jest zbyt konkretny co może związać ci ręce przy dalszych zmianach. Dodatkowo w Kotlinie ze względu na wykrywanie typów musisz się zastanowić czy pozostawić prace kompilatorowi, który pewnie znajdzie najbardziej konkretny typ czy też może zadecydować świadomie samemu.
Możesz zastosować podobne podejście do funkcji ale tutaj idzie to trochę w inną stronę. Czy obecna implementacja dla danego typu nie jest zbyt "wąska"? Klasycznym przykładem jest tutaj reduce które jest generalizacją dodawania, mnożenia i czego tam sobie zażyczymy.
fun add(list:List<Int>) : Int =if(list.isEmpty()) 0 else list[0] + add(list.drop(1)) fun multiply(list:List<Int>) : Int =if(list.isEmpty()) 1 else list[0] * multiply(list.drop(1)) fun <A> genericReduce(l:List<A>,neutral:A,reduction:(A,A)->A):A = if(l.isEmpty()) neutral else reduction(l[0], genericReduce(l.drop(1),neutral,reduction))
I zgodnie z oczekiwaniami
println(add(listOf(1,2,3,4,5))) println(multiply(listOf(1,2,3,4,5))) println(genericReduce(listOf(1,2,3,4,5),0,{a,b->a+b})) println(genericReduce(listOf(1,2,3,4,5),1,{a,b->a*b}))
Zasada 8 : Efekty uboczne tylko na granicy systemu
"Side effects should only appear on the boundaries of your application. The guts of your application should have no side effects."
Tutaj po pierwsze musisz używać funkcji, które nie zmieniają żadnego stanu poza nimi żadnego println czy też modyfikowania mutowalnej tablicy zdefiniowanej poza funkcją. To ograniczenie może być jeszcze bardziej interesujące gdy założysz, iż efekty na granicy systemu musza być reprezentowane przy pomocy odpowiednich typów.
W takich wypadkach możesz skorzystać z typu Optional dodanego do Javy8
typealias Coordinates=Pair<Int,Int> typealias CellPosition=Pair<Coordinates,Cell> typealias Board = kotlin.collections.List<CellPosition> val initializeBoard:(Coordinates) -> Board = ... val nextTurn: (Board) -> Board = ... fun readInput():Optional<Coordinates> = ... fun display(b:Board):Unit = ... readInput().map(initializeBoard).map(nextTurn).ifPresent(::display)
W przykładzie powyżej możesz zobaczyć, iż Kotlin całkiem wygodnie integruje Jawowe klasy. Optional służy tam do zasygnalizowania efektu ubocznego związanego z wprowadzaniem danych ze świata zewnętrznego - który jakby nie patrzyć jest trochę uboczny.
Bardziej zaawansowani uczestnicy mogą zechcieć skorzystać z również bardziej zaawansowanej biblioteki http://kategory.io/ i użyć IO Monad do wypisywania tekstu na konsolę http://kategory.io/docs/effects/io/ .
Zasada 9 : Nieskończone strumienie danych
W skrócie - nie możesz używać tablic ani kolekcji. Tam gdzie użyłbyś standardowej kolekcji musisz użyć strumieni/ nieskończonej sekwencji
Programowanie Funkcyjne kładzie ogromny nacisk na operowanie na wartościach bez mutowania stanu. jeżeli nie zmieniamy stanu i wartości są stałe to czy ważne jest kiedy operacje będą miały miejsce (stąd FP ułatwia operacje wielowątkowe)? No nie ma w związku z czym możemy pracować na tzw. leniwych kolekcjach które są niejako "opisane" ale nie istnieją puki nie trzeba.
Przykładowymi kandydatami do "strumieniowania" w grze życie są komórki - na z definicji nieskończonej planszy. Również gra sama w sobie jest nieskończoną kolekcją tur.
//here we are playing 10 turns val init:Board = initializeBoard() generateSequence(init,nextTurn).take(10).forEach(::display)
Ale zamiast 10 tur możemy chcieć grać do momentu kiedy na planszy mamy jakiekolwiek żywe komórki. Aby było bardziej interesująco przeskoczmy na chwile do Javy. Pierwszą rzeczą jaką chcemy sprawdzić to czy da się wykorzystać kod, ktory napisaliśmy w Kotlinie.
List<Pair<Pair<Integer, Integer>, Cell>> board = Life1Kt.initializeBoard();
Technicznie jest to możliwe ale praktycznie raczej nikomu nie będzie się chciało babrać z tak rozbudowanymi typami generycznymi. No dobrze to spróbujmy od początku :
Board init= ...; Predicate<Board> hasLiveCells = ...; UnaryOperator<Board> nextTurn = ...; Consumer<Board> display=...; //below iterate version possible from Java9 Stream.iterate(init,hasLiveCells,nextTurn).forEach(display); //take while possible from Java9 Stream.iterate(init,nextTurn).takeWhile(hasLiveCells).forEach(display);
Zasada 10 : Tylko funkcje jedno parometrowe
"Each function should have a single parameter."
Według mnie to najważniejsze ograniczenie by dobrze zrozumieć siłę kompozycji funkcji. Gdy przekazujesz do metody całą paczkę parametrów możesz ją postrzegać jako wielki monolit. ale kiedy przesłanie kolejnych parametrów określa tak naprawdę innego rodzaju funkcje - wtedy musisz się zastanowić co z czego tworzysz. Dla przykładu funkcję findNeighbours możesz zaimplementować na dwa sposoby.
findNeighbours : (Coordinates) -> (Board) -> Neighbours = ...
Czy widzisz różnicę? Czy lepiej mieć funkcje, która znajdzie sąsiadów dla przekazanych koordynat na z góry określonej planszy czy może wygodniej jest przekazywać planszę dla wcześniej orkeślonych koordynat?
Co do samego Kotlina to nie ma on tak wygodnego wsparcia dla curryingu jak scala (currying to własnie to, iż przekazujesz parametr po parametrze) ale jak się trochę ogarnie składnie to też daje radę
val evolve : (Neighbours) -> (Cell) -> Cell = {ns->{c->c}}
Ewentualnie produkuj funkcję z metody
fun evolve2(ns:Neighbours) : (Cell) -> Cell = {c -> c}
Zasada X : Żadnych nulli
Ta zasada co prawda nie pojawia się w oryginalnym zestawie ale w programowaniu funkcyjnym null jest ... bez sensu. Kotlin ma interesujące patenty na poziomie języka do radzenia sobie z nullami . jeżeli nie chcesz w nie wchodzić wtedy cóż, zawsze zostaje prawilne podejście z opakowywaniem każdego nullowego api jak map.get w Optional.
Podsumowanie
Podobnie jak chodząc na siłownię powtarzasz pewne ćwiczenia tak samo tutaj powtarzając cwiczenia nabierasz informatycznej siły. I analogicznie samo czytanie o ćwiczeniach kilka ci da. jeżeli chcesz zwiększyć siłę programowania funkcyjnego ćwicz,ćwicz,ćwicz,ćwicz,ćwicz,ćwicz,ćwicz,ćwicz,ćwicz,ćwicz,ćwicz,ćwicz. Poszukaj spotkania code retreat w twojej okolicy lub podobnych warsztatów i nie opierdalaj się!