Haskell-listák
6.2. Haskell-monádok
Az eddig megírt Haskell-függvényeket konstans bemenetekre hívtuk, de természetesen felmerül az igény, hogy az adatokat billentyűzetről vagy ál-lományból olvassuk be, és a képernyőre vagy egy állományba írjuk ki. A Haskell, hasonlóan más programozási nyelvekhez, biztosítja az adatbevitel-hez és az adatkiíráshoz szükséges eszközöket, azonban ezt korántsem volt olyan egyszerű megoldani.
Az olvasás/írás (I/O) műveletek mechanizmusa nem illik bele a funk-cionális paradigmába, ezért egy olyan eszközt, olyan struktúrát kellett ki-dolgozni, amely alkalmas azI/Oműveletek hatékony használatára, anélkül, hogy az megsértené a funkcionális gondolkodásmódot. A Haskell esetében ezt egy monád-nak nevezett struktúra bevezetésével oldották meg. A mo-nádok alkalmazásával az I/Oműveletek mellett a tömbök, a hibakezelések hatékony használatát is megvalósították, azaz minden olyan programozási művelet, amely mellékhatással jár, alkalmazható lett a Haskellben.
A monád fogalma a kategóriaelméletből származik. A kategóriaelmélet a legáltalánosabb matematikai struktúrák közötti kapcsolatokat írja le, az
6.2. Haskell-monádok 127 ezzel kapcsolatos értelmezések, fogalmak pontos leírását pedig megtaláljuk Awodey könyvében[1]. A monád egy számítási adattípust definiál, ami azt jelenti, hogy megadjuk, hogy egy adattípus értékein milyen számításokat végezhetünk, és ezek a számítások hogyan kombinálhatók.
Mint ahogyan eddig is megfigyelhettük, egy Haskell-függvény kiértéke-lése nem a programozó által definiált műveletek egymás után való végrehaj-tásából áll, azonban azI/Oműveletek elvégzése nem oldható meg másként.
Monádok segítségével megadhatjuk, hogy azI/Oműveleteket hogyan kom-bináljuk, azaz milyen sorrendben végezzük el őket, ilyenformán az imperatív nyelvekhez hasonló módon lehet megadni ezen műveletek kiértékelési sor-rendjét.
Ebben a fejezetben elsőször a Monad m típusosztályról lesz szó, majd az IO monádot mutatjuk be, amelynek segítségével a Haskellben olvasás, írás műveleteket végezhetünk. Későbbi fejezetekben bevezetésre fog kerülni még aMaybe monád.
Az IO monád és a Maybe monád is a Monad m típusosztálynak a példányai, ahol a Monad m típusosztály a standard Prelude-ben a követ-kezőképpen van definiálva:
class Monad m where
(>>=) :: m a -> (a -> m b) -> m b return :: a -> m a
A fenti típusosztályban két művelet, azaz két függvény/operátor típus-deklarációját láthatjuk.
A >>= függvény számítások (akciók) láncolását teszi lehetővé. Több-ször fogjuk használni a későbbiekben, csak egyszerűbben, egydo kifejezést (blokkot, jelölést) fogunk használni helyette, ahogyan azt korábban is már mutattuk. A >>= függvény például do jelölésre átírva a következőképpen néz ki:
m >>= f = do x <- m f x
Ez azt jelenti, hogy először végrehajtjuk azmszámítási sorozatot, amelynek eredménye az x-be kerül, majd meghívjuk az f függvényt az x bemene-ten, amelynek eredménye végül a do blokk eredménye lesz. A következő olvasIr_ függvény egy karakterláncot vár a billentyűzetről, amelyet rög-tön a beolvasás után ki is ír a képernyőre, ahol az általunk beolvasott értéket most is és a következőkben is mindig dőlt betűkkel fogjuk megjeleníteni. A
kiíratáshoz a putStrLnfüggvényt alkalmaztuk, amely hasonló a putStr -hez, csak használatakor a kiíratást automatikusan egynew line, azaz egy új sor karakter is követi.
olvasIr_ :: IO ()
olvasIr_ = getLine >>= putStrLn
> olvasIr_
Hello Haskell!
Hello Haskell!
A dojelölést használva a következőképpen módosul a függvény:
olvasIr :: IO () olvasIr = do
x <- getLine putStrLn x
A >> függvény két akció együttes elvégzését teszi lehetővé, azaz először az első, és utána a második akció kerül végrehajtásra, ahol az első akció által meghatározott érték nem lesz releváns. A következő függvény két karakter-lánc kiírását végzi:
ir_ :: IO ()
ir_ = putStr "Hello " >> putStrLn " Haskell!"
> ir_
Hello Haskell!
Ennek a függvénynek is megadjuk a dojelöléssel megírt változatát:
ir :: IO () ir = do
putStr "Hello "
putStrLn " Haskell!"
A következőkben az akciók megadását, illetve az egymás után való lán-colásukat, ahogy eddig is tettük, ezután is egydoblokkban fogjuk megadni.
Megjegyezzük még, hogy minden akciót külön sorba kell írni.
A return függvény segítségével elérjük, hogy a paramétereként meg-adott adatotbecsomagoljuk egyIOmonádba. A következő függvény egy ka-rakterláncot olvas be a billentyűzetről, majd a beolvasott értéket areturn alkalmazásával becsomagolja egy IO monádba, ezért a függvény kimeneté-nek típusa IO Stringlesz.
6.2. Haskell-monádok 129 olvasInt_ :: IO String
olvasInt_ = do temp <- getLine return temp
> olvasInt_
12
"12"
A következőkben azolvasInt_által meghatározott értéket egy<-művelet segítségével lekérjük, mondhatjuk azt is, hogy kicsomagoljuk, és a kapott értéket Inttípusú adattá alakítjuk:
> x <- olvasInt_
12
> read x :: Int 12
AzInttípusú adattá való átalakítás elvégezhető a függvénytörzsben is, aho-gyan ezt a következő olvasInt függvényben láthatjuk. A read függvény által meghatározott érték lekérésétlet ... =jelölés használatával fogjuk megoldani, mert így jelezzük a Haskellnek, hogy most egy tiszta függvény kiértékelése következik, nem egy akció végrehajtása. Figyeljük meg, hogy a let-et ebben az esetben nem lokális definíció megadására használjuk, aho-gyan azt korábban tettük. Areturnalkalmazásával azolvasIntfüggvény kimenetét most is becsomagoljuk egyIOmonádba, a különbség azonban az, hogy azolvasIntkimenete most IO Inttípusú érték lesz.
olvasInt :: IO Int olvasInt = do
temp <- getLine
let x = read temp :: Int return x
> olvasInt 12
12
Figyeljük meg, hogy azolvasIr_,olvasIr,ir_, illetveirfüggvények kimeneti értékének típusa IO (), ami azt jelenti, hogy semmit, azaz ()-t eredményeznek a függvényhívások. Haskellben a tiszta függvények mindig meghatároznak egy értéket. AzI/Oműveletek során azonban ez nem mindig lehetséges, ezért erre az esetre definiálták az IO () típust. Egy függvény kimeneti értékének típusa akkor IO (), ha az akciók sorát egy olyan I/O
függvény zárja, amely kimeneti értékének típusa szintén IO (), vagy az utolsó akcióreturn ().