W tym roku data Global Day of Code Retreat niestety pokryła się z Mobilizacją dlatego my jako JUG Lodz tegoż wydarzenia nie organizowaliśmy nic mi nie wiadomo by ktoś inny podjął się tego zadania także w Łodzi ćwiczenia tej jesieni chyba się nie odbyły. Cóż - Zobaczymy czy daty za rok dopasują. Ale pomimo braku połączenia video z Koluszkami Górnymi czy innymi miastami wokół globu (oraz brakiem standardowego browara po zajęciach) można samemu przeprowadzić ćwiczenie i wyciągnąć jakąś naukę ze zdarzenia, które się technicznie nie odbyło.
Ograniczenia i przyzwyczajenia
Im dłużej pracujemy w pewien sposób tym bardziej ten sposób dopracowaliśmy do "lokalnej perfekcji" choćby jeżeli to jedynie mało wydajne "maksimum lokalne" produktywności. Gdy próbujemy wyjść ze starego sposobu to opuszczamy to maksimum lokalne i choćby jeżeli za rogiem czai się potężna "górka efektywności" (ku*wa chyba pójdę w kołczing) to w pierwszym momencie i tak będzie nam szło gorzej.
Stąd też pomysł aby nałożyć sztuczne ograniczenia w trakcie pisania programu, tak abyśmy byli zmuszeni wyjść poza znane nam mechanizmy (ej naprawdę nazwać to "poza cyfrową strefe komfortu", kupić trochę pączków i jeździć po Polsce ze szkoleniami). Oczywiście ograniczenia musza być dobrane w sposób sensowny aby kierować nas w dobra stronę - a skąd wiemy, która jest dobra? CodeRetreat ma pewne gotowe schematy sesji, które czerpią inspirację z doświadczenia ludzie mających ogromne doświadczenie praktyczne. A Skąd wiadomo, iż oni mają dobre doświadczenie praktyczne? Tego nie wiadomo nigdy ale taką wskazówką jest podejście tych ludzi do tematu nauki, kiedy to twierdzą iż rozumieją, iż w trakcie edukacji trzeba cały czas powracać do etapu początkującego (taka skromność edukacyjna).
Mainstreamowy CodeRetreat jest raczej nakierowany na Javę i dobre praktyki programowania obiektowego - czy można to podejście wykorzystać do nauki programowania funkcyjnego? Może ograniczenie "tylko funkcje", "żadnych przypisań", "nie zmieniaj stanu", "żadnych efektów ubocznych"? No w sumie z myślą o takiej sesji, zostało przygotowane specjalne narzędzie edukacyjne - nazywa się onoHaskell...
Kod
Znam (niestety) wiele opinii i podejść do Haskella - "tego się nie używa to po co się uczyć". A może języka warto się nauczyć dla ... samej nauki? (chociaż tutaj pewnie fani Haskella powiedzą, iż w praktyce tu czy tam się go używa). W Haskellu programowaniu funkcyjne jest "natywne" i przez to nauka tego podejścia idzie tam naturalnie i duzo łatwiej niż w Scali czy Javie. No jak na przykład w książce "Functional Programming in Java" jest cały rozdział o tym jak w Javie zasymulować tail recursion, które w Haskellu (a choćby w Scali) po prostu "jest" - to istnieje duże prawdopodobieństwo, iż standardowy programista Javy zawróci z tej ścieżki traktując funkcje w Java8 jako taki kosmetyczny dodatek do kolekcji. (to trochę tka jakby naukę programowania zaczynać od lutowania rezystorów do płytki - "Lekcja 3 - nakładamy kalafonię")
Zasady gry Game Of Life, którą to implementuje się na code retreat - opisane są tutaj : https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life. Jak jest tyle a tyle komórek a nasza jest żywa to przezywa albo umiera. I podobnie gdy jest martwa. Czyli dosyć interesujący zestaw wymagań gdzie występuje stan zarówno wewnątrz komórki jak i poza nią.
No to spróbujmy Haskella i zobaczmy najciekawsze fragmenty, które udało się wygenerować. Na początek w naszym kodzie - komórka. Gra życie polega na ewolucji komórek w kolejnych pokoleniach. Często ludzie słysząc, iż komórka ma dwa stany myślą boolean. A Komórka to Komórka. Więc prezentujmy Komórkę jako komórkę.
data Cell = Live | Dead deriving (Show,Eq)
next :: Cell -> Int -> Cell next Dead 3 = Live next Dead _ = Dead next Live 2 = Live next Live 3 = Live next Live _ = Dead
type Coordinates = (Int,Int) type Board = M.Map Coordinates Cell initial:: [Coordinates] initial = [(0,0),(0,1),(1,1),(2,1)] initialBoard ::M.Map Coordinates Cell initialBoard = M.fromList $ map (\c->(c,Live)) initial
Tu macz comprehenszyn
neighbours:: Coordinates -> [Coordinates] neighbours (x,y) = [(xn,yn) |xn <- [x-1..x+1],yn<-[y-1..y+1], (xn,yn)/=(x,y) ]
liveNeighbours :: Coordinates -> Board -> Int liveNeighbours c board = sum [1 | coord <- neighbours c,M.member coord board]
Końcówka
Końcówkę umieszczam w zasadzie tylko aby za rok zrobić porównanie. Generalnie metody wydają się krótkie ale jest to złudne gdyż te kilka linijek w Javie7 zajęłoby linijek kilkadziesiąt. Logika jest dosyć "skompaktowana" i wymaga od umysłu programisty odpowiedniej mocy obliczeniowej na rozpakowanie.
potentialCells :: [Coordinates] -> [Coordinates] potentialCells cs = [(xn,yn) | c<-cs,(xn,yn)<-neighbours c, xn>=0 , yn>=0] nextStep :: Board -> Board nextStep board =M.filter ( == Live) $ M.fromList $ map mapCell newCells where newCells = potentialCells $ M.keys board mapCell cord =(cord, next (M.findWithDefault Dead cord board) (liveNeighbours cord board))
map (map fst . Data.Map.toList) $ take 5 $ iterate nextStep initialBoard -- [[(0,0),(0,1),(1,1),(2,1)],[(0,0),(0,1),(1,1),(1,2)],[(0,0),(0,1),(0,2),(1,0),(1,1),(1,2)], [(0,0),(0,2),(1,0),(1,2),(2,1)],[(1,0),(1,2),(2,1)]]
Ograniczenia
- maks jeden $ w linii - generalnie każdy kolejny '$' można traktować jako kolejną linie kodu tyle że... w poziomie.
- brak zagnieżdzonych nawiasów "))" - kolejny sposób na wywołanie instrukcji w instrukcji
- brak where,let i list comprehension - tak by nie zamykać logiki w lokalnych aliasach
surrounding :: Int -> [Int] surrounding i = filter (>=0) [i-1,i,i+1]
zip [1,2] [3,4] >> [(1,3),(2,4)]
- zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
- liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c
:info [] data [] a = [] | a : [a] -- Defined in ‘GHC.Types’ instance Eq a => Eq [a] -- Defined in ‘GHC.Classes’ instance Monad [] -- Defined in ‘GHC.Base’ instance Functor [] -- Defined in ‘GHC.Base’ instance Ord a => Ord [a] -- Defined in ‘GHC.Classes’ instance Read a => Read [a] -- Defined in ‘GHC.Read’ instance Show a => Show [a] -- Defined in ‘GHC.Show’ instance Applicative [] -- Defined in ‘GHC.Base’ `` <- spełnia!spełnia! ojjjj spełnia! ...Ale skąd weźmiemy funkcję? To jest dosyć interesująca perspektywa poznawcza. Struktura z która pracujemy w zasadzie... jest funkcją
:t (,) >> (,) :: a -> b -> (a, b)i eksperyment :
Control.Applicative.liftA2 (,) [1,2] [3,4] >> [(1,3),(1,4),(2,3),(2,4)] Control.Applicative.liftA2 (,) [0,1,2] [1,2] >> [(0,1),(0,2),(1,1),(1,2),(2,1),(2,2)]
I to w zasadzie tyle.
Tyle?!? Ale przecież nieskończone!!!
Nie musi być skończone a choćby nie powinno dlatego sesje na code retreat realizowane są 45 minut i nie zakładają, iż ktoś skończy. I aby nie przywiązywać się do jednego rozwiązania kasujemy kod po sesji i to też własnie robię.
Podsumowanie
W ferworze implementacji wyszło mi coś takiego map (\(coord,cell)->(coord,next cell)) na co haskell-ide zareagowała "po co się meczysz z czymś takim zamiast użyć Control.Arrow.second"
:t Control.Arrow.second Control.Arrow.second :: Arrow a => a b c -> a (d, b) (d, c)