czwartek, 12 listopada 2015

Code Retreat 2015 - nie bój się nauki

Global Day of Code Retreat już w tę sobotę ("będzie" albo "był" jeśli nie zdążyłem tego napisać i opublikować). Zapisać może się każdy a link jest tutaj --> Link na meetupie. I oczywiście jak co roku może pojawić się pytanie "ale po co?"

Niestety większość ludzi mylnie utożsamia naukę z ilością czasu jaką poświęcili na daną czynność. Czyli np. "mam 3 lata doświadczenia" jest utożsamiane z "przez trzy lata robiłem w zasadzie to samo, zebrałem ekspa i mogę przejść na super sajana 2". Jeśli się robi przez 3 lata to samo to raczej wiedza nie rośnie a pielęgnujemy te same nawyki. I nie wiadomo czy to jest dobre bo nie wiadomo czy same nawyki jako takie były dobre.

I tak np. ostatnio była dyskusja o tym jakie podejście jest lepsze , w czym i jak pisać itd - czyli dyskusja numer 13719273 o dyskusji. Pojawił się tam jednak ciekawy wątek o tym kiedy tak naprawdę - trochę duży poziom abstrakcji - można stwierdzić, że ktoś ma prawo twierdzić, ze coś jest lepsze od czegoś innego (no mówiłem, że duży poziom abstrakcji)

I pada takie sformułowanie "wypowiadam się o X bo napisałem w tym 14000 linii". I w sumie znałem kiedyś jednego takiego gościa co napisał w Javie 14000 - problem polegał na tym, że on to napisał je wszystkie w jednej klasie :D:D . W sumie to on i pewnie miał doświadczenia na 100.000 linii i na jakieś 6 klas ale znów specjalnie to jest uwypuklone by pokazać, że to wcale nie było dobre. Jak ktoś pisze przez 3 lata nieczytelny proceduralny kod to faktycznie ma 3 lata doświadczenia w pisaniu nieczytelnego proceduralnego kodu.

Cykl Kolba

Spójrz na ten rysunek :

Albo ten :

I teraz zastanów się czy tak podchodzisz do tego co robisz (ha! piszę w drugiej osobie tak jak ci smutni trenerzy rozwoju osobistego "gdy czytasz te słowa zastanów się czy jesteś tak efektywny jak mógłbyś być" ) czyli np. zastanawiasz się krytycznie (ale pod katem rozwojowym) co zrobiłeś źle i co możesz zrobić lepiej (brzmi trochę jak agile za czasów zanim korporacje go nie zjebały) czy może to taki model ala gierki RPG "robie robie robie, experience rośnie , to chyba samo się będzie się robić lepiej jak nowy level dostane co nie?"

no raczej nie

Generalnie kiedy coś robimy - powiedzmy na poziomie A1 to musimy wejść jeden poziom abstrakcji wzwyż na poziom A2, a nawet nie - musimy wejść jeszcze wyżej. Musimy wejść na poziom A3 gdyż na A2 możemy zobaczyć co robimy a na A3 - uwaga bardzo ważne - możemy zobaczyć siebie jak obserwujemy co robimy - bardzo bardzo ważne.

Przeszkoda w samokształceniu numer 1

Dla tych co mają samochody efekt jest znany - "ci co jadą wolniej ode mnie to cioty, ci co szybciej to wariaty!". Ja się spóźnię to wina znowu jebanego kierowcy autobusu, babci na przystankach, korków i kretów co mi an drodze wykopały kopce. jak ktoś inny się spóźni to oczywiście nie umie sobie planować czasu i jego "analytic skils" są słabe bo mógł wyjść wcześniej itd. Można sobie poczytać więcej o np. tutaj --> http://blog.krolartur.com/ja-chcialem-dobrze-ale-on-jest-wredny/

