Haskell Hero

Interaktivní učebnice pro začínající Haskellisty

Typy III

Proč typové třídy?

Z předchozích lekcí o typech víme, že typ je krabička obsahující hodnoty se společnými vlastnostmi. Například Integer je krabička obsahující všechna celá čísla.

Jakého typu je funkce (==)? Umí porovnávat celá čísla, takže bychom mohli říct, že je typu Integer -> Integer -> Bool. Dále umí porovnávat znaky, takže bychom mohli říct, že je typu Char -> Char -> Bool. V tuto chvíli nás napadnou polymorfní typy, takže řekneme, že funkce (==) je typu a -> a -> Bool. To by ovšem znamenalo, že i námi definované binární stromy se dají porovnávat na rovnost, což není pravda.

Jak tedy (==) otypovat? V tuto chvíli přichází na řadu typové třídy.

Krabičky krabiček


Znázornění typové třídy Eq

Tak toto je krabička Eq (z anglického equal), která obsahuje všechny typy hodnot, které se dají testovat na rovnost. Typ funkce (==) bude tedy následující:

(==)  ::  Eq a  =>  a -> a -> Bool
Znamená to funkce (==) může dostat dva argumenty jakéhokoli typu, který je z typové třídy Eq.

Jakého typu je funkce (+)? Umí sčítat celá čísla, umí sčítat desetinná čísla, ale neumí sčítat například hodnoty Bool. Chtěli bychom tedy říct, že umí sčítat cokoli číselného. Krabička všech čísel se jmenuje Num.


Znázornění typové třídy Num

Typ funkce (+) je tedy následující:

(+)  ::  Num a  =>  a -> a -> a
Třída Num zahrnuje mimo jiné i podtřídy Integral, což jsou všechna celá čísla, a Floating, což jsou všechna desetinná čísla. Nám bude stačit, když budeme znát krabičku Num.

Ještě zbývá zmínit krabičku Ord, kam patří všechno, co se dá seřadit podle velikosti. Jinak řečeno, všechny hodnoty, které lze mezi sebou porovnat operátory < a >.

Operátory < a > jsou tedy typu

(<)  ::  Ord a  =>  a -> a -> Bool

Vkládáme krabičky do krabiček

Nyní už víme, že existují funkce, které si jako argumenty berou "cokoli, co se dá testovat na rovnost". Například funkce

zipWith (==)  ::  Eq a => [a] -> [a] -> [Bool]
si bere dva seznamy čehokoli porovnatelného a vrací seznam pravdivostních hodnot. My bychom chtěli tuto funkci aplikovat například na dva seznamy typu [Nat], což jsou přirozená čísla, která jsme si definovali dříve. Jsou definována následovně:
data Nat  =  Zero  |  Succ Nat
Chtěli bychom, aby bylo možné provést následující vyhodnocení výrazu:
zipWith  (==)  [Zero, Succ Zero]  [Zero, Succ (Succ Zero)]

~>*  [True, False]
Takové vyhodnocení ale není možné, protože Nat nepatří do třídy Eq. Co s tím? Vložíme typ Nat do třídy Eq. Provedeme to tak, že zadefinujeme rovnost dvou hodnot typu Nat.


Kdy se dvě hodnoty typu Nat rovnají?

  • když jsou obě hodnoty Zero
  • když je první hodnota Succ x, druhá hodnota Succ y a x se rovná y, což se zjistí rekurzivně
  • ostatní případy (Zero, Succ x nebo Succ x, Zero) se sobě nerovnají

A přesně tak to zapíšeme. Nejprve musíme zmínit, co vlastně děláme. Vkládáme typ Nat do krabičky Eq, čili definujeme novou instanci třídy Eq.

instance Eq Nat
Doplníme definici rovnosti dvou prvků typu Nat podle výše uvedeného rozboru.
instance Eq Nat

  where Zero   == Zero    =  True
        Succ x == Succ y  =  x == y
        _      == _       =  False
kde v klauzuli Succ x == Succ y = x == y je == na pravé straně rekurzivně volaná ta samá funkce, neboť x i y jsou opět typu Nat, čili můžou být pouze ve tvaru Zero nebo Succ x.

A to je celé. Nyní můžeme hodnoty typu Nat používat všude, kde je vyžadován typ z třídy Eq.

Aby mohl být typ v typové třídě Ord, musí být zadefinována operace <=. U třídy Num už je to o něco složitější. Aby mohl být typ v třídě Num, musí být definovány následující operace:

  • (+) (sčítání)
  • (-) (odčítání)
  • (*) (násobení)
  • abs (absolutní hodnota)
  • fromInteger (funkce, která z celočíselného argumentu udělá číslo vkládaného typu)
  • signum (funkce, kde se výraz signum x vyhodnotí na 1, pokud je x kladné, na -1, pokud je x záporné, a na 0, pokud je x nula)