To jest post promujący CodeRetreat 2014 w Łodzi. Sponsorem wydarzenia w 2014 roku jest firma Symphony Teleca. Spotkanie odbędzie się 15 listopada zaś link do meetupa i zapisów jest tutaj -> link do zapisów.
Jeśli tylko nie zapomniałem odblokować zapisów to już można się dodać. W tym roku Marek chciał zaprosić jakieś gwiazdy zagraniczne do poprowadzenia CR jednakowoż coś tam komuś wypadło i na razie pozostała moja skromna osoba do pomocy przy tym evencie. Jeśli komuś znudził się ten sam prowadzący 3 rok z rzędu może śmiało napisać na contact@juglodz.pl z własną propozycją ;)
Zmiany
Na ostatnim spotkaniu JUGa kilka osób narzekało - "nie tylko nie znowu Game of Life". Gra życie jako taka jest dosyć ciekawa pod względem edukacyjnym dlatego chce ją zostawić ale dla zwiększenia różnorodności i nauki świeżych technik nadszedł czas na jakieś nowe ograniczenia. Oczywiście dla tych, którzy chcą pozostaną standardowe ćwiczenia.
Jakiś czas napisałem artykuły o nowych podejściach do rozwiązania problemu komórek:
Ponieważ nie każdy lubi Scalę a również powyższe rozwiązania mogą być zbyt egzotyczne dla osób codziennie programujących w Javie - więc poniżej przyjrzymy się kilku patentom zaprezentowanym w Javie8.Immutable i referential transparency
Pierwsze ograniczenie : obiekty muszą być Immutable (naprawdę nie wiem czy słowo "niemutowalne" w ogóle istnieje w słowniku polskim czy też to coś w stylu "komitować") i do tego należy zachować "referential transparency" (i znowu tłumaczenie tego zwrotu na polski może przynieść więcej szkody niż pożytku)
Spostrzeżenia :
- Komórki nie przechowują w zasadzie żadnego stanu
- Nie ma konieczności tworzyć za każdym razem nowej żywej/martwej komórki
abstract class Cell{ abstract Cell evolve(int liveNeighbours); } class LiveCell extends Cell{ @Override Cell evolve(int liveNeighbours) { if(liveNeighbours ==2 || liveNeighbours ==3){ return liveCell(); }else{ return new DeadCell(); } } static Cell liveCell(){ return new LiveCell(); } } class DeadCell extends Cell{ @Override Cell evolve(int liveNeighbours) { if(liveNeighbours ==3){ return new LiveCell(); }else{ return new DeadCell(); } } static Cell deadCell(){ return new DeadCell(); } }Ponieważ komórka jako obiekt nie może być raz żywa a raz martwa to nie ma potrzeby przechowywania informacji o stanie każdej komórki i możemy pokusić się o symulację nieskończonej planszy przy pomocy zwykłej mapy.
Map<Coordinates, Cell> board=new HashMap<>(); board.put(coordinates(1, 1), liveCell()); board.put(coordinates(1, 2), liveCell()); board.put(coordinates(1, 3), liveCell()); //evolve Map<Coordinates, Cell> nextPhaseBoard=new HashMap<>(); //Martwa komórka jest reprezentowana jako prosty "brak komórki" w mapie. I teraz możemy wykorzystać bardzo fajny patent z Javy 8, który został dodany do Mapy.
//calculations Cell cellAt22 = board.getOrDefault(coordinates(2, 2), deadCell()); ...
Bardziej Funkcyjnie
Na początek przyjmiemy ograniczenie - "Nie można tworzyć nowych klas" (Może z wyjątkiem klasy, która symuluje tuple/tupla bo to jest wygodne a w Javie8 jeszcze tego nie ma). Komórki będą zwykłym enumem co z jednej strony ogranicza rozszerzalność ale z drugiej ułatwia ćwiczenie.
Na początek nieskomplikowana implementacja coby mieć miłe wrażenie pracy z funkcjami :Function<Integer, Cell> evolveLife = liveNeighbours -> { if (liveNeighbours == 2 || liveNeighbours == 3) return LIVE; else return DEAD; }; Function<Integer, Cell> evolveDead = liveNeighbours -> { if (liveNeighbours == 3) return LIVE; else return DEAD; }; BiFunction<Cell,Integer, Cell> evolve = (cell,liveNeighbours) -> { if (cell == LIVE) { return evolveLife.apply(liveNeighbours); } else { return evolveDead.apply(liveNeighbours); } }; enum Cell { DEAD, LIVE }W powyższej próbce kody uwazny czytelnik zauważy funkcję "evolve" i dwie pomocnicze funkcje dedykowane dla konkretnych typów komórek - generalnie powinno łatwo dać się to testować. No i możemy rzucić okiem jak wyglądać będzie wywołanie:
System.out.println(evolveLife.apply(3)); System.out.println(evolveLife.apply(4)); System.out.println(evolveDead.apply(3)); System.out.println(evolveDead.apply(4)); System.out.println(evolve.apply(LIVE,4)); System.out.println(evolve.apply(LIVE,3));
Currying
Kolejne ograniczenie to "maksymalnie jeden parametr na funkcję". W tym przypadku nie trzeba używać BiFunction (a tak w ogóle to Java8 zdaje się nie ma czegoś takiego jak funkcja z 3 lub więcej parametrami).
Function<Cell,Function<Integer, Cell>> evolve = cell->liveNeighbours -> { if (cell == LIVE) { return evolveLife.apply(liveNeighbours); } else { return evolveDead.apply(liveNeighbours); } }; //wywołanie System.out.println(evolve.apply(LIVE).apply(4)); System.out.println(evolve.apply(LIVE).apply(3));W dużym uproszczeniu cały patent z jednym parametrem pozwala nam wstrzykiwać "kontekst" uzyskując funkcję jedno-argumentową przygotowaną do rozwiązania specyficznego problemu.
Function<Coordinates, List<Coordinates>> findNeigbhoursCoordinates=coordinates->{ return asList(//jakies tam obliczenia); }; Function<Map<Coordinates, Cell>, Function<Coordinates,Integer>> countNeighbours=board->coordinates->{ return (int)findNeigbhoursCoordinates.apply(coordinates) .stream() .map(c->board.getOrDefault(c, DEAD)) .filter(cell->cell==LIVE) .count(); }; Function<Integer,Function<Cell, Cell>> evolve = liveNeighbours->cell-> { if (cell == LIVE) { return evolveLife.apply(liveNeighbours); } else { return evolveDead.apply(liveNeighbours); } };I działać to mogłoby tak :
void test(){ Map<Coordinates, Cell> boardCurrentPhase=new HashMap<>(); Map<Coordinates, Cell> boardNextPhase=new HashMap<>(); List<Coordinates> potentialCellsWithChangeState=new LinkedList<>(); //wstrzykujemy planszę z danej fazy gry Function<Coordinates, Integer> currentPhaseNeighbours = countNeighbours.apply(boardCurrentPhase); potentialCellsWithChangeState.forEach(c->{ Integer n = currentPhaseNeighbours.apply(c); Cell cell = boardCurrentPhase.getOrDefault(c, DEAD); //tutaj akurat nie ma konkretnego zysku z rozdzielenia parametrów ale nadal zróbmy to dla ćwiczeń Cell evolvedCell = evolve.apply(n).apply(cell); if(evolvedCell==LIVE) boardNextPhase.put(c,evolvedCell); }); }
Powyższy przykład być może nie oddaje jakoś specjalnie wszystkich zalet konstrukcji [jeden argument,zwracana funkcja] ale na tę chwilę nic lepszego nie przychodzi mi do głowy. Jeśli ktoś jest ciekaw owych zalet to niech zerknie na link -> what-is-the-advantage-of-currying
W naszym kodzie można jeszcze zrobić coś takiego :
Map<Cell, Function<Integer, Cell>> handlers=new HashMap<Cell, Function<Integer, Cell>>(){{ put(LIVE,evolveLife); put(DEAD,evolveDead); }}; Function<Map<Cell, Function<Integer, Cell>>,Function<Integer,Function<Cell, Cell>>> evolveGeneral = handlers->liveNeighbours->cell-> handlers.get(cell).apply(liveNeighbours); Function<Integer, Function<Cell, Cell>> evolve = evolveGeneral.apply(handlers);Teraz możemy niezależnie modyfikować funkcje obsługi ewolucji poszczególnych komórek ale kosztem potworka w systemie typów (czyli to co tam jest po lewo)
Małe podsumowanie
- Funkcja evolve nic nie wie o tym na jakieś planszy toczy się gra czyli uzyskaliśmy tzw. "information hiding"
- Funkcja countNeighbours być może wie trochę za dużo - tutaj można by jeszcze oddzielić funkcjonalność zliczania określonych typów komórek od pobierania ich z planszy(Może zwrócić stream?).
- evolveLife i evolveDead to proste funkcje do testowania
- Można jeszcze podzielić to co dzieje się w metodzie test
- Teoretycznie po zmianie w countNeighbours ze stream na parallelStream wszystko powinno działać
Bardziej obiektowo
Nie koniecznie trzeba rozwiązywać CR funkcyjnie - po prostu można i z Javą 8 jest łatwiej (niż z Javą7). Można też pokusić się o rozwiązanie CR według niektórych bardziej obiektowo niż na klasach - a mianowicie na aktorach. Może coś w tym temacie uda mi się jeszcze opisać przed 15 listopada.