I teraz w czym to dokładnie przeszkadza. Ano w tym , że jak coś mi nie wychodzi to mogę mało obiektywnie stwierdzić "nie działa...no dobra...czego to może być wina... no nie moja oczywiście gdyż nazywam się principal architekt 33 i 1/3... no to chyba.... wiem... to chujowy język". Nie można też oczywiście przechodzić ze skrajności w skrajność bo są naprawdę słabe narzędzia i podejścia ale wspomnianego efektu świadomym być trzeba by mniej subiektywnie stwierdzać tę słabość.

ogólnie tematyka warta osobnego wpisu a opisane bardziej tutaj : https://pl.wikipedia.org/wiki/Teoria_atrybucji. I to tez jest fajne : https://pl.wikipedia.org/wiki/Podstawowy_b%C5%82%C4%85d_atrybucji

W trakcie code retreat między innymi po to się paruje z innymi i kasuje kod by ludzie mogli na luzie pogadać o tym co można zrobić lepiej bo i tak nie będzie dowodów :)

Przeszkoda w samokształceniu numer 2

Efekt Krugera-Dunninga - czyli w sumie dosyć logiczna dytuacja, kiedy nasza wiedza jest na tyle mała, że nie jesteśmy w stanie obiektywnie ocenić, że jest mała i wydaje nam się, że jest duża. Też to w sumie opisałem kiedyś : http://pawelwlodarski.blogspot.com/2013/02/efekt-krugera-dunninga.html bo sam to u siebie zauważyłem.

I to działa tak, że np. Mam grupę znajomych i jak się z nimi spotykam to opowiadam dowcipy i częstą się śmieją (opisuję tutaj sytuację hipotetyczną bo z moich nadal nikt się nie smieje). Myślę - "jestem niezłym kawalarzem". Problem polega na tym, że pomiar został wykonany na wąskiej i dosyć mało reprezentatywnej grupie. Teraz analogia do IT. Np. pisze sobie aplikację i tworzę taki nieczytelny zjebany kod ale dookoła mnie wszyscy tworzą Managery i Helpery a mój kod w sumie najmniej się wywala na produkcji no i do tego manager Franek zawsze mnie poklepuje po plecach za wzorowo wysprzątane biurko- no to chyba jestem jednym z "lepszych zawodników" tutaj "nie no po co mam się znowu uczyć podstaw jak jestem jednym z lepszych zawodników?"

To był chyba jeden ze słynniejszych eksperymentów gdzie wykazano, że jeśli mówisz o kimś "jaki on jest" czyli np. "inteligentny" to ten ktoś będzie wybierał sobie prostsze i łatwiejsze zadania by te ocenę potwierdzić. Masa jest przykładów na necie - można zerknąć także i tutaj --> http://www.focus.pl/czlowiek/azjatyckie-tygrysy-kontra-zachodnie-mieczaki-8208?strona=2

Moja skromna obserwacja - niekiedy przychodzą na code retreat jacyś seniorzy czy architekci i oni momentami bardziej kaleczą kod niż studenci. Sam się dzisiaj czegoś znowu nauczyłem gdy przypominałem sobie to ćwiczenie i w sumie zaraz będę kaleczył kod pastwiąc się nad kodem funkcyjnym.

Przeszkoda w samokształceniu numer 3

Tak zwana awersja do straty. Znowu temat rzeka : https://en.wikipedia.org/wiki/Loss_aversion ale generlanie co roku niektórzy naprawdę przeżywają szok w momencie gdy mają skasować kod. A Własnie o to chodzi, żeby totalnie poczuć, iż to jest nauka, wyluzować i próbować nowych rzeczy. Uważasz, ze masa małych klas to więcej szkody niż pożytku - ok ale poeksperymentuj i nie stresuj się bo kod i tak zaraz zniknie. W sumie to taki snap chat tylko, ze zamiast pokazywać cycki pokazujesz na chwile kod, którego nie jesteś pewien. Reeeeelaks - zaraz zniknie.

Kod

