beta

Moduly

Načítání modulů

moduly

Modul je v Haskellu kolekce souvisejících funkcí, typů a typových tříd. Program je v Haskellu kolekce modulů, kde hlavní modul načte ostatní moduly a poté použije funkce, které jsou v nich definované a něco pomocí nic udělá. Rozdělení kódu do několika modulů má celkem dost výhod. Jestliže je modul dostatečně obecný, funkce v něm mohou být použity ve velkém množství odlišných programů. Pokud je váš kód oddělený do samostatných modulů, které na sobě příliš nezávisí (říkáme jim také volně vázané), můžeme je později použít znovu. To dělá celou záležitost psaní kódu zvládnutelnější, když je kód rozdělený do více částí a každá část má svůj účel.

Haskellová standardní knihovna je rozdělená do modulů a každý z nich obsahuje funkce a typy, které spolu souvisí a slouží ke společnému účelu. Existuje modul pro zacházení se seznamy, modul pro souběžné programování, modul, jež se zabývá komplexními čísly atd. Všechny funkce, typy a typové třídy, se kterými jsme se zatím potkali, byly součástí modulu Prelude, jenž se importuje automaticky. V této kapitole prozkoumáme několik užitečných modulů a v nich obsažených funkcí. Ale nejprve se podíváme na importování modulů.

Syntax pro importování modulů v haskellovém skriptu je import <název modulu>. Musí se to napsat před definice jakýchkoliv funkcí, takže se importy většinou nachází na začátku souboru. Jeden skript může importovat samozřejmě více modulů — stačí každý import umístit na samostatný řádek. Naimportujme si modul Data.List obsahující několik užitečných funkcí pro práci se seznamy a použijme funkci, kterou exportuje, na vytvoření funkce, co nám řekne, kolik unikátních prvků seznam má.

import Data.List

numUniques :: (Eq a) => [a] -> Int
numUniques = length . nub

Když do nějakého souboru napíšete import Data.List, všechny funkce, které exportuje modul Data.List, se stanou dostupné v globálním jmenném prostoru, což znamená, že je můžete zavolat kdekoliv v daném skriptu. Funkce nub je definovaná v Data.List tak, že vezme seznam a vyřadí z něj duplicitní prvky. Složením funkcí length a nub napsáním length . nub vytvoří funkci, jež je ekvivalentní funkci \xs -> length (nub xs).

Můžete také vložit funkce z modulů do globálního jmenného prostoru GHCi. Když máte spuštěné GHCi a chcete mít možnost zavolat funkce, které exportuje modul Data.List, napište tohle:

ghci> :m + Data.List

Jestliže chceme načíst více modulů v GHCi, nemusíme psát :m + několikrát, můžeme prostě načíst několik modulů najednou.

ghci> :m + Data.List Data.Map Data.Set

Nicméně pokud načítáte skript, který už importuje nějaké moduly, nemusíte používat :m +, abyste k nim přistupovali.

Pokud potřebujete jenom pár funkcí z modulu, můžete si selektivně importovat jenom tyto funkce. Kdybychom chtěli importovat pouze funkce nub a sort z modulu Data.List, udělali bychom tohle:

import Data.List (nub, sort)

Můžete také chtít importovat všechny funkce z modulu kromě několika vybraných. To je často užitečné, když několik modulů exportuje funkci se stejným názvem a vy se chcete zbavit těch nepotřebných. Řekněme, že už máme svou vlastní funkci nazvanou nub a budeme chtít importovat všechny funkce z modulu Data.List kromě funkce nub:

import Data.List hiding (nub)

Jiný způsob, jak se vypořádávat s kolizemi, je používat kvalifikované importy. Modul Data.Map, nabízející datovou strukturu pro vyhledávání hodnot podle klíče, exportuje hromadu funkcí se stejným názvem jako mají funkce v Prelude, jako třeba filter nebo null. Takže jakmile importujeme Data.Map a poté zavoláme funkci filter, Haskell si nebude jistý, kterou z těch dvou funkcí použít. Tady je ukázané, jak to vyřešit:

import qualified Data.Map

Tohle zajistí, že když budeme chtít použít funkci filter z modulu Data.Map, musíme napsat Data.Map.filter, kdežto pouhé filter stále odkazuje na normální funkci filter, jak ji známe a milujeme. Vypisování Data.Map před každou funkci z toho modulu je celkem jednotvárné. To je důvod proč máme možnost přejmenovat kvalifikovaný import na něco kratšího:

import qualified Data.Map as M

Teď stačí pro použití funkce filter z modulu Data.Map napsat M.filter.

Rozhodně nahlédněte do této užitečné refereční dokumentace, abyste viděli, které moduly jsou ve standardní knihovně. Dobrý způsob, jak pochytit nové znalosti o Haskellu, je proklikávat se dokumentací a prozkoumávat moduly a jejich funkce. Můžete si také prohlížet zdrojový kód jednotlivých modulů Haskellu. Čtení kódu některých modulů je vážně dobrý způsob jak se naučit Haskell a získat pro něj náležitý cit.

Na hledání funkcí nebo zjišťování, kde jsou umístěné, používejte Hoogle. Je to opravdu skvělý haskellový vyhledávač. Můžete hledat podle názvu funkcí, modulů, nebo dokonce podle typu funkce.

Data.List

Modul Data.List se kupodivu celý věnuje seznamům. Poskytuje několik velice užitečných funkcí pro zacházení s nimi. Některé funkce jsme už potkali (jako map a filter), protože modul Prelude nechává příhodně exportovat některé funkce z Data.List. Nemusíte importovat Data.List kvalifikovaně, protože nekoliduje s žádným názvem z modulu Prelude, kromě těch, které Prelude už nakradlo z modulu Data.List. Pojďme se podívat na nějaké z funkcí se kterými jsme se zatím nesetkali.

Funkce intersperse vezme prvek a seznam a poté vloží ten prvek mezi každý pár prvků v seznamu. Tady je ukázka:

ghci> intersperse '.' "OPIČÁK"
"O.P.I.Č.Á.K"
ghci> intersperse 0 [1,2,3,4,5,6]
[1,0,2,0,3,0,4,0,5,0,6]

Funkce intercalate vezme seznam seznamů a seznam. Ten poté vloží mezi všechny ty seznamy a poté zarovná výsledek.

ghci> intercalate " " ["hej","nazdar","kluci"]
"hej nazdar kluci"
ghci> intercalate [0,0,0] [[1,2,3],[4,5,6],[7,8,9]]
[1,2,3,0,0,0,4,5,6,0,0,0,7,8,9]

Funkce transpose prohodí (transponuje) seznam seznamů. Pokud si představíte seznam seznamů jako 2D matici, řádky se stanou sloupci a naopak.

ghci> transpose [[1,2,3],[4,5,6],[7,8,9]]
[[1,4,7],[2,5,8],[3,6,9]]
ghci> transpose ["hej","nazdar","kluci"]
["hnk","eal","jzu","dc","ai","r"]

Řekněme, že máme polynomy 3x2 + 5x + 9, 10x3 + 9 a 8x3 + 5x2 + x - 1 a chceme je spojit dohromady. Můžeme je v Haskellu reprezentovat pomocí seznamů [0,3,5,9], [10,0,0,9] a [8,5,1,-1]. A teď, abychom je sečetli, jediné, co musíme udělat, je tohle:

ghci> map sum $ transpose [[0,3,5,9],[10,0,0,9],[8,5,1,-1]]
[18,8,6,17]

Když prohazujeme tyhle tři seznamy, třetí mocniny jsou pak v prvním řádku, druhé ve druhém a tak dále. Namapováním funkce sum na tento transponovaný seznam seznamů dosáhneme požadovaného výsledku.

nákupní seznamy

Funkce foldl', foldl1', foldr' a foldr1' jsou striktní verze svých příslušných líných podob. Když použijeme líný fold na opravdu velký seznam, může nastat chyba přetečení zásobníku. Viník této chyby je líná podstata foldů, protože hodnota akumulátoru se ve skutečnosti při skládání neaktualizuje. Co se děje ve skutečnosti je to, že akumulátor tak nějak slibuje, že spočítá svou hodnotu, jakmile se to po něm bude chtít. Takhle je to u každého akumulátoru s mezivýsledkem a všechny tyhle nahromaděné sliby přetečou váš zásobník. Striktní foldy nejsou líní lemplové a ve skutečnosti vypočítají mezivýsledky hned jak k nim přicházejí místo aby plnily zásobník sliby. Takže jestliže někdy dostanete chybu přetečení zásobníku při skládání seznamů, zkuste přejít na striktní verzi foldů.

