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í.