Starczy tego nawijania. To będzie moja 4 edycja gdy prowadzę i 5 coderetreat gdy uczestniczę i naprawdę co roku powtarza się kilka opinii, ze "ło jezu znowu to samo ćwiczenie" a co roku KAŻDEN się uczy na tym bez WYJONTKU. Znudziła ci się Java? We no popróbuj czegoś innego. Za kilka akapitów zamierzam wystawić się na pośmiewisko próbując implementacji w Haskellu. Ale jednocześnie pokaże, jak to ćwiczenie pomoże mi lepiej zrozumieć Javę 8 i Scalę (ba nawet uświadomi mi pewne obszary ze skali, których jeszcze nie opanowałem).

A tutaj rożne sposoby podejść z poprzednich lat :

Łobiektuwka

Zazwyczaj ludzie zaczynają implementacje od tablicy booleanów i arrow coda z tysiącem ifów. Później gdy TDD wkracza do gry i kilku złych nawyków się zabrania - wtedy pojawia się jakaś implementacja w postaci klas czy typów i pierwsze zarysy modularyzacji. No i teraz popróbujmy tego cyklu Kolba czyli trochę refleksji.

Np. obiektowo to może być tak, że faktycznie "zenkapsulujemy" jakiś mutowalny stan :

object RealOOPCells {
  abstract class Cell{
    def evolve(neighbours:Int):Unit
  }

  class LiveCell(private var state:Boolean) extends Cell{
    override def evolve(neighbours:Int): Unit= neighbours match {
      case 2 | 3 => state=true
      case _ => state = false
    }

    override def toString="LiveCell"
  }

  class DeadCell(private var state:Boolean) extends Cell {
    override def evolve(neighbours: Int): Unit = neighbours match {
      case 3 => state=true
      case _ => state=false
    }

    override def toString="DeadCell"
  }
}

Teoretycznie jest dobrze ale własnie teraz przeprowadźmy obserwację. W sumie stan komórki jest tylko częścią równania gdyż równie istotni są jej sasiedzi, którzy już z punktu widzenia komórki "zenkapsulowani" nie są. W toku ćwiczeń i pisania testów w TDD często "samoz siebie" wychodzi, że tak naprawdę nie tyle jest ważny stan ile sam fakt czy komórka jest żywa czy nie. I wteyd można tak (bacznie obserwujcie jak zniknął typ Unit z deklaracji metod - czy to jest łatwiejsze do testów):

 abstract class Cell{
    def evolve(neighbours:Int):Cell
  }

  object LiveCell extends Cell{
    override def evolve(neighbours:Int): Cell = neighbours match {
      case 2 | 3 => LiveCell
      case _ => DeadCell
    }

    override def toString="LiveCell"
  }

  object DeadCell extends Cell {
    override def evolve(neighbours: Int): Cell = neighbours match {
      case 3 => LiveCell
      case _ => DeadCell
    }

    override def toString="DeadCell"
  }

teraz teoretycznie mamy dwa singletony - i mimo panującej nienawiści skierowanej na to słowo - tutaj to akurat dobrze. Bo gdy ludzie dochodzą do tego lub podobnego rozwiązania to nawet nie są świadomi, ze oto wyszli z mainstreamowego nurtu wzorców i zaimplementowali coś co się zwie się Flyweight_pattern czyli waga piórkowa. To po obiektowemu bo po kolejny momencie "AHA" okazuje się, że możemy iść z wytłumaczeniem w tę stronę --> Algebraic_data_type

No i dalej jakoś separujemy informacje o budowie planszy poprzez abstrakcyjne rozumienie współrzędnych :

trait Coordinates{
    def neighbours:Seq[Coordinates]
  }
  case class FlatCoordinates(x:Int,y:Int) extends Coordinates {
    override def neighbours: Seq[Coordinates] = for {
      xe <- (x-1 to x+1) if xe>=0
      ye <- (y-1 to y+1) if (ye >=0 && (xe,ye) != (x,y))
    } yield new FlatCoordinates(xe,ye)

    override def toString=s"($x,$y)"
  }

