Típusosztályok

Alapprobléma

Egy műveletet szeretnénk több típusra is megvalósítani úgy, hogy az algoritmus függ a típustól. Példák ilyen műveletekre:

Ilyen problémák megoldására valók a Haskell típusosztályok.


A Haskell típusosztályok nem azonosak az objektumorientált programozásbeli osztályokkal. A legfőbb különbség, hogy a típusosztályokban nincsenek adatmezők, csak metódusok. Ezért a típusosztályok inkább a Java interfész fogalmához állnak közel.

Esetleges polimorfizmus (ismétlés)

Össze lehet adni Int, Double, Integer típusú értékeket, de Char típusú értékeket nem lehet összeadni.

A Haskellben ezt így fejezzük ki:

-- (+) :: Num a => a -> a -> a
-- instance Num Int
-- instance Num Double
-- instance Num Integer
-- -- instance Num Char  -- ilyen nincs!

Num a a típusban az úgynevezett kényszer.

Metódusok

Olyan dolgokat tudunk duplázni, amik összeadhatóak:

double :: Num a => a -> a
double a = a + a

Kérdés: A double visszavezethető (+)-ra. A (+) mire vezethető vissza?

Válasz: A (+) kódja típusfüggő, ezért nem írható le egyetlen általános definícióval, mint a double. Ezt úgy mondjuk hogy a (+) a Num osztály egy metódusa. Egy típusosztály nem más mint metódusok halmaza.

Osztálydefiníciók

A típusosztály definiálásakor fel kell sorolni az összes metódust. Egy egyszerű osztálydefiníció:

-- class Eq a where
--     (==) :: a -> a -> Bool

Ebből következően (==) típusa:

-- (==) :: Eq a => a -> a -> Bool

Elnevezések:

Lehetőség van többparaméteres típusosztály definiálására is.

Lehetőség van egy típusosztályban több metódus definiálására is.

Figyeljük meg hogy az (==) típusából nem látszik hogy ez a függvény metódus-e. Ez a kérdés nem is lényeges az (==) alkalmazása szempontjából.

Példányosítás

A példányosításkor definiálni kell az osztály összes metódusát az adott típusra. Egy egyszerű osztálypéldány:

data Dir = L | R
instance Eq Dir where
     L == L   = True
     R == R   = True
     _ == _   = False

Egy osztálynak egy típusra csak egy példánya lehet. Itt figyelembe kell venni az általános polimorfizmust is, így például egy osztálynak nem lehet példánya [Int]-re is és [a]-ra is.

(A GHC egyik kiterjesztése lehetővé teszi az átfedő osztálypéldányokat.)

Feladat: Pontok egyenlősége

Definiáljuk az egyenlőségvizsgálatot:

data P = P Int Int
instance Eq P where
    P a b == P c d  =  a == c  &&  b == d
 -- mintaillesztés, (&&)
Test>
False :: Bool

Feladat: Peano-számok egyenlősége

Definiáljuk az egyenlőségvizsgálatot:

data Nat          -- egy természetes szám
   = Zero         -- vagy nulla,
   | Succ Nat     -- vagy egy természetes szám rákövetkezője
instance Eq Nat where
    Zero   == Zero     = True
    Succ n == Succ m = n == m
    _      == _      = False
 -- mintaillesztés, rekurzió
Test>
True :: Bool

Feladat: Színek egyenlősége

Definiáljuk az egyenlőségvizsgálatot:

data Colour
    = Named String    -- névvel adott szín
    | RGB Int Int Int -- RGB komponensekkel adott szín
toRGB :: Colour -> (Int, Int, Int)

Test>
True :: Bool

(Az egyszerűség kedvéért csak néhány nevezett színnel foglalkozzunk.)

Feladat: Fák egyenlősége

Definiáljuk az egyenlőségvizsgálatot a következő adattípusra:

data T a 
    = E  
    | N (T a) a (T a)
instance Eq a => Eq (T a) where
    E       == E          = True
    N l x r == N l' x' r' = l==l' && x==x' && r==r'
    _       == _          = False
 -- mintaillesztés, rekurzió, (&&), (==)
