niedziela, 26 października 2014

CodeRetreat 2014 - nowe propozycje

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ć
Czy można to zrobić lepiej? Na pewno - rok temu próbowałem napisać coś takiego w scali i wyszła masakra :) - generalnie za każdym razem idzie to coraz lepiej. Na tym właśnie polega nauka. Na CR nikt nikogo nie ocenia i tez po to usuwamy zawsze kod - by nie bać się oceny. Strach przed oceną to chyba jeden z największym hamulców w nauce i samorozwoju ogólnie.

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.

Brak komentarzy:

Prześlij komentarz