Fajne jest też to, że mając w Scali REPLa można sobie porobić "Eksperyment Driven Development" (bo musi być jakieś driven development) i posprawdzać czy działa

import LifeLibrary._
LiveCell.evolve(1)
LiveCell.evolve(2)
LiveCell.evolve(3)
DeadCell.evolve(1)
DeadCell.evolve(2)
DeadCell.evolve(3)
/*
res0: LifeLibrary.Cell = DeadCell
res1: LifeLibrary.Cell = LiveCell
res2: LifeLibrary.Cell = LiveCell
res3: LifeLibrary.Cell = DeadCell
res4: LifeLibrary.Cell = DeadCell
res5: LifeLibrary.Cell = LiveCell
 */

new FlatCoordinates(0,0).neighbours
new FlatCoordinates(2,2).neighbours
//res6: Seq[LifeLibrary.Coordinates] = Vector((0,1), (1,0), (1,1))
//res7: Seq[LifeLibrary.Coordinates] = Vector((1,1), (1,2), (1,3), (2,1), (2,3), (3,1), (3,2), (3,3))

No to było trochę gadania o tym by się nie bać eksperymentować to zobaczmy czy można jednocześnie wprowadzić odpowiednią abstrakcje planszy i wykorzystać tak czy inaczej dziedziczoną metode "toString"

 trait Board{
    def liveNeighbourhood:Iterable[Coordinates]
    def cellAt(c:Coordinates):Cell
  }

  class FlatBoard(board:Map[Coordinates,Cell]) extends Board{
    val newLine=System.getProperty("line.separator")

    override def liveNeighbourhood: Iterable[Coordinates] =
      board.keySet ++ board.keySet.flatMap(_.neighbours)

    override def toString: String ={
      val cells=for {
        y<- (0 to 10)
        x<- (0 to 10)
      } yield (x,y,board.getOrElse(new FlatCoordinates(x,y),DeadCell))

      cells.map{
        case (10,y,LiveCell) => "O"+newLine
        case (x,y,LiveCell) => "O"
        case (10,y,DeadCell) => "X"+newLine
        case (x,y,DeadCell) => "X"
      }.mkString(",")
    }
    override def cellAt(c: Coordinates): Cell = board.getOrElse(c,DeadCell)
  }

Wygląda dziwnie, trochę strasznie i niepokojąco. No dobra to było tak w kwestii przypomnienia, a teraz...

Czas an rzeźnię

Research results from the psychology of
programming indicate that expertise in programming is far more strongly related to
the number of different programming styles understood by an individual than it is the
number of years of experience in programming

Code retreat to także szansa na naukę innego języka gdy obok ciebie usiądzie ktoś kto np. programuje w czymś czego na oczy nie widziałeś/widziałaś. Cytat powyższy (ten w szarej ramce) jest podobno autorstwa tego zioma Tim Budd, który jest jakimś tam badaczem IT. I pomimo, że nie chce mi się tego wersyfikować to cytat brzmi fajnie. A do tego np. koncept, który był w Effective Java odnośnie używania niemutowalnych obiektów w sumie lepiej można zrozumieć jak się trochę takiej niemutowalnej codzienności z FP uszczknie w bardziej funkcyjnych językach.

Teraz eksperyment będzie polegał na implementacji choćby kawałka game of life w Haskellu, który jako język czysto funkcyjny nie pozwala na oszukiwanie. I później przeniesienie tego podejścia na Jave8 i Scalę by nauczyć się programowania czysto funkcyjnego w tychże językach. Początek jest obiecujący bo typ Komórki tworzymy po prostu tak :

data Cell = LiveCell | DeadCell deriving (Show,Eq)

Pierwsza rzecz, to nie ma tutaj żadnych powiązań z pojęciem klasy jaką znamy z Javy bo Typ to Typ. A druga kwestia to to "deriving". Tutaj własnie używamy sławnych (bądź niesławnych) "Klas Typów" czyli mówimy, że nasz Typ będzie "porównywalny" i "wyświetlany".

Teraz nasze wymagania co do evolucji komórki

evolveLive 2 = LiveCell
evolveLive 3 = LiveCell
evolveLive _ = DeadCell

evolveDead 3 = LiveCell
evolveDead _ = DeadCell

Apropo tego przykładu można w koncu zczaić dlaczego Pattern Matching to "mechanizm funkcyjny". W scali to za bardzo przypominało rozbudowany "instanceof". a tutaj - przypomnijcie sobie, ze szkoły przypadki gdy definiuje się funkcje, która dla "0 ma taką wartość a dla x!=0 inną - jest to pattern matching na intach na tablicy kredą!!"

I teraz to będzie ciekawe by obadać jak wyjdzie implementacja tego samego w innych językach, najpierw scala

sealed trait Cell
object LiveCell extends Cell
object DeadCell extends Cell

val evolveLive:Int => Cell = n =>  n match{
  case 2|3 => LiveCell
  case _ => DeadCell
}

val evolveDead:Int => Cell = n =>  n match{
  case 3 => LiveCell
  case _ => DeadCell
}

Mamy mechanizm zapewniania typu zamkniętego przy pomocy słówka "sealed" oraz wbudowany pattern matching. A teraz dla porównania Java8.

enum Cell {LiveCell,DeadCell}

Function<Integer,Cell> evolveLive=n-> {
            switch (n) {
                case 2:
                case 3:
                    return LiveCell;
                default:
                    return DeadCell;
            }
        };

        Function<Integer,Cell> evolveDead=n-> {
            switch (n) {
                case 3:
                    return LiveCell;
                default:
                    return DeadCell;
            }
        };

No nie wiem, może robię coś źle że takie rozbudowane konstrukcje mi wychodzą.

Dalej jakiś currying

evolve :: Cell -> Int -> Cell
evolve LiveCell = evolveLive
evolve DeadCell = evolveDead

Takie coś powinno nam pozwolić mapować później sekwencyjnie komórki w jakiejś kolekcji. Jednocześnie znowu czas na refleksję - w obiektówce i tak pierwszym parametrem byłoby this z obiektu i jakieś dynamic dispatch. Tutaj niezależność tej funkcji sprawia, że moglibyśmy ją sobie przekazywać,dekorować, liftować i masa innych ciekawych patentów. I reszta :

Scala:
val evolve:Cell=>Int=>Cell= _ match {
  case LiveCell => evolveLive
  case DeadCell => evolveDead
}
Java:
  Function<Cell,Function<Integer,Cell>> evolve=c->{
            switch(c){
                case LiveCell: return evolveLive;
                default: return evolveDead;
            }
        };

W Javie w switchu ostatnie musi być default bo inaczej kompilator nie przepuszcza. I jeszcze trochę eksperymwntów w Replu Haskella

*GameLive> evolveLive 2
LiveCell
*GameLive> evolveLive 7
DeadCell
*GameLive> evolveDead 3
LiveCell
*GameLive> evolveDead 1
DeadCell

///evolution
*GameLive> :t evolve LiveCell 
evolve LiveCell :: Int -> Cell


*GameLive>  evolve LiveCell 2
LiveCell
*GameLive>  evolve LiveCell 4
DeadCell

Abstrakcja poprzez aliasy

Aby operować słowami domenowymi możemy zamiast tworzenia nowych klas (bo tutaj chyba jako takich klas obiektowych nie ma) stworzyć aliasy typów.

type Coordinates = (Int,Int)
type Board=Map Coordinates Cell

board :: Map Coordinates Cell
board  = Map.fromList [
  ((0,0), LiveCell),
  ((1,0), LiveCell),
  ((2,0), LiveCell)]

Co ciekawe po przeklejeniu tego kodu do Scali trzeba było tylko lekko poprawić definicje mapy i wsio się kompiluje.

type Coordinates = (Int,Int)
type Board=Map[Coordinates,Cell]

val board  = Map[Coordinates,Cell](
              ((0,0), LiveCell),
              ((1,0), LiveCell),
              ((2,0), LiveCell)
            )

Przygotowania do nowej rundy

neighbours (x,y) =[(xe,ye)|xe<-[x-1..x+1], ye<-[y-1..y+1] , ye>=0 ,xe>=0,(xe,ye)/=(x,y)]

liveCoordinates = Map.keys
coordinatesToEvolve liveCoordinates =  liveCoordinates >>= \c -> neighbours c

cellsToEvolve :: Board -> [Coordinates]
cellsToEvolve=Data.List.nub . coordinatesToEvolve . liveCoordinates

Ten kod dla javowca zapewne będzie szokiem ale generalnie nie ma tam nic odkrywczego. "neighbours (x,y)" to nic innego jak iloczyn kartezjański z wyłączeniem środka czyli wszyscy sąsiedzi 2D. Zaś następny kawałek kodu może być ciekawy "liveCoordinates >>= \c -> neighbours c" tak więc ">>=" to flat map a "\c-> neighbours c" to lambda. I w sumie jak na to teraz patrze to ta lambda jest tam nadmiarowa

coordinatesToEvolve liveCoordinates =  liveCoordinates >>= neighbours

Niestety dalej nie umiem tego uprościć a pewnie można. Teraz nauczymy się czegoś w Scali gdy nie ma do dyspozycji całej tej obiektówki

val neighbours:Coordinates=>Seq[Coordinates]= c=> {
  val (x,y)=c
  for {
    xe <- x-1 to x+1
    ye <- y-1 to y+1 if (xe>=0 && ye >=0 && (xe,ye)!=(x,y))
  } yield (xe,ye)
}

val liveCoordinates: (Board) => Seq[Coordinates] =(b:Board) => b.keys.toSeq
val coordinatesToEvolve=(liveCoordinates:Seq[Coordinates])=> liveCoordinates flatMap neighbours

I teraz zaczynają się dziać ciekawe rzeczy bo przez to, że nie widzę, żadnej "standalone" funkcji, która zostawi mi unikalne elementy w kolekcji to muszę kombinować. To jest ten moment gdzie ze względu na czysto funkcyjny charakter ćwiczenia obiektówka w scali nie pomaga bo definiuje metodę "distinct" na instancji.

I mogę to rozwiązać definiując swoją własną funkcję :

val unique=(c:Seq[Coordinates])=>c.distinct
val cellsToEvolve=
  unique compose coordinatesToEvolve compose liveCoordinates

Trochę to marnotrawstwo więc może jakaś metoda generyczna , ETA ekspansion i coś w ten deseń.

def uniqueGeneric[A](c:Seq[A])=c.distinct
val cellsToEvolve2:Board=>Seq[Coordinates]=
  (uniqueGeneric (_:Seq[Coordinates])) compose coordinatesToEvolve compose liveCoordinates

Ewentualnie można coś pokombinować z lambdami

val cellsToEvolve3:Board=>Seq[Coordinates]=
  ((_:Seq[Coordinates]).distinct) compose coordinatesToEvolve compose liveCoordinates

val cellsToEvolve4:Board=>Seq[Coordinates]=
  ((l:Seq[Coordinates])=> l.distinct) compose coordinatesToEvolve compose liveCoordinates

Jak widać rozwiązań jest kilka i każde ma w sobie coś przerażającego. Co do rzeczy przerażających pozostało nam jeszcze :

class Coordinates{
    public final Integer x;
    public final Integer y;

    Coordinates(Integer x, Integer y) {
        this.y = y;
        this.x = x;
    }

    @Override
    public String toString() {
        return "("+x+","+y+")";
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Coordinates that = (Coordinates) o;

        if (x != null ? !x.equals(that.x) : that.x != null) return false;
        return !(y != null ? !y.equals(that.y) : that.y != null);

    }

    @Override
    public int hashCode() {
        int result = x != null ? x.hashCode() : 0;
        result = 31 * result + (y != null ? y.hashCode() : 0);
        return result;
    }
}

aaa pola publiczne, wszyscy zginiemy!!! I jeszcze znajdowanie sąsiadów

public static Collection<Coordinates> neighbours(Coordinates c){
        return IntStream
                .rangeClosed(c.x - 1, c.x + 1)
                .boxed()
                .flatMap(x ->
                                IntStream.rangeClosed(c.y - 1, c.y + 1)
                                        .boxed()
                                        .map(y -> new Coordinates(x, y))

                ).filter(coord -> coord.x >= 0 && coord.y >= 0 && !c.equals(coord))
                .collect(Collectors.toList());
    }

Być może na zwykłych forach po bożemu byłoby to bardziej intuicyjne ale ćwiczenie polega - przypomnijmy - na implementacji jak najbardziej czysto funkcyjnej. I dla porównania kod powyżej robi dokładnie to samo co ta linijka w Haskellu :

neighbours (x,y) =[(xe,ye)|xe<-[x-1..x+1], ye<-[y-1..y+1] , ye>=0 ,xe>=0,(xe,ye)/=(x,y)]

Na podstawie tego eksperymentu nie polecałbym Haskella do organizacji gdzie nagradza się za ilość napisanych linii kodu (oni to chyba nazywają Performance Indikator czy coś takiego, taki koncepcik zabrany z już pożółkłych książek epoki industrialnej) natomiast polecałbym do organizacji gdzie każda zbędne linia kodu kosztuje.

I obiecana rzeźnia na koniec

howManyLiveNeighbours :: Board -> Coordinates -> Int
howManyLiveNeighbours b c =
  let neighbourCells=fmap (`Map.lookup` b) (neighbours c) in
  length $ filter (/= Nothing) neighbourCells

--nextTurn :: Board -> Board
--nextTurn =  cellsToEvolve

getCells board=fmap (Data.Maybe.fromMaybe DeadCell)  $ fmap (`Map.lookup`  board)  $  cellsToEvolve board

toLiveNeighbours=fmap (howManyLiveNeighbours  board) . cellsToEvolve

finalSequence=cellsToEvolve board  `zip` (getCells board `zip` toLiveNeighbours board)

Generalnie ten kod jest mieszanką mojej nieudolności w Haskellu i podpowiedzi z pluginu "Stylish Haskell", który robił co było w jego mocy. Dodatkowo nauczyłem się pisać komentarze w Haskellu.

Nauka

Pomimo, że ostatni wklejony akapit kodu jest czystym złem to jego celem nie jest ładnie wyglądać a stanowić szczebel w nauce. Na przykład jak idziesz pierwszy raz na siłkę to raz, że używasz małych ciężarów a dwa to ćwiczysz źle - po prostu źle. Każdy kto ćwiczy kiedyś zaczynał na siłce i 90% procent ludzi to rozumie i pomaga nowym (te 10% to ludzie którzy wstrzyknęli nie w tę żyłę oraz top managerowie, którzy muszą być top performent szczególnie na tle innych). Na szczęście jeszcze nie ma kursów "zarządzanie swoim wizerunkiem na siłowni" i nie trzeba udawać idealności w każdej pozycji z ciężarem w ręku. Podobnie Ty będziesz mógł nie przejmować się bzdurami new age w trakcie ćwiczeń bo kasujesz swój kod a z tego kodu co tutaj wkleiłem w tym artykule ludzie będą się jeszcze długo śmiali (przy optymistycznym założeniu, że ktoś to wszystko przeczyta)

1 komentarz:

  1. Ktoś przeczytał, ze sporym uśmiechem. Dzięki Pawle. Będę wracał.

    OdpowiedzUsuń