beta

Typy a typové třídy

Věřte typům

búú

Již jsme zmínili, že Haskell má statický typový systém. Typ každého výrazu je znám už v době překladu, což vede k bezpečnějšímu kódu. Pokud napíšete program, kde se pokusíte dělit booleovský typ číslem, ani se ho nepodaří přeložit. To je dobré, protože je lepší odchytávat chyby tohoto druhu v čase překladu než aby program havaroval. Všechno v Haskellu má svůj typ, takže toho překladač ví o vašem programu celkem hodně, než ho vůbec začne překládat.

Na rozdíl od Javy nebo Pascalu má Haskell odvozování typů. Pokud napíšete číslo, nemusíte Haskellu říkat, že to je číslo. Může si ho odvodit sám, takže nemusíte explicitně vypisovat typy svých funkcí a výrazů pro jejich funkčnost. Zabývali jsme se základy Haskellu a na typy jsme se podívali jenom zběžně. Avšak porozumění typovému systému je velmi důležitá součást učení se Haskellu.

Typ je něco jako štítek, který je na každém výrazu. Říká nám, do jaké kategorie věcí výraz patří. Výraz True je booleovský, "ahoj" je řetězec apod.

A teď použijeme GHCi na zjištění typu nějakých výrazů. Učiníme tak pomocí příkazu :t, za který stačí napsat platný výraz pro zjištění jeho typu. Tak to teda prubneme.

ghci> :t 'a'
'a' :: Char
ghci> :t True
True :: Bool
ghci> :t "NAZDAR!"
"NAZDAR!" :: [Char]
ghci> :t (True, 'a')
(True, 'a') :: (Bool, Char)
ghci> :t 4 == 5
4 == 5 :: Bool

bomba Zde vidíme, že napsáním :t a výrazu vypíše zadaný výraz, následovaný :: a jeho typem. Čtyři tečky :: se čtou jako „má typ“. Explicitní typy jsou vždy označovány tak, že mají počáteční písmeno velké. Výraz 'a', jak můžeme vidět, má typ Char. Není těžké usoudit, že se jedná o znak (character). Výraz True je typu Bool. To dává smysl. Ale co je tohle? Prozkoumání typu výrazu "NAZDAR!" vypsalo [Char]. Hranaté závorky symbolizují seznam. Takže to čteme jako seznam znaků. Na rozdíl od seznamů, každá n-tice určité délky (arity) má svůj vlastní typ. Takže výraz (True, 'a') má typ (Bool, Char), kdežto výraz jako ('a','b','c') by měl mít typ (Char, Char, Char). Výraz 4 == 5 bude vždycky vracet False, tedy je typu Bool.

Funkce také mají typy. Když si píšeme vlastní funkci, můžeme se rozhodnout jí explicitně deklarovat typ. To je obecně považováno za dobrý zvyk, pokud se ale nejedná o velmi krátkou funkci. Zkusíme deklarovat typ funkcím, jež jsme zatím vytvořili. Pamatujete si na ten generátor seznamu, který filtroval řetězec, aby z něj zůstala pouze velká písmena? Tady je ukázáno, jak vypadá s typovou deklarací.

removeNonUppercase :: [Char] -> [Char]
removeNonUppercase st = [ c | c <- st, c `elem` ['A'..'Z']]

Funkce removeNonUppercase má typ [Char] -> [Char], což znamená, že se řetězec zobrazí na řetězec. Je tomu tak, protože vezme řetězec jako parametr a vrátí jiný jako výsledek. Typ [Char] má synonymum String, takže je srozumitelnější napsat removeNonUppercase :: String -> String. Nemusíme psát k této funkci její typ, protože si překladač může sám odvodit, že se jedná o funkci z řetězce do řetězce, ale přesto jsme to udělali. Ale jak zapíšeme typ funkce, která požaduje několik parametrů? Zde je jednoduchá funkce, jež vezme tři celá čísla a sečte je:

addThree :: Int -> Int -> Int -> Int
addThree x y z = x + y + z

Parametry jsou odděleny šipkou -> a tím pádem nejsou parametry a návratový typ explicitně odlišeny. Návratový typ je poslední položka v deklaraci a parametry jsou tady ty první tři položky. Později uvidíme, proč jsou všechny pouze odděleny ->, místo aby bylo více zřejmé oddělení parametrů od návratových typů, jako třeba Int, Int, Int -> Int nebo podobně.

Pokud chcete napsat ke své funkci typovou deklaraci, ale nejste si jistí, jaká by měla být, můžete vždycky funkci napsat bez ní a ověřit si to pomocí :t. Funkce jsou taktéž výrazy, takže :t bude s funkcemi fungovat bez problémů.

Tady je přehled několika běžných typů.

Int zastupuje celá čísla. Třeba 7 může být Int, ale 7.2 ne. Typ Int je ohraničený, což znamená, že má minimální a maximální hodnotu. Na 32bitových počítačích je obvykle maximum hodnoty typu Int 2147483647 a minimum -2147483648.

Integer zastupuje, ehm… také celá čísla. Hlavní rozdíl je v tom, že není ohraničený, takže může být použit pro vyjádření fakt fakt velkých čísel. Tím myslím fakt velkých. Nicméně typ Int je efektivnější.

factorial :: Integer -> Integer
factorial n = product [1..n]
ghci> factorial 50
30414093201713378043612608166064768844377641568960512000000000000

Float je reálné číslo s plovoucí desetinnou čárkou.

circumference :: Float -> Float
circumference r = 2 * pi * r
ghci> circumference 4.0
25.132742

Double je reálné číslo s plovoucí desetinnou čárkou a větší přesností!

circumference' :: Double -> Double
circumference' r = 2 * pi * r
ghci> circumference' 4.0
25.132741228718345

Bool je booleovský (logický) typ. Může nabývat pouze dvou hodnot: True and False.

Char zastupuje znak. Znak se zapisuje mezi dvě jednoduché uvozovky. Seznam znaků je řetězec.

N-tice mají také svůj typ, ale ten závisí na jejich velikosti a typu jednotlivých složek, takže může být teoreticky nekonečně typů n-tic, což je víc než můžeme popsat v tomhle tutoriálu. Všimněte si, že prázdná n-tice () je také typ, který může nabývat pouze jedné hodnoty: ().

Typové proměnné

Jaký si myslíte že je typ funkce head? Funkce head vezme seznam věcí libovolného typu a vrátí první prvek, takže jaký by to mohl být typ? Podívejme se na to!

ghci> :t head
head :: [a] -> a

krabice Hmmm! Co je to a? Je to typ? Vzpomeňte si, že jsme předtím tvrdili, že typy se zapisují velkým počátečním písmenem, takže to není zrovna typ. Protože to není napsáno velkým písmenem, je to ve skutečnosti typová proměnná. Což znamená, že a může být jakéhokoliv typu. Je to podobné jako generika v jiných jazycích, jenomže haskellová typová proměnná je mnohem užitečnější, protože nám umožňuje jednoduše psát obecné funkce, pokud není potřeba určitých typových specifik. Funkce, které obsahují typové proměnné, se nazývají polymorfní funkce. Typová deklarace funkce head uvádí, že vezme seznam libovolného typu a vrací jeden prvek stejného typu.

I když typové proměnné mohou mít názvy delší než jeden znak, obvykle je pojmenováváme a, b, c, d…

Pamatujete si funkci fst? Vrátí první složku z dvojice. Prozkoumejme její typ.

ghci> :t fst
fst :: (a, b) -> a

Vidíme, že fst vezme n-tici, jež obsahuje dva typy a vrátí prvek stejného typu, jaký má první složka. To je důvod, proč můžeme použít fst na dvojici, která obsahuje jakékoliv dva typy. Všimněte si, že ačkoliv jsou a a b různé typové proměnné, nemusí mít rozdílný typ. Pouze to uvádí, že je typ první složky a návratové hodnoty stejný.

Základy typových tříd

třída

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

Jaký typ má funkce ==?

ghci> :t (==)
(==) :: (Eq a) => a -> a -> Bool
Poznámka: operátor rovnosti == je funkce. Stejně jako +, *, -, / a skoro všechny operátory. Když se funkce skládá pouze ze zvláštních znaků, je obvykle považována za infixovou. Pokud se chceme podívat na její typ, předat ji jiné funkci nebo ji zavolat prefixově, musíme ji obklopit kulatými závorkami.

Zajímavé. 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.

Funkce elem je typu (Eq a) => a -> [a] -> Bool, protože využívá funkci == v seznamu, aby ověřila, jestli seznam obsahuje požadovanou hodnotu.

Některé základní typové třídy:

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 /=. Všechny typy, jež jsme zmínili předtím, kromě funkcí, jsou součástí Eq, takže mohou být testovány na rovnost.

ghci> 5 == 5
True
ghci> 5 /= 5
False
ghci> 'a' == 'a'
True
ghci> "Ho, ho" == "Ho, ho"
True
ghci> 3.432 == 3.432
True

Ord je typová třída podporující porovnávání. Je určena pro typy, na nichž je definováno uspořádání.

ghci> :t (>)
(>) :: (Ord a) => a -> a -> Bool

Všechny zatím probrané typy, kromě typů funkcí, jsou součástí třídy Ord. 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čený 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ý.

Aby mohl být typ instancí Ord, musí nejprve patřit do prestižní a exkluzivní třídy Eq.

ghci> "Abrakadabra" < "Zebra"
True
ghci> "Abrakadabra" `compare` "Zebra"
LT
ghci> 5 >= 2
True
ghci> 5 `compare` 3
GT

Instance třídy Show může být převedena do řetězce. Všechny zatím probrané typy, kromě typů funkcí, jsou součástí třídy Show. 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.

ghci> show 3
"3"
ghci> show 5.334
"5.334"
ghci> show True
"True"

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.

ghci> read "True" || False
True
ghci> read "8.2" + 3.8
12.0
ghci> read "5" - 2
3
ghci> read "[1,2,3,4]" ++ [3]
[1,2,3,4,3]

Zatím v pohodě. Opět jsou všechny typy zahrnuty v této typové třídě. Ale co se stane, jestliže zkusíme napsat read "4"?

ghci> read "4"
<interactive>:1:0:
    Ambiguous type variable `a' in the constraint:
      `Read a' arising from a use of `read' at <interactive>:1:0-7
    Probable fix: add a type signature that fixes these type variable(s)

GHCi se nám snaží sdělit, že neví, jakou hodnotu chceme vrátit. Všimněte si, že jsme v předchozích příkladech s read později něco dělali s výsledkem. Pomocí toho GHCi mohlo odvodit, jaký druh výsledku chceme dostat z funkce read. Pokud by to byl booleovský typ, GHCi by to vědělo a vrátilo by to jako Bool. Ale tady pouze ví, že chceme nějaký typ, který je součástí třídy Read, jenže neví, jaký. Podívejme se blíže na typ funkce read.

ghci> :t read
read :: (Read a) => String -> a

Vidíte? Vrátí typ, jenž je součástí Read, jenomže pokud ho nepoužijeme později, nebude mít možnost zjistit, jaký typ to je. To je důvod, proč bychom měli použít explicitní typovou anotaci. Typová anotace je způsob konkrétního určení typu nějakého výrazu. Uděláme to přidáním čtyř teček :: za výraz a poté uvedením typu. Sledujte:

