Moduly a typové třídy

Základem je dokumentace a tutoriál.

Moduly

Haskell podporuje modulární programování — každý program se může sestávat z modulů a podmodulů, které spolu vzájemně spolupracují. To napomáhá udržovatelnosti kódu, proto se u větších projektů bez rozdělení do modulů těžko obejdeme.

Import modulů

Pokud chceme nějaký modul využít, musíme ho zavést pomocí klíčového slova import. Zavedené moduly se musí nacházet ve zdrojovém kódu před definicemi našich funkcí a každý takový import musí být uveden na samostatném řádku. Příklad zavedení modulu pro práci se seznamy:

import Data.List

Pokud nám názvy funkcí z modulů kolidují, máme dvě možnosti: skrýt jednu z funkcí nebo zadávat plné jméno jedné nebo obou funkcí. Skrytím funkce z jednoho modulu můžeme bez problémů používat přímo funkci z modulu druhého. Příklad nahrazení funkce length za více obecnou (výpočet aritmetického průměru číselné řady od 1 po 1000):

import Prelude hiding (length)
import Data.List (genericLength)

length :: (Num i) => [b] -> i
length = genericLength

main :: IO ()
main = print $ sum x / length x
    where x = [1..1000]

První příkaz importuje celý modul Prelude kromě funkce length, druhý příkaz importuje pouze funkci genericLength z modulu Data.List, pomocí které si můžeme předefinovat funkci length, aniž bychom měnili zbylý kód.

V případě, kdy chceme využívat více funkcí se stejným názvem, můžeme celý modul importovat kvalifikovaně, tedy nastavit mu menší prioritu. Kupříkladu modul Data.Set obsahuje funkci map, která pracuje s množinami namísto se seznamy. Při použití této funkce bychom museli zadat plný název (tedy Data.Set.map). Kvalifikovaný import provedeme takto:

import qualified Data.Set

Pokud se nám celá cesta nechce vypisovat, není problém název modulu zkrátit:

import qualified Data.Set as S

Pak bychom k funkci map pro množiny přistupovali napsáním S.map.

Psaní vlastních modulů

Dle konvence bychom měli názvy modulů začínat velkým písmenem a při rozdělování do adresářů názvy oddělovat tečkou. Pokud např. v adresáři Modul budeme mít soubor Podmodul.hs, do tohoto souboru napíšeme:

module Modul.Podmodul
( funkce1
, funkce2
...
, funkcex
) where
...

Do závorek specifikujeme názvy funkcí, které se mají exportovat. V mnohém se to podobá rozhraní a viditelnosti funkcí z objektově-orientovaných jazyků. Pokud chceme exportovat i nějakou datovou strukturu, napíšeme za její název dvě tečky obklopené závorkami, což zpřístupní i všechny její konstruktory:

module Modul.Podmodul
( Struktura(..)
, funkce1
, funkce2
...
) where

data Struktura = Konstruktor
...

Typové třídy

V Haskellu se typy dají dělit do tříd. Ty slouží ke sloučení typů s podobnými vlastnostmi. Například všechna čísla patří do třídy Num a rozdělují se do několika dalších podtříd (Integral pro celá čísla, Rational pro zlomky, Fractional pro desetinná čísla, Floating pro čísla s plovoucí desetinnou čárkou a další). Kromě jednoduchého rozdělení do kategorií mohou být vlastnosti specifičtějšího charakteru, třeba uspořádatelnost (typová třída Ord), porovnatelnost (typová třída Eq), nebo převoditelnost na řetězec (typová třída Show). Tyto vlastnosti se dají k danému datovému typu připojit klíčovým slovem deriving a jejich chování definovat pomocí klíčového slova instance. Samotná typová třída se vytváří pomocí klíčového slova class. Příklad připojení vlastností k datovému typu:

data Grayscale = Black | Gray | White deriving (Show, Read)

Takto definovaný datový typ (výčet tří barev) můžeme převádět na řetězec a z řetězce pomocí funkcí show :: (Show a) => a -> String a read :: (Read a) => String -> a:

> show Gray
"Gray"
> read "Gray"

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

Funkce read potřebuje znát datový typ, do kterého řetězec převádíme.

Příklady

Zásobník

module Data.Stack
( Stack(..)
, stack
, empty
, push
, pop
, toList
, fromList
) where

data Stack a = Stack [a] deriving (Show, Read, Eq, Ord)

stack :: Stack a
stack = Stack []

empty :: Stack a -> Bool
empty (Stack xs) = null xs

push :: a -> Stack a -> Stack a
push x (Stack xs) = Stack (x:xs)

pop :: Stack a -> (a, Stack a)
pop (Stack (x:xs)) = (x, Stack xs)
pop (Stack      _) = error "The stack is empty."

toList :: Stack a -> [a]
toList (Stack xs) = reverse xs

fromList :: [a] -> Stack a
fromList xs = Stack (reverse xs)

Fronta

module Data.Queue
( Queue(..)
, queue
, empty
, enqueue
, dequeue
, toList
, fromList
) where

data Queue a = Queue [a] [a]
    deriving (Show, Read, Eq, Ord)

queue :: Queue a
queue = Queue [] []

empty :: Queue a -> Bool
empty (Queue xs ys) = null xs && null ys

enqueue :: a -> Queue a -> Queue a
enqueue x (Queue xs ys) = Queue (x:xs) ys

dequeue :: Queue a -> (a, Queue a)
dequeue (Queue []     []) = error "The queue is empty."
dequeue (Queue xs     []) = dequeue (Queue [] (reverse xs))
dequeue (Queue xs (y:ys)) = (y, Queue xs ys)

toList :: Queue a -> [a]
toList (Queue xs ys) = xs ++ reverse ys

fromList :: [a] -> Queue a
fromList xs = Queue xs []

← IB016