Funkce concat (zřetězení) zarovná seznam seznamů na prostý seznam prvků.

ghci> concat ["foo","bar","baz"]
"foobarbaz"
ghci> concat [[3,4,5],[2,3,4],[2,1,1]]
[3,4,5,2,3,4,2,1,1]

Jenom to prostě odstraní jednu úroveň zanoření. Takže pokud chceme zarovnat výraz [[[2,3],[3,4,5],[2]],[[2,3],[3,4]]], který je seznam seznamů seznamů, musíme použít funkci concat dvakrát.

Napsáním funkce concatMap uděláme stejnou věc, jako kdybychom nejprve namapovali funkci na seznam a poté ji zřetězili pomocí funkce concat.

ghci> concatMap (replicate 4) [1..3]
[1,1,1,1,2,2,2,2,3,3,3,3]

Funkce and vezme seznam booleovských hodnot a vrátí True pouze pokud jsou všechny hodnoty v seznamu rovny True.

ghci> and $ map (>4) [5,6,7,8]
True
ghci> and $ map (==4) [4,4,4,3,4]
False

Funkce or je podobná funkci and, jenom vrátí True pokud nějaká z booleovských hodnot v seznamu je True.

ghci> or $ map (==4) [2,3,4,5,6,1]
True
ghci> or $ map (>4) [1,2,3]
False

Funkce any a all vezmou predikát a poté zkontrolují, zdali některý z prvků nebo všechny prvky v seznamu vyhovují predikátu, v tomto pořadí. Obvykle použijeme tyhle dvě funkce místo abychom na seznam namapovali funkce and nebo or.

ghci> any (==4) [2,3,5,6,1,4]
True
ghci> all (>4) [6,9,10]
True
ghci> all (`elem` ['A'..'Z']) "HEJKLUCIjakje"
False
ghci> any (`elem` ['A'..'Z']) "HEJKLUCIjakje"
True

Funkce iterate vezme funkci a počáteční hodnotu. Následně aplikuje funkci na počáteční hodnotu, poté aplikuje funkci na výsledek té aplikace funkce, poté opět funkci na výsledek předchozí aplikace atd. Vrátí všechny výsledky ve formě nekonečného seznamu.

ghci> take 10 $ iterate (*2) 1
[1,2,4,8,16,32,64,128,256,512]
ghci> take 3 $ iterate (++ "haha") "haha"
["haha","hahahaha","hahahahahaha"]

Funkce splitAt vezme číslo a seznam. Potom rozdělí seznam na pozici, kterou zadává číslo, a jako výsledek vrátí dva seznamy v n-tici.

ghci> splitAt 3 "hejchlape"
("hej","chlape")
ghci> splitAt 100 "hejchlape"
("hejchlape","")
ghci> splitAt (-3) "hejchlape"
("","hejchlape")
ghci> let (a,b) = splitAt 3 "foobar" in b ++ a
"barfoo"

Funkcička takeWhile je opravdu užitečná. Postupně tahá prvky ze seznamu, zatímco platí predikát, a poté, když narazí na prvek, jež nevyhovuje predikátu, je tahání přerušeno. To se ukázalo být velmi užitečné.

ghci> takeWhile (>3) [6,5,4,3,2,1,2,3,4,5,4,3,2,1]
[6,5,4]
ghci> takeWhile (/=' ') "Tohle je věta."
"Tohle"

Řekněme, že chceme znát součet všech třetích mocnin (přirozených čísel) menších než 10000. Nemůžeme namapovat výraz (^3) na seznam [1..], aplikovat nějaký filtr a poté to celé zkusit sečíst, protože filtrování nekonečného seznamu nikdy neskončí. Možná víte, že tahle řada prvků narůstá, ale Haskell ne. To je důvod, proč můžeme udělat tohle:

ghci> sum $ takeWhile (<10000) $ map (^3) [1..]
53361

Aplikujeme výraz (^3) na nekonečný seznam a hned poté co narazíme na prvek, který je větší nebo rovný číslu 10000, seznam je oříznut. Pak tedy můžeme výsledek jednoduše sečíst.

Funkce dropWhile je podobná, jenom zahazuje všechny prvky, dokud je predikát pravdivý. Jakmile se jednou predikát vyhodnotí jako False, vrátí se zbytek seznamu. Extrémně užitečná a půvabná funkce!

ghci> dropWhile (/=' ') "Tohle je věta."
" je věta."
ghci> dropWhile (<3) [1,2,2,2,3,4,5,4,3,2,1]
[3,4,5,4,3,2,1]

Byl nám zadán seznam představující hodnotu akcií k určitému datu. Tento seznam je vytvořený z n-tic jejichž první složka je hodnota akcií, druhá je rok, třetí měsíc a čtvrtá den. Chtěli bychom vědět, kdy hodnota akcií poprvé přesáhla tisíc dolarů!

ghci> let stock = [(994.4,2008,9,1),(995.2,2008,9,2),(999.2,2008,9,3),(1001.4,2008,9,4),(998.3,2008,9,5)]
ghci> head (dropWhile (\(val,y,m,d) -> val < 1000) stock)
(1001.4,2008,9,4)

Funkce span je podobná funkci takeWhile, jenom vrací dvojici seznamů. První seznam obsahuje všechno co by obsahovalo zavolání funkce takeWhile na stejný predikát a seznam. Druhý seznam obsahuje část seznamu, která by se zahodila.

ghci> let (fw, rest) = span (/=' ') "Tohle je věta." in "První slovo: " ++ fw ++ ", zbytek:" ++ rest
"První slovo: Tohle, zbytek: je věta."

Zatímco funkce span rozdělí seznam za místem, kde predikát platí, funkce break ho roztrhne tam, kde platí poprvé. Když napíšeme break p, je to stejné jako bychom napsali span (not . p).

ghci> break (==4) [1,2,3,4,5,6,7]
([1,2,3],[4,5,6,7])
ghci> span (/=4) [1,2,3,4,5,6,7]
([1,2,3],[4,5,6,7])

Po použití funkce break bude druhý výsledný seznam začínat prvním prvkem, který vyhovuje predikátu.

Funkce sort jednoduše seřadí seznam. Typ prvků v seznamu musí patřit do typové třídy Ord, protože pokud prvky v seznamu nemají stanovené uspořádání, seznam nemůže být seřazen.

ghci> sort [8,5,3,2,1,6,4,2]
[1,2,2,3,4,5,6,8]
ghci> sort "Tohle bude brzy serazeno"
"   Tabbdeeeehlnoorrsuyzz"

Funkce group vezme seznam a seskupí sousedící prvky, které jsou si rovny, do dalšího seznamu.

ghci> group [1,1,1,1,2,2,2,2,3,3,2,2,2,5,6,7]
[[1,1,1,1],[2,2,2,2],[3,3],[2,2,2],[5],[6],[7]]

Jestliže seřadíme seznam před seskupením, můžeme zjistit, kolikrát se v tom seznamu jednotlivé prvky vyskytují.

ghci> map (\l@(x:xs) -> (x,length l)) . group . sort $ [1,1,1,1,2,2,2,2,3,3,2,2,2,5,6,7]
[(1,4),(2,7),(3,2),(5,1),(6,1),(7,1)]

Funkce inits a tails jsou podobné funkcím init a tail, jenom s tím rozdílem, že se aplikují rekurzivně na seznam tak dlouho dokud něco ještě zbývá. Sledujte.

ghci> inits "w00t"
["","w","w0","w00","w00t"]
ghci> tails "w00t"
["w00t","00t","0t","t",""]
ghci> let w = "w00t" in zip (inits w) (tails w)
[("","w00t"),("w","00t"),("w0","0t"),("w00","t"),("w00t","")]

Zkusíme použít fold pro implementaci hledání částí seznamu.

search :: (Eq a) => [a] -> [a] -> Bool
search needle haystack =
    let nlen = length needle
    in  foldl (\acc x -> if take nlen x == needle then True else acc) False (tails haystack)

Nejprve zavoláme funkci tails na seznam ve kterém hledáme. Poté projdeme každý zbytek a zjistíme, jestli začiná tím stejným co hledáme.

Takhle jsme vlastně vytvořili funkci která se chová stejně jako isInfixOf. Funkce isInfixOf hledá v seznamu jeho část a vrací True jestliže se hledaná část vyskytuje někde v cílovém seznamu.

ghci> "zloděj" `isInfixOf` "jsem zloděj koček"
True
ghci> "Zloděj" `isInfixOf` "jsem zloděj koček"
False
ghci> "zloději" `isInfixOf` "jsem zloděj koček"
False

Funkce isPrefixOf a isSuffixOf hledají část seznamu na jeho začátku a konci, v tomto pořadí.

ghci> "hej" `isPrefixOf` "hej ty tam!"
True
ghci> "hej" `isPrefixOf` "sakra, hej ty tam!"
False
ghci> "tam!" `isSuffixOf` "hej ty tam!"
True
ghci> "tam!" `isSuffixOf` "hej ty tam"
False

Funkce elem a notElem zjišťují, jestli prvek je nebo není v daném seznamu.

Funkce partition vezme nějaký seznam a predikát a vrátí dvojici seznamů. První seznam ve výsledku obsahuje všechny prvky vyhovující predikátu, druhý obsahuje všechny nevyhovující.

ghci> partition (`elem` ['A'..'Z']) "BOBsidneyMORGANeddy"
("BOBMORGAN","sidneyeddy")
ghci> partition (>3) [1,3,5,6,3,2,1,0,3,7]
([5,6,7],[1,3,3,2,1,0,3])

Je důležité porozumět tomu, jak se tohle liší oproti funkcím span a break:

ghci> span (`elem` ['A'..'Z']) "BOBsidneyMORGANeddy"
("BOB","sidneyMORGANeddy")

Zatímco funkce span a break jsou hotovy jakmile narazí na první prvek který nevyhovuje nebo vyhovuje predikátu, funkce partition prochází celý seznam a rozděluje ho podle predikátu.

Funkce find vezme nějaký seznam a predikát a vrací první prvek který vyhovuje tomu predikátu. Avšak vrátí nám ho zabalený v datovém typu Maybe. Algebraické datové typy probereme více do hloubky v následující kapitole, ale prozatím nám stačí vědět, že hodnota datového typu Maybe může být buď Just něco nebo Nothing. Podobně jako seznam může být buď prázdný seznam nebo seznam obsahující nějaké prvky, Maybe je možná prázdné (neobsahuje nic) nebo možná obsahuje právě jeden prvek. A jako je typ seznamu kupříkladu celých čísel [Int], typ, který možná obsahuje celé číslo, je Maybe Int. V každém případě, pojďme si vzít naši funkci find na projížďku.

ghci> find (>4) [1,2,3,4,5,6]
Just 5
ghci> find (>9) [1,2,3,4,5,6]
Nothing
ghci> :t find
find :: (a -> Bool) -> [a] -> Maybe a

Všimněte si typu funkce find. Její výsledek je Maybe a. To je podobné jako když máme typ [a], akorát hodnota typu Maybe může obsahovat buď žádné prvky nebo jeden prvek, zatímco seznam může obsahovat žádné prvky, jeden prvek, nebo více prvků.

Vzpomeňte si jak jsme určovali, kdy poprvé hodnota našich akcií přesáhla tisíc dolarů. Vytvořili jsme výraz head (dropWhile (\(val,y,m,d) -> val < 1000) stock). Vzpomeňte si, že funkce head rozhodně není bezpečná. Co by se stalo, kdyby hodnota našich akcií nikdy nepřekonala tisíc dolarů? Naše aplikace funkce dropWhile by vrátila prázdný seznam a pokus o získání počátku toho seznamu by skončil běhovou chybou. Nicméně pokud to přepíšeme na find (\(val,y,m,d) -> val > 1000) stock, bude to mnohem bezpečnější. Jestliže naše akcie nikdy nepřesáhly tisíc dolarů (tedy jestliže žádný prvek nevyhovoval predikátu), dostali bychom hodnotu Nothing. Ale ten seznam obsahoval platnou odpověď, dostali bychom něco jako Just (1001.4,2008,9,4).

Funkce elemIndex je něco jako elem, jenom nevrací booleovskou hodnotu. Možná vrátí index prvku, který hledáme. Jestliže se v našem seznamu prvek nevyskytuje, vrátí se Nothing.

ghci> :t elemIndex
elemIndex :: (Eq a) => a -> [a] -> Maybe Int
ghci> 4 `elemIndex` [1,2,3,4,5,6]
Just 3
ghci> 10 `elemIndex` [1,2,3,4,5,6]
Nothing

Funkce elemIndices je podobná funkci elemIndex, jenom vrátí seznam indexů určujících vícenásobný výskyt prvku v našem seznamu. Jelikož používáme seznam pro znázornění indexů, nepotřebujeme typ Maybe, protože neúspěch se dá vyjádřit prázdným seznamem, který je synonymní s Nothing.

ghci> ' ' `elemIndices` "Kde tu jsou mezery?"
[3,6,11]

Funkce findIndex je podobná funkci find, ale jenom možná vrátí index prvního prvku, který vyhovuje predikátu. Podobně tak funkce findIndices vrátí indexy všech prvků, které vyhovují predikátu, ve formě seznamu.

ghci> findIndex (==4) [5,3,2,1,6,4]
Just 5
ghci> findIndex (==7) [5,3,2,1,6,4]
Nothing
ghci> findIndices (`elem` ['A'..'Z']) "Kde Tu Jsou Verzálky?"
[0,4,7,12]

Funkcemi zip a zipWith jsme se už zabývali. Vysvětlili jsme si, že tyto funkce sepnou dva seznamy, buď jako dvojici, nebo pomocí binární funkce (což je funkce se dvěma parametry). Ale co když chceme dát dohromady tři seznamy? Nebo sepnout tři seznamy pomocí funkce se třemi parametry? Tak k tomu nám slouží funkce zip3, zip4 atd. a funkce zipWith3, zipWith4 atd. Takhle to pokračuje až k číslu 7. I když se to může zdát nedostatečné, úplně to stačí, protože se nestává moc často, že byste potřebovali dávat dohromady osm a víc seznamů. Mimo to existuje velmi chytrý způsob, jak sepnout nekonečný počet seznamů, ale zatím jsme nepokročili natolik, abychom si to mohli ukázat.

ghci> zipWith3 (\x y z -> x + y + z) [1,2,3] [4,5,2,2] [2,2,3]
[7,9,8]
ghci> zip4 [2,3,3] [2,2,2] [5,5,3] [2,2,2]
[(2,2,5,2),(3,2,5,2),(3,2,3,2)]

Stejně jako u normálního spínání se seznamy, které jsou delší než nejkratší seznam z těch spínaných, oříznou na jeho velikost.

Funkce lines je užitečná, pokud se snažíme zpracovat soubory nebo vstup odněkud. Vezme řetězec a vrátí každý řádek toho řetězce jako položku seznamu.

ghci> lines  "první řádek\ndruhý řádek\ntřetí řádek"
["první řádek","druhý řádek","třetí řádek"]

Znak '\n' znázorňuje ukončení řádku v UNIXu. Zpětné lomítko má v haskellových řetězcích a znacích zvláštní význam.

Funkce unlines je opačná funkce k funkci lines. Vezme seznam řetězců a spojí je pomocí znaku '\n'.

ghci> unlines ["první řádek", "druhý řádek", "třetí řádek"]
"první řádek\ndruhý řádek\ntřetí řádek\n"

Funkce words a unwords jsou určené na rozdělování řádku textu na slova a na spojování seznamu slov do textu. Jsou dost užitečné.

ghci> words "hej tohle jsou slova v téhle větě"
["hej","tohle","jsou","slova","v","téhle","větě"]
ghci> words "hej tohle        jsou   slova v téhle\nvětě"
["hej","tohle","jsou","slova","v","téhle","větě"]
ghci> unwords ["hej", "nazdar", "kámo"]
"hej nazdar kámo"

Funkci nub jsme si již také zmínili. Vezme seznam a vytřídí z něj duplikátní prvky, což nám vrátí seznam, jehož každý prvek je sněhová unikátní vločka! Tahle funkce má celkem divné jméno. Zjistil jsem, že „nub“ znamená v angličtině malou hrudku nebo nezbytnou část něčeho. Podle mého názoru by měli používat opravdová slova pro názvy funkcí místo archaických.

ghci> nub [1,2,3,4,3,2,1,2,3,4,3,2,1]
[1,2,3,4]
ghci> nub "Hrozně moc slov a tak"
"Hrozně mcslvatk"

Funkce delete vezme nějaký prvek a k němu seznam a odstraní první výskyt toho prvku v tom seznamu.

ghci> delete 'h' "hej ty sthará bandho!"
"ej ty sthará bandho!"
ghci> delete 'h' . delete 'h' $ "hej ty sthará bandho!"
"ej ty stará bandho!"
ghci> delete 'h' . delete 'h' . delete 'h' $ "hej ty sthará bandho!"
"ej ty stará bando!"

Operátor \\ je rozdílová funkce pro seznamy. Chová se v zásadě jako rozdíl množin. Za každý prvek v pravém seznamu odstraní výskyt odpovídajícího prvku v levém.

ghci> [1..10] \\ [2,5,9]
[1,3,4,6,7,8,10]
ghci> "Já, velké dítě" \\ "velké"
"Já,  dítě"

Napsání [1..10] \\ [2,5,9] je stejné jako delete 2 . delete 5 . delete 9 $ [1..10].

Funkce union se také chová podobně jako množinová funkce. Vrátí sjednocení dvou seznamů. V podstatě projde všechny prvky ve druhém seznamu a připojí je k prvnímu, jestliže tam už nejsou obsaženy. Dávejte si ovšem pozor, že duplikáty z druhého seznamu zmizí!

ghci> "hej chlape" `union` "chlape jak je"
"hej chlapek"
ghci> [1..7] `union` [5..10]
[1,2,3,4,5,6,7,8,9,10]

Funkce intersect funguje jako průnik množin. Vrátí pouze ty prvky, které jsou nalezeny v obou seznamech.

ghci> [1..7] `intersect` [5..10]
[5,6,7]

Funkce insert vezme nějaký prvek seznam prvků, které mohou být porovnány, a vloží ho do toho seznamu. Vloží ho tam tak, aby všechny prvky, jež jsou větší nebo rovné, byly napravo od něj. Jestliže použijeme funkci insert pro vložení prvku do seřazeného seznamu, tento seznam zůstane seřazený.

ghci> insert 4 [1,2,3,5,6,7]
[1,2,3,4,5,6,7]
ghci> insert 'g' $ ['a'..'f'] ++ ['h'..'z']
"abcdefghijklmnopqrstuvwxyz"
ghci> insert 3 [1,2,4,3,2,1]
[1,2,3,4,3,2,1]

Funkce length, take, drop, splitAt, !! a replicate mají společnou vlastnost, že vezmou nějakou hodnotu typu Int jako jeden z jejich parametrů, přestože by mohlo být mnohem obecnější a použitelnější, kdyby prostě akceptovali jakýkoliv typ, který je součástí typových tříd Integral nebo Num (záleží na jednotlivých funkcích). Je tomu tak z historických důvodů. Nicméně, po opravě tohohle by přestalo fungovat velké množství existujícího kódu. To je důvod, proč modul Data.List obsahuje obecnější protějšky těchhle funkcí, pojmenované genericLength, genericTake, genericDrop, genericSplitAt, genericIndex a genericReplicate. Kupříkladu funkce length má typ length :: [a] -> Int. Když si zkusíme vypočítat průměr seznamu čísel napsáním let xs = [1..6] in sum xs / length xs, dostaneme typovou chybu, protože nemůžeme použít operátor / k celočíselnému dělení. Funkce genericLength má, na druhou stranu, typ genericLength :: (Num a) => [b] -> a. Protože se Num může chovat jako desetinné číslo, průměr se napsáním let xs = [1..6] in sum xs / genericLength xs spočítá bez problémů.

Každá z funkcí nub, delete, union, intersect a group má svůj obecnější protějšek nazvaný nubBy, deleteBy, unionBy, intersectBy a groupBy. Rozdíl mezi nimi je v tom, že první skupina funkcí používá operátor == pro testování rovnosti, zatímco ty s By vezmou porovnávací funkci a poté pomocí ní testují prvky v seznamu. Funkce group tedy odpovídá groupBy (==).

Kupříkladu si vezměme seznam popisující chování funkce každou sekundu. Chtěli bychom ho rozdělit do podseznamů podle toho, jestli hodnoty byly menší nebo větší než nula. Kdybychom použili obyčejné group, museli bychom seskupit pouze sousední hodnoty, které se rovnají. Ale my je chceme seskupit na základě toho, zdali jsou záporné nebo ne. Proto na scénu vstupuje funkce groupBy! Porovnávací funkce poskytovaná funkci s By by měla vzít dva prvky stejného typu a vrátit True, jestliže je dle svých standardů považuje za rovné.

ghci> let values = [-4.3, -2.4, -1.2, 0.4, 2.3, 5.9, 10.5, 29.1, 5.3, -2.4, -14.5, 2.9, 2.3]
ghci> groupBy (\x y -> (x > 0) == (y > 0)) values
[[-4.3,-2.4,-1.2],[0.4,2.3,5.9,10.5,29.1,5.3],[-2.4,-14.5],[2.9,2.3]]

Z příkladu jasně vidíme, které sekce jsou kladné a které záporné. Poskytnutá porovnávací funkce vezme dva prvky a poté vrátí True pouze pokud jsou oba záporné nebo kladné. Tato funkce může býta také zapsána jako \x y -> (x > 0) && (y > 0) || (x <= 0) && (y <= 0), ačkoliv si myslím, že ten první způsob je mnohem čitelnější. Ještě přehlednější způsob jak zapsat porovnávající funkce pro funkce s By je importování funkce on z modulu Data.Function. Tato funkce je definována jako:

on :: (b -> b -> c) -> (a -> b) -> a -> a -> c
f `on` g = \x y -> f (g x) (g y)

Takže výraz (==) `on` (> 0) vrátí porovnávací funkci, která odpovídá funkci \x y -> (x > 0) == (y > 0). Funkce on se hodně používá s funkcemi s By, protože pomocí ní můžeme napsat:

ghci> groupBy ((==) `on` (> 0)) values
[[-4.3,-2.4,-1.2],[0.4,2.3,5.9,10.5,29.1,5.3],[-2.4,-14.5],[2.9,2.3]]

To je opravdu hodně čitelné! Dá to i přečíst nahlas: seskup hodnoty pomocí porovnání jestli jsou prvky větší než nula.

Podobně také funkce sort, insert, maximum a minimum mají své obecnější protějšky. Funkce jako groupBy vezmou funkci, která určuje, kdy se dva prvky rovnají. Funkce sortBy, insertBy, maximumBy a minimumBy vezmou funkci, která určí, kdy je jeden prvek větší, menší nebo rovný druhému. Typ funkce sortBy je sortBy :: (a -> a -> Ordering) -> [a] -> [a]. Jestli si vzpomínáte z dřívějška, typ Ordering může nabývat hodnoty LT, EQ, nebo GT. Funkce sort odpovídá funkci sortBy compare, protože funkce compare prostě vezme dva prvky jejichž typ patří do typové třídy Ord, a vrátí jejich uspořádání.

Seznamy mohou být porovnávány, a když na to dojde, tak jde o lexikografické porovnání. Co když máme nějaký seznam seznamů a chtěli bychom ho seřadit ne podle obsahu vnitřních seznamů, ale podle jejich délky? No, pravděpodobně jste odhadli, že bychom na to použili funkci sortBy.

ghci> let xs = [[5,4,5,4,4],[1,2,3],[3,5,4,3],[],[2],[2,2]]
ghci> sortBy (compare `on` length) xs
[[],[2],[2,2],[1,2,3],[3,5,4,3],[5,4,5,4,4]]

Paráda! Část compare `on` length je v podstatě normální angličtina! Jestli si nejste jistí, k čemu tady funkce on je, tak výraz compare `on` length funguje stejně jako \x y -> length x `compare` length y. Když se zabýváte funkcemi s By využívajícími porovnávací funkce, obvykle napíšete (==) `on` něco, a když se zabýváte funkcemi s By využívajícími porovnávací funkce, obvykle napíšete compare `on` něco.

Data.Char

lego znak

Modul Data.Char dělá přesně to, co jeho název napovídá. Exportuje funkce zabývajícími se znaky. Také je užitečný při filtrování a mapování řetězců, protože to jsou vlastně jenom seznamy znaků.

Mimo jiné modul Data.Char exportuje znakové predikáty. To jsou funkce, které vezmou nějaký znak a řeknou nám o něm, jestli určité předpoklady platí nebo ne. Tady je máme:

Funkce isControl zkontroluje, zdali se jedná o řídící znak.

Funkce isSpace zkontroluje, zdali se jedná o prázdné znaky. To zahrnuje mezery, tabulátory, nové řádky apod.

Funkce isLower zkontroluje, zdali se jedná o minusku (malé písmeno).

Funkce isUpper zkontroluje, zdali se jedná o verzálku (velké písmeno).

Funkce isAlpha zkontroluje, zdali se jedná o písmeno.

Funkce isAlphaNum zkontroluje, zdali se jedná o písmeno nebo číslici.

Funkce isPrint zkontroluje, zdali se jedná o tisknutelný znak. Kupříkladu řídící znaky nejsou tisknutelné.

Funkce isDigit zkontroluje, zdali se jedná o číslici.

Funkce isOctDigit zkontroluje, zdali se jedná o oktalovou (osmičkovou) číslici.

Funkce isHexDigit zkontroluje, zdali se jedná o hexadecimální (šestnáctkovou) číslici.

Funkce isLetter zkontroluje, zdali se jedná o písmeno. (Je totožná s funkcí isAlpha.)

Funkce isMark zkontroluje, zdali se jedná o unikódové diakritické znaménko. To jsou znaky, které se kombinují s předcházejícími písmeny, aby vytvořily znak s diakritikou. Používejte tohle, pokud jste Francouz.

Funkce isNumber zkontroluje, zdali se jedná o číslici. (Je totožná s funkcí isDigit.)

Funkce isPunctuation zkontroluje, zdali se jedná o interpunkci.

Funkce isSymbol zkontroluje, zdali se jedná o elegantní matematický symbol nebo označení měny.

Funkce isSeparator zkontroluje, zdali se jedná o unikódovou mezeru nebo oddělovač.

Funkce isAscii zkontroluje, zdali se znak nachází mezi prvními 128 pozicemi znakové sady.

Funkce isLatin1 zkontroluje, zdali se znak nachází mezi prvními 256 pozicemi znakové sady.

Funkce isAsciiUpper zkontroluje, zdali se jedná o ASCII verzálku.

Funkce isAsciiLower zkontroluje, zdali se jedná o ASCII minusku.

Všechny tyhle predikáty jsou typu Char -> Bool. Většinou je budete používat k filtrování řetězců nebo k podobnému účelu. Kupříkladu řekněme, že vytváříme program, který požaduje uživatelské jméno a tohle uživatelské jméno se může sestávat pouze z alfanumerických znaků. Můžeme použít funkci all z modulu Data.List v kombinaci s predikátem z modulu Data.Char, abychom stanovili, zdali je uživatelské jméno v pořádku.

ghci> all isAlphaNum "bobby283"
True
ghci> all isAlphaNum "eddy ryba!"
False

Bezva. Pro případ že si nevzpomínáte, funkce all vezme predikát a nějaký seznam a vrátí hodnotu True pouze tehdy, když predikát vyhovuje každému prvku z daného seznamu.

Také můžeme použít funkci isSpace pro simulaci funkce words z modulu Data.List.

ghci> words "hej kluci to jsem já"
["hej","kluci","to","jsem","já"]
ghci> groupBy ((==) `on` isSpace) "hej kluci to jsem já"
["hej"," ","kluci"," ","to"," ","jsem"," ","já"]
ghci>

Hmmm, no, dělá to něco podobného co funkce words, ale zůstanou nám prvky obsahující mezery. Hmm, co bychom s tím mohli dělat? Já vím, odfiltrujeme ty zmetky.

ghci> filter (not . any isSpace) . groupBy ((==) `on` isSpace) $ "hej kluci to jsem já"
["hej","kluci","to","jsem","já"]

Ach.

Modul Data.Char také exportuje datový typ podobající se typu Ordering. Typ Ordering může nabývat hodnoty LT, EQ nebo GT. Je to jakýsi výčet. Popisuje několik možných výsledků, jež mohou nastat při porovnávání dvou prvků. Typ GeneralCategory je rovněž výčtový. Poskytuje nám několik možných kategorií, do kterých znak může spadat. Hlavní funkce pro získání obecné kategorie se nazývá generalCategory. Její typ je generalCategory :: Char -> GeneralCategory. Existuje nějakých 31 kategorií, takže si je sem všechny vypisovat nebudeme, ale pohrajeme si s touhle funkcí.

ghci> generalCategory ' '
Space
ghci> generalCategory 'A'
UppercaseLetter
ghci> generalCategory 'a'
LowercaseLetter
ghci> generalCategory '.'
OtherPunctuation
ghci> generalCategory '9'
DecimalNumber
ghci> map generalCategory " \t\nA9?|"
[Space,Control,Control,UppercaseLetter,DecimalNumber,OtherPunctuation,MathSymbol]

Vzhledem k tomu, že typ GeneralCategory je součástí typové třídy Eq, můžeme také testovat věci jako třeba generalCategory c == Space.

Funkce toUpper převede znak na verzálku. Mezery, čísla a podobné znaky zůstanou nezměněny.

Funkce toLower převede znak na minusku.

Funkce toTitle převede znak na titulkovou velikost. Pro většinu znaků odpovídá titulková velikost verzálce.

Funkce digitToInt převede znak na typ Int. Aby byl převod úspěšný, převáděný znak musí být v rozsazích '0'..'9', 'a'..'f' nebo 'A'..'F'.

ghci> map digitToInt "34538"
[3,4,5,3,8]
ghci> map digitToInt "FF85AB"
[15,15,8,5,10,11]

Opačná funkce k funkci digitToInt je intToDigit. Vezme hodnotu typu Int v rozsahu 0..15 a převede ho na číslici nebo minusku.

ghci> intToDigit 15
'f'
ghci> intToDigit 5
'5'

Funkce ord a chr převedou znak na jeho odpovídající číselnou hodnotu a naopak:

ghci> ord 'a'
97
ghci> chr 97
'a'
ghci> map ord "abcdefgh"
[97,98,99,100,101,102,103,104]

Rozdíl mezi hodnotami ord dvou znaků se rovná jejich vzdálenosti v tabulce Unicode.

Caesarova šifra je primitivní metoda pro zakódování zpráv, která posouvá každý znak o stanovený počet pozic v abecedě. Můžeme si sami jednoduše vytvořit obměnu Ceasarovy šifry, jenom se nebudeme omezovat na abecedu.

encode :: Int -> String -> String
encode shift msg =
    let ords = map ord msg
        shifted = map (+ shift) ords
    in  map chr shifted

Zde nejprve převedeme řetězec na seznam čísel. Poté přidáme posunutí každého čísla před převedením seznamu čísel zpátky na znaky. Jestliže jste skládací kovbojové, mohli byste zapsat tělo funkce jako map (chr . (+ shift) . ord) msg. Zkusme zkusit zakódovat několik zpráv.

ghci> encode 3 "Heeeeej"
"Khhhhhm"
ghci> encode 4 "Heeeeej"
"Liiiiin"
ghci> encode 1 "abcd"
"bcde"
ghci> encode 5 "Veselé Vánoce! Ho, ho, ho!"
"[jxjqî%[æsthj&%Mt1%mt1%mt&"

Zakódovalo se to bez problémů. Rozkódování zprávy je v podstatě pouhé posunutí o stejný počet míst jako se posouvalo při zakódování, jenom na druhou stranu.

decode :: Int -> String -> String
decode shift msg = encode (negate shift) msg
ghci> encode 3 "Jsem čajová konvička."
"Mvhp#Đdmryä#nrqylĐnd1"
ghci> decode 3 "Mvhp#Đdmryä#nrqylĐnd1"
"Jsem čajová konvička."
ghci> decode 5 . encode 5 $ "Tohle je věta."
"Tohle je věta."

Data.Map

Asociační seznamy (taktéž nazývané slovníky) jsou seznamy používané pro ukládání dvojic klíč-hodnota, u kterých nezáleží na pořadí. Kupříkladu můžeme použít asociační seznam na ukládání telefonních čísel, kde by telefonní čísla byla hodnotami a jména lidí by byla klíči. Nestaráme se o pořadí, v jakém jsou uloženy, stačí nám jenom získat správné telefonní číslo pro určitou osobu.

Nejzřejmější způsob znázornění asociačních seznamů v Haskellu by bylo mít seznam dvojic. První složka seznamu by byla klíč, druhá složka hodnota. Tady máme příklad asociačního seznamu s telefonními čísly:

phoneBook =
    [("betty","555-2938")
    ,("bonnie","452-2928")
    ,("patsy","493-2928")
    ,("lucille","205-2928")
    ,("wendy","939-8282")
    ,("penny","853-2492")
    ]

I přes zdánlivě podivné odsazení je tohle seznam dvojic řetězců. Nejběžnější úloha pro práci s asociačními seznamy je vyhledání nějaké hodnoty pomocí klíče. Napišme si funkci, která najde hodnotu podle jejího klíče.

findKey :: (Eq k) => k -> [(k,v)] -> v
findKey key xs = snd . head . filter (\(k,v) -> key == k) $ xs

Celkem jednoduché. Tahle funkce vezme klíč a seznam, profiltruje seznam, takže zůstanou pouze odpovídající klíče, vybere z nich první dvojici a vrátí danou hodnotu. Ale co se stane když klíč který hledáme není přítomen v asociačním seznamu? Hmm. V tomhle případě skončíme u pokusu získat první prvek z prázdného seznamu, což vyhodí běhovou chybu. Rozhodně bychom se měli vyhnout vytváření tak lehce havarovatelných programů, takže zkusme použít datový typ Maybe. Jestliže nenalezneme klíč, vrátíme hodnotu Nothing. Pokud ho ale nalezneme, vrátíme Just něco, kde něco je hodnota odpovídající klíči.

findKey :: (Eq k) => k -> [(k,v)] -> Maybe v
findKey key [] = Nothing
findKey key ((k,v):xs) = if key == k
                            then Just v
                            else findKey key xs

Podívejte se na typ funkce. Ta vezme klíč jež se dá porovnat, k němu nějaký asociační seznam a možná vyprodukuje nějakou hodnotu. Zní to celkem dobře.

Tohle je učebnicový příklad rekurzivní funkce pracující se seznamem. Okrajový případ, rozdělení seznamu na první prvek a zbytek, rekurzivní volání, je to tam všechno. Tohle je klasické skládání, takže se pojďme podívat, jak by se to dalo přepsat pomocí foldu.

findKey :: (Eq k) => k -> [(k,v)] -> Maybe v
findKey key = foldr (\(k,v) acc -> if key == k then Just v else acc) Nothing
Poznámka: je obvykle lepší použít foldy pro tuhle obyčejnou seznamovou rekurzi namísto explicitního rekurze, protože se jednodušeji čtou a rozeznávají. Každý ví, že se jedná o skládání, když vidí volání funkce foldr, ale k rozpoznání explicitní rekurze je potřeba více přemýšlení.
ghci> findKey "penny" phoneBook
Just "853-2492"
ghci> findKey "betty" phoneBook
Just "555-2938"
ghci> findKey "wilma" phoneBook
Nothing
lego mapa

Funguje to jedna radost! Jestliže máme v našem seznamu telefonní číslo holky, vybereme právě to číslo, jinak nic nevrátíme.

Právě jsme si vlastně napsali funkci lookup z modulu Data.List. Jestliže chceme najít odpovídající hodnotu ke klíči, musíme projít všechny prvky ze seznamu než na něj narazíme. Modul Data.Map nabízí asociační seznamy, jež jsou mnohem rychlejší (protože jsou vnitřně implementovány pomocí stromů), a také poskytuje velké množství užitečných funkcí. Odteď budeme říkat, že pracujeme s mapami místo s asociačními seznamy.

Protože modul Data.Map exportuje funkce, které by kolidovaly s těmi z modulů Prelude a Data.List, vyřešíme to kvalifikovaným importem.

import qualified Data.Map as Map

Vložte tuhle deklaraci importu do skriptu a poté ho načtěte do GHCi.

Pokročíme dál a podíváme se, co pro nás modul Data.Map může uložit! Tady je základní přehled jeho funkcí.

Funkce fromList vezme asociační seznam (ve formě seznamu dvojic) a vrátí mapu těchto asociací.

ghci> Map.fromList [("betty","555-2938"),("bonnie","452-2928"),("lucille","205-2928")]
fromList [("betty","555-2938"),("bonnie","452-2928"),("lucille","205-2928")]
ghci> Map.fromList [(1,2),(3,4),(3,2),(5,5)]
fromList [(1,2),(3,2),(5,5)]

Pokud existují duplikátní klíče v originálním asociačním seznamu, jsou zahozeny. Takhle vypadá typ funkce fromList:

Map.fromList :: (Ord k) => [(k, v)] -> Map.Map k v

Říká, že vezme nějaký seznam dvojic typů k a v a vrátí mapu která zobrazí klíče typu k na hodnoty typu v. Všimněte si, že když jsme vyráběli asociační seznamy z běžných seznamů, klíče musely být porovnatelné (jejich typ patřil do typové třídy Eq), ale teď musí být uspořádatelné. To je podstatné omezení modulu Data.Map. Potřebuje, aby klíče byly uspořádatelné, a mohl je tak uspořádat do stromu.

Měli byste vždycky použít modul Data.Map pro asociační seznamy, kromě případu, kdy byste měli klíče, které nejsou součástí typové třídy Ord.

Nulární funkce empty reprezentuje prázdnou mapu. Nepožaduje žádné argumenty, pouze vrátí prázdnou mapu.

ghci> Map.empty
fromList []

Funkce insert vezme klíč, hodnotu a mapu a vrátí novou mapu, která je stejná jako ta stará, jenom má v sobě navíc vložený klíč s hodnotou.

ghci> Map.empty
fromList []
ghci> Map.insert 3 100 Map.empty
fromList [(3,100)]
ghci> Map.insert 5 600 (Map.insert 4 200 ( Map.insert 3 100  Map.empty))
fromList [(3,100),(4,200),(5,600)]
ghci> Map.insert 5 600 . Map.insert 4 200 . Map.insert 3 100 $ Map.empty
fromList [(3,100),(4,200),(5,600)]

Můžeme si napsat svou vlastní funkci fromList za použití prázdné mapy, funkce insert a foldu. Sledujte:

fromList' :: (Ord k) => [(k,v)] -> Map.Map k v
fromList' = foldr (\(k,v) acc -> Map.insert k v acc) Map.empty

Je to celkem nekomplikovaný fold. Začínáme s prázdnou mapou, kterou skládáme zprava a průběžně vkládáme dvojice klíčů a hodnot do akumulátoru.

Funkce null ověří, jestli je mapa prázdná.

ghci> Map.null Map.empty
True
ghci> Map.null $ Map.fromList [(2,3),(5,5)]
False

Funkce size nahlásí velikost mapy.

ghci> Map.size Map.empty
0
ghci> Map.size $ Map.fromList [(2,4),(3,3),(4,2),(5,4),(6,4)]
5

Funkce singleton vezme klíč a hodnotu a vytvoří mapu o velikosti jedna.

ghci> Map.singleton 3 9
fromList [(3,9)]
ghci> Map.insert 5 9 $ Map.singleton 3 9
fromList [(3,9),(5,9)]

Funkce lookup funguje podobně jako ta z modulu Data.List, jenom pracuje s mapami. Vrátí Just něco, pokud nalezne pro něco klíč, a Nothing, pokud ne.

Funkce member je predikát, který vezme klíč a mapu a informuje, zdali se klíč v mapě vyskytuje nebo ne.

ghci> Map.member 3 $ Map.fromList [(3,6),(4,3),(6,9)]
True
ghci> Map.member 3 $ Map.fromList [(2,5),(4,5)]
False

Funkce map a filter fungují skoro stejně jako jejich seznamové protějšky.

ghci> Map.map (*100) $ Map.fromList [(1,1),(2,4),(3,9)]
fromList [(1,100),(2,400),(3,900)]
ghci> Map.filter isUpper $ Map.fromList [(1,'a'),(2,'A'),(3,'b'),(4,'B')]
fromList [(2,'A'),(4,'B')]

Funkce toList je opakem funkce fromList.

ghci> Map.toList . Map.insert 9 2 $ Map.singleton 4 3
[(4,3),(9,2)]

Funkce keys a elems vrátí seznam klíčů, respektive hodnot. Funkce keys dělá totéž co map fst . Map.toList a funkce elems totéž co map snd . Map.toList.

Funkce fromListWith je senzační funkcička. Chová se stejně jako funkce fromList, jenom nezahazuje duplicitní klíče, ale využije zadanou funkci pro rozhodnutí, jak s nimi naložit. Řekněme že holky mohou mít několik telefonních čísel a my máme zadán následující asociační seznam.

phoneBook =
    [("betty","555-2938")
    ,("betty","342-2492")
    ,("bonnie","452-2928")
    ,("patsy","493-2928")
    ,("patsy","943-2929")
    ,("patsy","827-9162")
    ,("lucille","205-2928")
    ,("wendy","939-8282")
    ,("penny","853-2492")
    ,("penny","555-2111")
    ]

Když bychom teď na vytvoření mapy použili funkci fromList, ztratili bychom několik čísel! Takže uděláme tohle:

phoneBookToMap :: (Ord k) => [(k, String)] -> Map.Map k String
phoneBookToMap xs = Map.fromListWith (\number1 number2 -> number1 ++ ", " ++ number2) xs
ghci> Map.lookup "patsy" $ phoneBookToMap phoneBook
"827-9162, 943-2929, 493-2928"
ghci> Map.lookup "wendy" $ phoneBookToMap phoneBook
"939-8282"
ghci> Map.lookup "betty" $ phoneBookToMap phoneBook
"342-2492, 555-2938"

Jestliže je nalezen duplicitní klíč, naše funkce je použita pro sloučení hodnot těchto klíčů do jiné hodnoty. Můžeme taktéž nejprve všechny hodnoty přetvořit na jednoprvkový seznam a poté použít operátor ++ na přidávání dalších čísel.

phoneBookToMap :: (Ord k) => [(k, a)] -> Map.Map k [a]
phoneBookToMap xs = Map.fromListWith (++) $ map (\(k,v) -> (k,[v])) xs
ghci> Map.lookup "patsy" $ phoneBookToMap phoneBook
["827-9162","943-2929","493-2928"]

Nádhera! Další možné použití je v případě, kdy vytváříme mapu z asociačního seznamu čísel, u kterého si chceme uchovat pouze největší z hodnot, jakmile je nalezen duplicitní klíč.

ghci> Map.fromListWith max [(2,3),(2,5),(2,100),(3,29),(3,22),(3,11),(4,22),(4,15)]
fromList [(2,100),(3,29),(4,22)]

Nebo bychom si hodnoty odpovídajících klíčů mohli přát sečíst.

ghci> Map.fromListWith (+) [(2,3),(2,5),(2,100),(3,29),(3,22),(3,11),(4,22),(4,15)]
fromList [(2,108),(3,62),(4,37)]

Funkce insertWith je ve stejném vztahu s funkcí insert, jako je funkce fromListWith s funkcí fromList. Vloží dvojici klíč-hodnota do mapy, ale pokud tato mapa již obsahuje daný klíč, zadaná funkce stanoví, co se má provést.

ghci> Map.insertWith (+) 3 100 $ Map.fromList [(3,4),(5,103),(6,339)]
fromList [(3,104),(5,103),(6,339)]

Tohle je pouze několik funkcí z modulu Data.Map. V dokumentaci můžete nalézt jejich kompletní seznam.

Data.Set

lego set

Modul Data.Set nám poskytuje množiny podobající se těm matematickým. Množiny jsou něco mezi seznamy a mapami. Všechny prvky v množině jsou unikátní. A protože jsou interně implementovány pomocí stromů (podobně jako mapy z modulu Data.Map), jsou taktéž seřazeny. Zjišťování příslušnosti, vkládání, mazání atd. je mnohem rychlejší než provádění stejných akcí se seznamy. Nejobvyklejšími operacemi na množinách jsou vkládání do množiny, zjišťování příslušnosti a převádění množiny na seznam.

Protože názvy funkcí z modulu Data.Set hodně kolidují s názvy v modulech Prelude a Data.List, musíme provést kvalifikovaný import.

Vložte tento příkaz do skriptu:

import qualified Data.Set as Set

… a poté tento skript načtěte přes GHCi.

Řekněme že máme dva různé texty. Chceme zjistit, jaké znaky jsou použity v obou dvou.

text1 = "Měl jsem zrovna anime sen. Anime... Skutečnost... Jak moc se liší?"
text2 = "Stařík převrhl popelnici a teď jsou odpadky rozházené po celém mém trávníku!"

Funkce fromList funguje zhruba tak jak byste čekali. Vezme libovolný seznam a převede ho na množinu.

ghci> let set1 = Set.fromList text1
ghci> let set2 = Set.fromList text2
ghci> set1
fromList " .?AJMSaceijklmnorstuvzíčěš"
ghci> set2
fromList " !Sacdehijklmnoprstuvyzáéíďř"

Jak můžete vidět, položky jsou seřazeny a každý prvek je unikátní. A teď se použijeme funkci intersection, abychom se podívali, které prvky mají oba texty společné.

ghci> Set.intersection set1 set2
fromList " Saceijklmnorstuvzí"

Můžeme také využít funkci difference na zjištění, která písmena jsou obsažena v první množině, ale ne ve druhé, a naopak.

ghci> Set.difference set1 set2
fromList ".?AJMčěš"
ghci> Set.difference set2 set1
fromList "!dhpyáéďř"

Nebo si můžeme nechat vypsat všechny unikátní znaky v obou řetězcích pomocí funkce union.

ghci> Set.union set1 set2
fromList " !.?AJMSacdehijklmnoprstuvyzáéíčďěřš"

Funkce null, size, member, empty, singleton, insert a delete fungují přesně tak jak byste čekali.

ghci> Set.null Set.empty
True
ghci> Set.null $ Set.fromList [3,4,5,5,4,3]
False
ghci> Set.size $ Set.fromList [3,4,5,3,4,5]
3
ghci> Set.singleton 9
fromList [9]
ghci> Set.insert 4 $ Set.fromList [9,3,8,1]
fromList [1,3,4,8,9]
ghci> Set.insert 8 $ Set.fromList [5..10]
fromList [5,6,7,8,9,10]
ghci> Set.delete 4 $ Set.fromList [3,4,5,4,3,4,5]
fromList [3,5]

Také můžeme zjišťovat, zdali je určitá množina podmnožinou či vlastní podmnožinou dané množiny. Množina A je podmnožinou množiny B, jestliže B obsahuje všechny prvky z množiny A. Množina A je vlastní podmnožina množiny B, jestliže B obsahuje všechny prvky z množiny A, ale v množině B se musí nacházet více prvků než v A.

ghci> Set.fromList [2,3,4] `Set.isSubsetOf` Set.fromList [1,2,3,4,5]
True
ghci> Set.fromList [1,2,3,4,5] `Set.isSubsetOf` Set.fromList [1,2,3,4,5]
True
ghci> Set.fromList [1,2,3,4,5] `Set.isProperSubsetOf` Set.fromList [1,2,3,4,5]
False
ghci> Set.fromList [2,3,4,8] `Set.isSubsetOf` Set.fromList [1,2,3,4,5]
False

Rovněž můžeme v množinách využívat funkce map a filter.

ghci> Set.filter odd $ Set.fromList [3,4,5,6,7,2,3,4]
fromList [3,5,7]
ghci> Set.map (+1) $ Set.fromList [3,4,5,6,7,2,3,4]
fromList [3,4,5,6,7,8]

Množiny jsou často používány pro vyčištění seznamu od duplikátů, a to tak, že se seznam převede na množinu pomocí funkce fromList, která se následně převede zpět na seznam pomocí funkce toList. Funkce nub z modulu Data.List tohle umí také, ale vyřazení duplikátů z většího seznamů je mnohem rychlejši, když ho nacpeme do množiny a poté ho přeměníme zpátky na seznam, než abychom použili funkci nub. Jenže funkci nub stačí, aby prvky daného seznamu byly součástí typové třídy Eq, kdežto pokud chcete nacpat nějaké prvky do množiny, musí patřit do typové třídy Ord.

ghci> let setNub xs = Set.toList $ Set.fromList xs
ghci> setNub "JAK TI DUPOU KRÁLÍCI?"
" ?ACDIJKLOPRTUÁÍ"
ghci> nub "JAK TI DUPOU KRÁLÍCI?"
"JAK TIDUPORÁLÍC?"

Naše funkce setNub je na větších seznamech obecně rychlejší než funkce nub, ale jak můžete vidět, tak funkce nub zachovává pořadí prvků v seznamu, zatímce funkce setNub ne.

Vytváření vlastních modulů

vytváření modulů

Zkoumali jsme zatím pár skvělých modulů, ale jak si vytvoříme vlastní? Téměř každý programovací jazyk nám umožňuje rozdělit náš kód do více souborů a v Haskellu tomu není jinak. Při psaní programů je dobrým zvykem vzít všechny funkce a data, jež slouží k podobnému účelu, a vložit do modulu. Takto můžete jednoduše opětovně použít tyto funkce v jiných programech pouhým importováním daného modulu.

Podívejme se na to, jak bychom si mohli napsat naše vlastní moduly vytvořením malého modulu, který poskytuje funkce pro výpočet objemu a povrchu několika geometrických objektů. Začneme vytvořením souboru nazvaného Geometry.hs.

Řekněme, že modul exportuje funkce. Co to znamená je to, že když importuji modul, mohu používat funkce, které exportuje. Mohou v něm být definovány funkce volající jiné vnitřní funkce, avšak my uvidíme a budeme používat pouze funkce exportované modulem.

Na začátku modulu si stanovíme jeho název. Jestliže máme soubor s názvem Geometry.hs, měli bychom náš modul pojmenovat Geometry. Poté určíme funkce, které budou exportovány, a pak začneme psát funkce. Takže začneme tímhle.

module Geometry
( sphereVolume
, sphereArea
, cubeVolume
, cubeArea
, cuboidArea
, cuboidVolume
) where

Jak můžete vidět, budeme počítat povrchy a objemy koulí, krychlí a kvádrů. Pustíme se tedy do toho a definujeme si naše funkce:

module Geometry
( sphereVolume
, sphereArea
, cubeVolume
, cubeArea
, cuboidArea
, cuboidVolume
) where

sphereVolume :: Float -> Float
sphereVolume radius = (4.0 / 3.0) * pi * (radius ^ 3)

sphereArea :: Float -> Float
sphereArea radius = 4 * pi * (radius ^ 2)

cubeVolume :: Float -> Float
cubeVolume side = cuboidVolume side side side

cubeArea :: Float -> Float
cubeArea side = cuboidArea side side side

cuboidVolume :: Float -> Float -> Float -> Float
cuboidVolume a b c = rectangleArea a b * c

cuboidArea :: Float -> Float -> Float -> Float
cuboidArea a b c = rectangleArea a b * 2 + rectangleArea a c * 2 + rectangleArea c b * 2

rectangleArea :: Float -> Float -> Float
rectangleArea a b = a * b

Máme tu celkem standardní geometrii, i když je tu pár věcí k povšimnutí. Protože je krychle speciální případ kvádru, definovali jsme si jeho povrch a objem tím, že jsme s ní nakládali jako s kvádrem jehož strany mají stejnou délku. Taktéž jsme si definovali pomocnou funkci nazvanou rectangleArea, která vypočítá obsah obdelníku v závislosti na délce jeho stran. Je to poměrně triviální, protože to je pouhé násobení. Všimněte si, že jsme tuto funkci využili v definicích jiných funkcí (jmenovitě cuboidArea a cuboidVolume), ale neexportovali jsme ji! Protože chceme, aby náš modul poskytoval pouze funkce pro zpracovávání trojdimenzionálních objektů, použili jsme funkci rectangleArea, ale neexportujeme ji.

Když vytváříme modul, obvykle exportujeme pouze ty funkce, které se chovají trochu jako rozhraní k našemu modulu, takže je samotná implementace skrytá. Jestliže někdo hodlá použít náš modul Geometry, nebude se muset zabývat funkcemi jež neexportujeme. Můžeme se rozhodnout tyto funkce úplně změnit nebo je odstranit v novější verzi (mohli bychom odstranit funkci rectangleArea a místo ní použít operátor násobení) a nikomu by to nevadilo, protože je vůbec neposkytujeme.

Pro použití našeho modulu stačí napsat:

import Geometry

Přičemž soubor Geometry.hs musí být ve stejném adresáři jako je program, který ho importuje.

Moduly také mohou být strukturovány hierarchicky. Každý modul může mít několik podmodulů a ty mohou mít své vlastní podmoduly. Oddělíme si naše funkce, aby Geometry byl modul mající tři podmoduly, každý pro jiný typ objektů.

Nejprve si vytvoříme adresář nazvaný Geometry. Dávejte pozor na velké písmeno G. V tomto adresáři si vytvoříme tři soubory: sphere.hs, cuboid.hs a cube.hs. Tady je výpis toho, co tyto soubory mají obsahovat:

sphere.hs

module Geometry.Sphere
( volume
, area
) where

volume :: Float -> Float
volume radius = (4.0 / 3.0) * pi * (radius ^ 3)

area :: Float -> Float
area radius = 4 * pi * (radius ^ 2)

cuboid.hs

module Geometry.Cuboid
( volume
, area
) where

volume :: Float -> Float -> Float -> Float
volume a b c = rectangleArea a b * c

area :: Float -> Float -> Float -> Float
area a b c = rectangleArea a b * 2 + rectangleArea a c * 2 + rectangleArea c b * 2

rectangleArea :: Float -> Float -> Float
rectangleArea a b = a * b

cube.hs

module Geometry.Cube
( volume
, area
) where

import qualified Geometry.Cuboid as Cuboid

volume :: Float -> Float
volume side = Cuboid.volume side side side

area :: Float -> Float
area side = Cuboid.area side side side

Prima! Takže první je modul Geometry.Sphere. Všimněte si, že je uložen v adresáři nazvaném Geometry a v modulu je pojmenován jako Geometry.Sphere. Totéž jsme udělali s kvádrem. Taktéž si všimněte, že jsme ve všech třech podmodulech definovali funkce se stejnými názvy. Můžeme si to dovolit, protože se jedná o oddělené moduly. Dále chceme použít v modulu Geometry.Cube funkce z modulu Geometry.Cuboid, ale nemůžeme toho docílit obyčejným příkazem import Geometry.Cuboid, protože by se exportovaly funkce se stejnými názvy jako jsou v Geometry.Cube. To je důvod, proč jsme použili kvalifikovaný import a všechno je v pořádku.

Tak když teď založíme soubor nacházející se ve stejné úrovni jako je adresář Geometry, můžeme napsat, řekněme:

import Geometry.Sphere

Poté zavoláme funkce area a volume a ty nám vypočítají povrch a objem koule. Když bychom si chtěli pohrát se dvěma nebo více těmito moduly, musíme je importovat kvalifikovaně, protože exportují funkce se stejnými názvy. Takže napíšeme něco takového:

import qualified Geometry.Sphere as Sphere
import qualified Geometry.Cuboid as Cuboid
import qualified Geometry.Cube as Cube

Pak máme možnost zavolat funkce Sphere.area, Sphere.volume, Cuboid.area atd. a každá nám vypočítá povrch nebo objem odpovídajícího objektu.

Až budete příště vytvářet soubor, který je opravdu velký a obsahuje hodně funkcí, zkuste se podívat, které funkce slouží společnému účelu a zkuste je oddělit do vlastního modulu. Jakmile bude váš program potřebovat nějakou funkci z tohoto modulu, bude pak pouze stačit ho naimportovat.