Už tomu jsou dva roky, co jsem si řekl, že chci poznat svět funkcionálního programování. O Haskellu jsem slyšel, že je to ten správný výběr, ale akademický. Takže jsem sáhl po jednom z těch populárnější, Scale. Zkusil jsem si kurz na Courseře a… no, nedokončil jsem. Scala mi nějak k srdci nepřirostla. Nebavilo mne v ní programovat. Asi tam na mne bylo stále moc Javy a málo prostoru pro funkcionální styl. Takže jsem přeci jen nakonec skončil u Haskellu.
Začít se učit funkcionální styly v Haskellu pro jeho puričnost je náročné. Několikrát mi málem explodovala hlava, jak je vše úplně jinak, než jsem doposud znal. Ale přežil jsem a jak se říká, co tě nezabije… to si zamiluješ. :-)
Uvědomil jsem si to ve chvíli, kdy jsem napsal Pytohní kód, který na mne zařval výjimkou. Že prý druhý řádek není callable…
ZeroDict = defaultdict(lambda: 0)
d = ZeroDict({'a': 1, 'b': 2})
Aneb technika známá jako partial application ve funkcionálním světě. Každá funkce bere ve skutečnosti pouze jeden parametr, což se v Haskellu velmi často prakticky využívá.
Jakmile jsem chybu opravil, zařvalo na mne NoneType object has no …
Něco, co
se mi v Haskellu nemůže stát. Prázdná hodnota totiž neexistuje. Pokud se
může něco pokazit, musí se taková událost explicitně vyjádřit. Což může znít
děsivě, ale díky monádám (žádný strach) se kód neprasí samými podmínkami a
pracuje se s ním mnohem lépe.
Například jsem si udělal jednoduchý RSS a Atom parser. Samotné poskládání dílčích celků dohromady vypadá nějak takto:
getSourcesFromDatabase >>= fetchFeeds >>= mapM createQuery >>= commitQueries
A pokud se něco po cestě pokazí, script nespadne. Normálně doběhne a každá část se může rozhodnout, jak se situací naložit. Může to znít, že nakonec ty podmínky oddře spodnější vrstva. Ano, musí se kód přizpůsobit, každopádně sám jsem podmínku v Haskellu nepoužil. Žádnou! Aspoň ne v podobě, jakou ji běžně známe. Ani cyklus.
Další skvělá věc je totiž pattern matching. Dalo by se to lehce přirovnat k overloaded funkcím na steroidech. Příklad k parsování RSS či Atom feedu (pro lepší pochopení: vstup je seznam tagů jak jdou za sebou v XML struktuře, včetně komentářů či textu):
parseFeed :: Tags -> Maybe Feed
parseFeed [] = Nothing
parseFeed ((TagOpen "rss" _):tags) = parseRss tags
parseFeed ((TagOpen "feed" _):tags) = parseAtom tags
parseFeed (_:tags) = parseFeed tags -- Skip XML declaration and comments at the beggining.
Hledá se známý tag rss
či feed
a podle toho se provede další parsování.
Jinak se pokračuje dál. A pokud nenajde, vrátí se Nothing
, což je možná
hodnota typu Maybe
. Při použití se pak musí s takovou možností explicitně
počítat. Ale jak bylo vidět výše, monády (což není nic jiného, než design
pattern) to podstatně zjednodušují.
Navíc vše je krásně vidět, čímž mám na mysli typy. Haskell je velice chytrý a umí si typy téměř vždy odvodit sám. Ale mohu mu je předat a pomoct si tak při debugování. Jinými slovy Haskell je silně typový, ale zároveň nehází programátorovi klacky pod nohy.
Na Haskellu je hezká další věc, na kterou nemám vhodnou ukázku, takže si půjčím jak najít pravoúhlý trojúhelník s obvodem 24 jednotek z Learn You a Haskell for Great Good:
ghci> let allTriangles = [(a,b,c) | c <- [1..], b <- [1..c], a <- [1..b]]
ghci> let solutions = filter (\(a,b,c) -> a^2 + b^2 == c^2 && a+b+c == 24) allTriangles
ghci> take 1 solutions
[(6,8,10)]
A to, že se problémy neřeší zapsáním algoritmu, ale matematickým zápisem – udělá se množina možných řešení (klidně nekonečných) a aplikují se různé transformace, kterými se odfiltrují nevhodná. Až zůstanou jen ta správná řešení.
Hm, opravdu nekonečných? No jasně! Však se mrkněte na ukázku, konkrétně právě
[1..]
generuje čísla do nekonečna. Haskell je totiž, jako správný
programátor, lazy. Dokud nemusí, nic neprovede. Pokud spustíte dlouho trvající
(či nekonečnou) funkci, ale nepoužijete její výsledek, Haskell ji vůbec
nevykoná. Což pak otevírá spoustu zajímavých možností řešit úkoly s
nekonečnem. Jako třeba tu, kde chci jen první řešení, takže jakmile se najde,
zbytek se vůbec nevyhodnotí.
Bohužel to taky má své stinné stránky. Sice to v některých případech oproti ostatním jazykům problémy řeší, v jiných naopak je potřeba dávat velký pozor. Aby něco takového fungovalo, musí si Haskell pamatovat stack, jak v případě potřeby danou hodnotu vypočítat. Což u velkých struktur může znamenat stack overflow. Tedy programátor na to musí myslet a na správných místech použít ty správné funkce či jinak vynutit vypočítat hodnotu, protože to je na nějakém místě prostě efektivnější.
Abych to shrnul: nikdy jsem si nemyslel, že se mi bude líbit jazyk, kde nejsou podmínky, cykly a prázdné hodnoty. Přišlo mi něco takového jako bláznovství. Ale po seznámení s Haskellem se mi takové praktiky opravdu líbí. Bohužel si myslím, že díky puričnosti, za což je často odsuzován, se do praxe moc nedostane. Ale osobně se mi to, překvapivě, opravdu líbí. Myslím si, že právě striktní oddělení pure funkcionálních funkcí dává méně prostoru pro chyby.
Mimochodem v Haskellu nejde debugovat s print příkazy jako třeba v Pythonu. Nejlepší způsob jsou testy. Takže mne to hezky nutí co nejvíce kódu umístit do lehce testovatelných pure funkcí a psát řádné testy vždy při vývoji, nikoliv až potom. A tím jsem zjistil, že TDD je pouze mentální switch. Nijak to nekomplikuje práci, je potřeba si na takový postup pouze zvyknout.
Pokud se chcete na Haskell také podívat, doporučuji tyto zdroje: