Haskell - vlastní typy
Užitečnou vlastnosti Haskellu jsou list comprehensions. V jednoduchých případech se chovají jako kombinace map
a filter
. Umožňují nám ze seznamu vybrat prvky, které splňují nějakou podmínku a rovnou na ně aplikovat nějakou funkci. Syntaxe je podobná matematické syntaxi pro výběr prvků z množiny. Např. pokud chceme ze seznamu xs
vybrat prvky menší než zvolené číslo, můžeme to napsat jako
mensiNez :: Ord a => a -> [a] -> [a]
mensiNez n xs = [x | x <- xs, x < n]
Pokud zároveň tyto prvky budeme chtít vynásobit dvěma, můžeme to napsat takhle:
mensiNez2 :: (Ord a, Num a) => a -> [a] -> [a]
mensiNez2 n xs = [2*x | x <- xs, x < n]
Samozřejmě s list comprehension jde dělat i složitější věci. Jeden užitečný příklad je např. seznam všech uspořádaných dvojic. Ten už se jen pomocí map
a filter
napsat nedá. Jak přesně tato konstrukce funguje si povíme později, až si budeme povídat o monádách.
usporadaneDvojice :: [a] -> [b] -> [(a,b)]
usporadaneDvojice xs ys = [(x,y) | x <- xs, y <- ys]
A co třeba jen dvojice čísel, jejichž součet je větší než zvolené n
?
usporadaneDvojiceVetsi :: (Ord a, Num a) => [a] -> [a] -> a -> [(a,a)]
usporadaneDvojiceVetsi xs ys n = [(x,y) | x <- xs, y <- ys, x + y > n]
Samozřejmě i v Haskellu si můžete nadefinovat svoje vlastní typy. Celý typový systém Haskellu je univerzálnější, než typové systémy, na které jste asi zvyklí z jiných programovacích jazyků.
Nejjednodušší variantou definice nového typu je to, co se v jiných jazycích nazývá enum. Prostě seznam konstant. Příkladem takové definice může být např. seznam různých barev, které chcete někde použít.
data Barva = Cervena | Zelena | Modra | Bila | Cerna deriving (Show)
Nový typ se definuje pomoci klíčového slova data
, za kterým následuje název typu (přesněji typový konstruktor), potom =
a seznam datových konstruktorů oddělených symbolem |
. V případě typu Barva
tedy Barva
je název typu (typový konstruktor) a seznam Cervena | Zelena | ...
jsou datové konstruktory. Proč se tomu říká konstruktor si povíme za chvíli. Prozatím si můžete představovat, že Cervená, Zelena
, atd. jsou možné hodnoty tohoto typu. Důležité je, že jak typové tak datové konstruktory musí začínat velkým písmenem. Poslední část definice nového typu (deriving (Show)
) říká, že typ patří do typové třídy Show
, tzn. že je možné jeho hodnoty vypisovat, bez toho by je vypsat nešlo. Haskell si sám vytvoří jednoduchou funkci, která typ zobrazuje, není třeba ji psát.
Když už máme nadefinovaný typ barva, můžeme například napsat funkci, která převádí barvu na trojici RBG hodnot.
barvaNaRGB :: Barva -> (Int, Int, Int)
barvaNaRGB Cervena = (255,0,0)
barvaNaRGB Zelena = (0,255,0)
barvaNaRGB Modra = (0,0,255)
barvaNaRGB Bila = (255,255,255)
barvaNaRGB Cerna = (0,0,0)
Můžete si všimnout, že i na nově definovaný typ funguje pattern matching.
Mít typ, ve kterém jde pouze reprezentovat pár konstant, ale pro reprezentaci všech barev nestačí. Hodilo by se nám, kromě možnosti mít barvu jako konstantu mít i možnost zadefinovat ji pomoci rgb složek. K tomu se hodí další typ. (Jména barev konci na RGB protože mohou být definována jen jednou.)
data BarvaRGB = CervenaRGB | ModraRGB | ZelenaRGB | BilaRGB | CernaRGB | RGB Int Int Int deriving (Show)
Všimněme si, že pro reprezentaci trojice se použije jiný datový konstruktor, v tomhle případě se jmenuje RGB
a jako parametry má 3 krát Int
.
Jaký typ ma RGB?
> :t RGB
RGB :: Int -> Int -> Int -> BarvaRGB
Datové konstruktory jsou tedy z pohledu Haskellu funkce, a to také vysvětluje, proč se jim říká konstruktory - vytváří z různých hodnot hodnotu typu BarvaRGB
.
Napišme teď funkci, která trojici RGB převede na barvu, použije konstantu, pokud ji pro takovou kombinaci máme, jinak použije RGB trojici.
rgbNaBarvu :: (Int, Int, Int) -> BarvaRGB
rgbNaBarvu (0,0,0) = CernaRGB
rgbNaBarvu (255,255,255) = BilaRGB
rgbNaBarvu (0,0,255) = ModraRGB
rgbNaBarvu (0,255,0) = ZelenaRGB
rgbNaBarvu (255,0,0) = CervenaRGB
rgbNaBarvu (a,b,c) = RGB a b c
Všimněte si univerzálnosti typového systému Haskellu. V typu BarvaRGB
může být uložena jak konstanta, tak trojice čísel. A to ještě zdaleka není všechno :).
Můžeme například nadefinovat typ Tvar
, do kterého budeme ukládat buď čtverec jako souřadnice levého horního rohu a délky strany, nebo kruh, jako souřadnice středu a poloměr.
data Tvar = Ctverec Int Int Int | Kruh Int Int Int
Podobný typ by se dal v C/C++ napsat jako union, v Javě a ostatních vyšších jazycích už potřebujeme dědičnost. Nebo si někam poznamenat, jestli ta trojice reprezentuje kruh nebo čtverec. V Haskellu nic z toho dělat nemusíme, funkce, která by pracovala s tímto typem může udělat pattern matching a rozhodnout se podle toho, jestli chce zrovna Kruh
, nebo Ctverec
.
Ale ani tady flexibilita typového systému v Haskellu nekončí. Kdo Haskell podcenil a myslel si, že nezvládne generické typy, tak se mýlí :).
Začneme pro jednoduchost typem, který nám umožní ukládat dvojice prvků stejného typu (na rozdíl od (a,b)
, kde a
a b
můžou mít různý typ).
data Pair a = P a a
Tady je teď vidět, proč se Pair a
říká typový konstruktor. Když za a
dosadíme např. Int
dostaneme teprve typ - Pair Int
, do kterého si můžeme uložit libovolně dva prvky typu Int
.
V příkladu nahoře jsme použili různé názvy pro typový a datový konstruktor, ale není to vůbec nutné. Mohli jsme klidně napsat něco jako
data Dvojice a = Dvojice a a deriving (Show)
V tomto případě se oba konstruktory jmenují stejně. Je to obvyklý způsob pojmenování konstruktoru.
Napišme teď funkce prvni
a druhy
, které vrací první a druhý prvek dvojice.
prvni::Dvojice a -> a
prvni (Dvojice a _) = a
druhy::Dvojice a -> a
druhy (Dvojice _ a) = a
Možná pro vás je pojmenování obou konstruktorů stejně trochu matoucí, ale zvyknete si. Důležité je si uvědomit, že typové konstruktory se objevují jen v definici typu funkce. V samotné funkci už se objevují jen datové konstruktory.
Vyzkoušejte si právě nadefinovanou funkci a potom napište funkci, která převádí naší dvojici na normální Haskellovsky pár (x,y)
.
naTuple :: Dvojice a -> (a,a)
naTuple (Dvojice a b) = (a,b)