ghci> read "5" :: Int
5
ghci> read "5" :: Float
5.0
ghci> (read "5" :: Float) * 4
20.0
ghci> read "[1,2,3,4]" :: [Int]
[1,2,3,4]
ghci> read "(3, 'a')" :: (Int, Char)
(3, 'a')

U většiny výrazů může překladač typ odvozovat sám. Ale někdy překladač neví, zdali má vrátit kupříkladu typ Int nebo Float pro výraz jako read "5". Protože je Haskell staticky typovaný jazyk, musí vědět všechny typy před tím, než je kód zkompilován (nebo v případě GHCi interpretován). Takže musíme říct Haskellu: „Hej, tenhle výraz má takovýhle typ, pro případ, že bys to nevěděl!“

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. Typy, jenž jsou zahrnuty do této třídy: (), Bool, Char, Ordering, Int, Integer, Float a Double.

ghci> ['a'..'e']
"abcde"
ghci> [LT .. GT]
[LT,EQ,GT]
ghci> [3 .. 5]
[3,4,5]
ghci> succ 'B'
'C'

Instance třídy Bounded mají horní a spodní ohraničení.

ghci> minBound :: Int
-2147483648
ghci> maxBound :: Char
'\1114111'
ghci> maxBound :: Bool
True
ghci> minBound :: Bool
False

Funkce minBound a maxBound jsou zajímavé, protože mají typ (Bounded a) => a. Jsou v jistém smyslu polymorfní konstanty.

Všechny n-tice jsou součástí třídy Bounded, pokud do ní patři i jednotlivé složky.

ghci> maxBound :: (Bool, Int, Char)
(True,2147483647,'\1114111')

Num je numerická typová třída. Její instance mají tu vlastnost, že se chovají jako čísla. Podívejme se na typ čísla.

ghci> :t 20
20 :: (Num t) => t

Vypadá to, že celá čísla jsou také polymorfní konstanty. Mohou se chovat jako typ, jenž je instancí typové třídy Num.

ghci> 20 :: Int
20
ghci> 20 :: Integer
20
ghci> 20 :: Float
20.0
ghci> 20 :: Double
20.0

Toto jsou typy z typové třídy Num. Jestliže se podíváme na typ operátoru *, uvidíme, že akceptuje veškerá čísla.

ghci> :t (*)
(*) :: (Num a) => a -> a -> a

Vezme dvě čísla stejného typu a vrátí výsledek, jenž má shodný typ. To je důvod, proč vyhodnocení výrazu (5 :: Int) * (6 :: Integer) skončí typovou chybou, kdežto 5 * (6 :: Integer) bude fungovat a výsledek bude typu Integer.

Aby se typ mohl přidat do Num, musí být už instancí tříd Show a Eq.

Integral je také numerická typová třída. 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.

Třída Floating obsahuje pouze čísla s plovoucí desetinnou čárkou, tedy Float a Double.

Pro zacházení s čísly je velmi užitečná funkce fromIntegral. Má deklarovaný typ fromIntegral :: (Num b, Integral a) => a -> b. Podle jejího typového omezení můžeme vidět, že vezme celé číslo a udělá z něj obecnější číslo. To je užitečné když chceme, aby spolupracovala celá a desetinná čísla. Například funkce length je typu length :: [a] -> Int, místo aby byla obecnějšího typu (Num b) => length :: [a] -> b. Řekl bych, že je to z historických důvodů nebo tak něco a zdá se mi to celkem hloupé. Každopádně, jestliže zkusíme zjistit délku seznamu a poté ji přičíst třeba k 3.2, dostaneme chybu, protože se snažíme spojit dohromady číslo typu Int a desetinné číslo. Je potřeba to obejít napsáním fromIntegral (length [1,2,3,4]) + 3.2, což to vyřeší.

Všimněte si, že funkce fromIntegral má několik typových omezení v definici typu. To je úplně v pořádku a jak můžete vidět, typová omezení se v kulatých závorkách oddělují čárkami.