====== Haskell – typové třídy, monads, vstup/výstup, paralelizace ====== ===== Typové třídy ===== Typová třída je druh rozhraní, které definuje nějaké chování. Pokud je typ součástí nějaké typové třídy, znamená to, že podporuje a implementuje chování, jež ta typová třída definuje. Hodně lidí, co někdy programovalo v objektově orientovaných jazycích, je zmatených, protože si myslí, že jsou stejné jako objektové třídy. No, nejsou. Můžete je považovat za taková lepší javová rozhraní. Když se podíváme, jaký typ má například funkce **''==''** (tedy porovnání dvou prvků), tak je to : (==) :: (Eq a) => a -> a -> Bool Vidíme tu novou věc, symbol ''%%=>%%''. Údaje před symbolem ''%%=>%%'' se nazývají typová omezení. Můžeme přečíst předchozí deklaraci typu jako: funkce rovnosti vezme dvě libovolné hodnoty, které jsou stejného typu, a vrátí ''Bool''. Typ těchto dvou hodnot musí být instancí třídy ''Eq'' (to bylo typové omezení). Typová třída ''Eq'' poskytuje rozhraní pro testování rovnosti. Každý typ, u něhož dává smysl testovat dvě jeho hodnoty na rovnost, by měl být instancí třídy ''Eq''. Všechny standardní haskellové typy s výjimkou ''IO'' (typ, který obstarává vstup a výstup) a funkcí jsou součástí typové třídy ''Eq''. * **''Eq''** – Je použita pro typy podporující testování rovnosti. Funkce, implementované v této třídě, jsou ''=='' a ''/=''. Takže pokud je u nějaké typové proměnné omezení třídou **''Eq''**, funkce používá ve své definici operátor ''=='' nebo ''/=''. * **''Ord''** – Je typová třída podporující porovnávání. Je určena pro typy, na nichž je definováno uspořádání. Typová třída **''Ord''** pokrývá standardní porovnávací funkce jako jsou ''>'', ''<'', ''>='' a ''%%<=%%''. Funkce ''compare'' vezme dvě instance třídy **''Ord''** stejného typu a vrátí jejich uspořádání. Pro uspořádání je určeny typ **''Ordering''**, který může nabývat hodnot **''GT''**, **''LT''** nebo **''EQ''**, které znamenají (v tomto pořadí) //větší než//, //menší než// a //rovný//. * **''Show''** – Instance třídy ''Show'' může být převedena do řetězce. Nejpoužívanější funkce, jež je zahrnutá v typové třídě **''Show''**, je ''show''. Vezme hodnotu, která je instancí typu **''Show''** a převede ji na řetězec. * **''Read''** – Je něco jako opačná typová třída k **''Show''**. Funkce read vezme řetězec a vrátí typ, který je instancí třídy **''Read''**. * **''Enum''** – Instance třídy **''Enum''** jsou sekvenčně seřazené typy — mohou být vyjmenovány. Hlavní výhoda spočívá v tom, že třída **''Enum''** může být použita v rozsazích. Má definovány následníky a předchůdce, které můžeme dostat pomocí funkcí ''succ'' a ''pred''. * **''Bounded''** – Mají horní a spodní ohraničení. * **''Num''** – Její instance mají tu vlastnost, že se chovají jako čísla * **''Integral''** – Narozdíl od **''Num''**, která zahrnuje všechna čísla včetně reálných a celých, **''Integral''** obsahuje pouze celá čísla. V této typové třídě jsou typy ''Int'' a ''Integer''. * **''Floating''** – Obsahuje pouze čísla s plovoucí desetinnou čárkou, tedy ''Float'' a ''Double''. ===== Uživatelské datové typy ===== Síla typových tříd, jakožto i síla jakéhokoliv programovacího jazyka, je do značné míry určena možností tvorby a užití vlastních datových typů. ==== Typová synonyma ==== Mezi nejjednodušší vlastní typy v jazyku Haskell patří typová synonyma. Vytvářejí se tak, že dáme nové jméno již existujícímu typu, či složenině, např.: type ComplexF = (Float, Float) type Matrix a = [[a]] Jak je vidět, tak lze pojmenovat zcela konkrétní typ (''ComplexF''), nebo je možné typ parametrizovat (''Matrix''). Parametrizace snese více jak jeden parametr. Pro zapsání tohoto datového typu musíme použít konstrukci **''(3.5, 6.3) :: ComplexF''**. Tedy explicitně říct typ. Pouze zápis **''(3.5, 6.3)''** by byl chápán jako dvojice desetinných čísel. ==== Jednoduché datové typy ==== Kromě typových synonym je možné vytvářet i jednoduché datové typy. Od definice typového synonyma se neliší nijak výrazně. Kromě klíčového slova je to nutnost datového konstruktoru na začátku popisu typu. Pokud upravíme předchozí ukázku, tak můžeme psát např.: newtype ComplexC = ReIm (Float, Float) newtype MatrixC a = Matrix [[a]] Kromě rozdílů v definici a výsledku se liší i použití. Jednoduché datové typy už mají svůj konstruktor, takže pokud napíšeme v našem programu ''ReIm (3.5, 6.3)'', bude tento literál zcela jistě typu ''ComplexC''. Není nutné explicitně typovou signaturu přidávat. Pokud chceme, aby šly námi definované typy vypsat a nebo vzájemně porovnat, tak je potřeba implementovat příslušné funkce typových tříd. A nebo pokud se spokojíme s implicitním výpisem/porovnáním/uspořádáním, tak nemusíme nic implementovat. Stačí říct, že je datový typ odvozen od příslušných tříd, a když Haskell nenajde explicitní definice funkcí, tak si je sám domyslí. newtype ComplexC = ReIm (Float, Float) deriving Show ==== Komplexní datové typy ==== Nejpokročilejší forma uživatelských datových typů. Takto lze deklarovat výčtové typy, rošířené typy, parametrické typy a rekurzivní datové typy. Schema tvorby takového typu je následující: data Jméno_typu a1 a2 ... an = Konstruktor_1 | Konstruktor_2 ... | Konstruktor_m deriving (...) Přitom parametry typu a klauzule **''deriving''** jsou nepovinné. * **Výčtové typy** (hodnoty musejí být velkým písmenem!!!):data Color = Red | Green | Blue * **Rozšířené typy**: data Color' = Red | Green | Blue | Grayscale Int * **Parametrické typy**: data RGBColor a = RGBc a a a data CMYColor a = CMYc a a a data Color a = RGB (RGBColor a) | CMY (CMYColor a) | Grayscale a -- pouziti: rgb2grayscale (RGB (RGBc r g b)) = Grayscale ((2*r+4*g+2*b)/8) * **Rekurzivní typy**: Rekurzivní datové typy vlastně využívají naplno možností tvorby uživatelských typů v jazyku Haskell. V části parametrů konstruktorů sevšak již odkazují na sebe sama. Např. definice typu pro zásobník: data Stack a = Top a (Stack a) | Bottom deriving (Show) ===== Monády ===== Monády jsou určitý druh abstraktního datového konstuktoru, který místo dat uzavírá část programové logiky. Definice monád umožňují programátorovi spojovat jednotlivé akce dohromady a vytvořit zřetězení příkazů pro dosažení výsledku výpočtu. Jde vlastně o progamovou strukturu, která reprezentuje určitý výpočet. Ve funkcionálním programování výstup funkce záleží jen a pouze na vstupních parametrech. Nezáleží tedy na pořadí volání, kontextu a tak podobně. Představa je asi taková, že existuje seznam operací (příkazy, tak jak jdou za sebou). Monáda potom definuje operaci **''bind''**, která nějak spojí vždy dva za sebou jdoucí příkazy. Druhou operací, kterou monády implementují je **''return''**, která vezme data a vytvoří z nich monadickou strukturu. Použití nachází například u vstupně/výstupních operací. Myšlenka je taková, že se celé IO děje uvnitř monády, která těm IO operacím "skrytě" předává jako parametr "stav světa" a každá z těch operací vrací "aktualizovaný stav světa". Teď už ale hodně laicky řečeno. Jde o propojení funkcionálního světa s imperativním programovaním. Ale toto je opravdu je pro představu, protože to je jen jedna možnost použití. Další vlastností, je třeba definice backtracingu. Následující kód vrátí seznam všech dvojic mající součet 7: x <- [1, 2, 3, 4, 5] y <- [1, 2, 3, 4, 5] 7 <- x + y return (x, y) Obecně jde o nějakou konstrukci v teorii kategorií ===== Vstupy a výstup ===== Jedná se o monadické funkce. Jejich typem je vždy buď obyčejný datový typ s prefixem **''IO''**, tedy například **''IO Char''**, nebo akce nevracející nic má typ **''IO ()''**. Práce s těmito funkcemi musí být prováděna až za klíčovým slovem **''do''**: main :: IO () main = do c <- getChar putChar c Existuje mnoho funkcí pro práci se soubory. Například: * ''getChar'' – načtení znaku ze stdin * ''putChar'' – vložení znaku do stdout * ''openFile'' – otevření souboru * ''hClose'' – zavření souboru * ''hGetChar'' – načtení znaku obecně ze souboru * ''hPutChar'' – uložení znaku obecně do souboru * ''hGetContents'' – načtení dat ze souboru * ''hPutString'' – uložení řetězce do souboru ===== Paralelizace ===== Pracuje se zase v prostředí monád. Modul **''Control.Concurrent''** nám umožňuje pracovat s vlákny, která se dají vytvářet pomocí funkce **''forkIO''**. Tato vlákna můžeme synchronizovat přes hodnoty **''MVar''** a zasíláním zpráv přes kanály **''Chan''**. Tyto mechanismy nám pomáhají vyvarovat se typickým problémům uváznutí a vyhladovění.