Test>
True :: Bool

Alapértelmezett metódusok

A Preludebeli definíció:

-- class Eq a where
--     (==) :: a -> a -> Bool
--     a == b  =  not (a /= b)
--     (/=) :: a -> a -> Bool
--     a /= b  =  not (a == b)

A példány definiálásakor bármelyik metódust definiálhatjuk (mindkettőt is).


Felvetődik a kérdés, hogy a fenti megoldás mivel nyújt többet a következő megoldásnál:

Válasz: Az előző megoldásban megvan a lehetőség a (/=) függvény definiálására a típustól függően. Az előny még egyértelműbb a következő esetben:

Ha így definiáljuk az Eq osztályt, akkor a példányosításakor választhatunk, hogy a (==) vagy a (/=) metódust definiáljuk; a másik automatikusan definiált lesz. Van lehetőség mindkét metódus definiálására is. (Ha egyik metódust sem definiáljuk, a program kiértékelése végtelen ciklusba kerülhet, mivel a metódusok egymást hívják.)

Alosztályok

Az Ord típusosztály definíciója:

-- class Eq a => Ord a where
--     (<), (<=), (>=), (>) :: a -> a -> Bool
--     x <= y   =  x < y || x == y
--     x <  y   =  not (x >= y)
--     x >= y   =  x > y || x == y
--     x >  y   =  not (x <= y)

Az összehasonlításkor feltételezzük, hogy már van egyenlőségvizsgálat.


Az Eq a kényszer használatának az osztálydefinícióban két következménye lesz:

  1. Csak olyan a típusra definiálhatjuk Ord példányát, amire már van Eq példány.
  2. Az Ord a kényszer magában hordozza az Eq a kényszert

Az alosztály, ősosztály elnevezések összhangban vannak a objektumorientált nyelvekben használt hasonló fogalmakkal. Egy típusosztálynak lehet több ősosztálya is, azaz a többszörös öröklés lehetséges.

Az öröklési hierarchia nem tartalmazhat kört. Ennek nem is lenne értelme, mivel az ilyen esetekben az osztályokat összevonni érdemes:

Ehelyett:

Alosztályok használata

member :: Ord a => a -> T a -> Bool
member a E = False
member a (N l b r)
    | a <  b  =  member a l
    | a == b  =  True           
    | a >  b  =  member a r

Nincs Eq a kényszer, mégis használhatjuk az (==) műveletet, mivel Eq ősosztálya Ord-nak!

Származtatott osztálypéldányok

Az Eq, Ord, Enum, Show, Read példányosítása rábízható a fordítóra. Ezt az igényünket az adattípus definíciójánál a deriving kulcsszóval kell jeleznünk:

-- data Maybe a 
--     = Nothing 
--     | Just a
--         deriving (Eq, Ord, Show, Read)

Az Eq osztály származtatása

A származtatott Eq osztálypéldány a struktúrális egyenlőségvizsgálat. Két kifejezés struktúrálisan egyenlő, ha:

Eq nem származtatható olyan típusra aminek a definíciójában (->) szerepel.

Az Ord osztály származtatása

A származtatott osztálypéldány a struktúrális rendezés. Számít a típusdefinícióban a konstruktorok sorrendje:

-- data Maybe a 
--     = Nothing 
--     | Just a
--         deriving (Eq, Ord, Show, Read)
Test>
True :: Bool
Test>
True :: Bool
Test>
True :: Bool

Ha más sorrendben adtuk meg a konstruktorokat:

A többértelműségi probléma

show :: Show a => a -> String
read :: Read a => String -> a

Többértelműségi probléma: a show (read s) kifejezésben nem egyértelmű hogy a read és a show melyik osztály metódusa.

A többértelműségi probléma feloldása:

-- show (read s :: Int)

vagy

-- show (read s `asTypeOf` x)

ahol x helyén tetszőleges kifejezés lehet.


Az asTypeOf függvény definíciója:

Törvények

A törvények olyan szabályok amit egy példány definiálásakor a programozónak érdemes betartani, de a fordító nem tudja ezeket ellenőrizni.

Példa törvényekre: