Haskell-listák
6.3. A standard bemenet és kimenet
A következőkben példákon keresztül mutatjuk be, hogy a Haskellben hogyan használjuk a standard bemenetet és kimenetet.
6.4. feladat Írjunk egy Haskell-függvényt, amely meghatározza a billentyű-zetről beolvasottn szám páros osztóinak listáját.
foParosO :: IO () foParosO = do
putStr "n = "
temp <- getLine
let n = read temp :: Int
let res = [i | i <- [2, 4 .. n], mod n i == 0]
print res
> foParosO n: 60
[2,4,6,10,12,20,30,60]
A foParosO függvénynek nincs bemeneti paramétere, kimeneti értékének típusa IO (). A getLinefüggvény, ahogy ezt már megfigyelhettük, a bil-lentyűzetről történő adatbevitelt teszi lehetővé, típusdeklarációjából jól lát-szik, hogy nem vár bemeneti értéket, kimenetének típusa pedigIO String, azaz egy IO monád lesz, ami egy olyan számítást definiál, ahol a végered-mény típusaString. Hasonlóan aputStrés aprintis egy-egyIOmonád, amelyek olyan számításokat definiálnak, ahol a végeredmény típusa (), a függvények kimenetének típusa pedig IO ().
getLine :: IO String putStr :: String -> IO () print :: Show a => a -> IO ()
AfoParosOfüggvény törzse egydoblokk keretén belül több, összesen öt akció-nak nevezett műveletet hajt végre, abban a sorrendben, ahogyan megadtuk. Szerepük a következő:
1. aputStrfüggvénnyel kiírunk a képernyőre,
6.3. A standard bemenet és kimenet 131 2. agetLinefüggvénnyel adatot olvasunk be billentyűzetről, amelyet
a <-használatával kicsomagolunk éstemp-pel jelölünk,
3. a billentyűzetről beolvasott String típusú értéket átalakítjuk Int típussá, amelyet a let ... =jelölést használvan-el jelölünk, 4. a kért lista előállításához halmazműveletet használunk, az eredményt
a res-sel jelöljük, megint alet ... =jelölést használjuk, 5. aprintfüggvénnyel kiírunk a képernyőre.
A fenti akciók közül a harmadik és negyedik akció olyan függvények kiér-tékelését valósítják meg, amelyeknek nincs mellékhatásuk, ezért használjuk a let ... =jelölést, azaz jelezzük, hogy tiszta funkcionális programozási stílusban megírt függvényeket kell kiértékelni. A Haskell tehát külön jelölési rendszert dolgozott ki arra, amikor egy tiszta függvényt akar kiértékelni, és mást használ, amikor egy mellékhatással járó függvényt hajt végre. Élesen elkülöníti a tisztán funkcionális paradigmában írt függvényeket a mellékha-tással járó függvényektől.
A következő függvény egy karakterláncot olvas be a billentyűzetről, majd meghatározza azt a két karakterláncot, amelyben csak a kis- és nagy-betűk, illetve azt, amelyben csak a számjegyek szerepelnek. Figyeljük meg, hogy a return használatakor nem következik be az olvasStr_-ből való kilépés.
import Data.Char (isAlpha, isNumber) olvasStr_ :: IO ()
olvasStr_ = do
putStr "kerek egy karakterlancot: "
str <- getLine
x <- return (filter isAlpha str) y <- return (filter isNumber str)
putStrLn $ "Az alfanumerikus karakterek: " ++ x putStrLn $ "A szamok: " ++ y
A következő függvény ugyanazt végzi, mint az előző, csak az implemen-táció során a filter által meghatározott értékeket másképpen kérdezzük le. A jegyzet további részében ezen utóbbi módszer szerint fogunk eljárni.
olvasStr :: IO () olvasStr = do
putStr "kerek egy karakterlancot: "
str <- getLine
let x = filter isAlpha str let y = filter isNumber str
putStrLn $ "Az alfanumerikus karakterek: " ++ x putStrLn $ "A szamok: " ++ y
> olvasStr
kerek egy karakterlancot: Marosvasarhely 2001 oktober 1 Az alfanumerikus karakterek: Marosvasarhelyoktober A szamok: 20011
6.5. feladat Írjunk egy Haskell-függvényt, amely meghatározza a billentyű-zetről beolvasott számok rendezett sorrendjét. Használjuk asort könyvtár-függvényt.
import Data.List (sort) foRendez :: IO ()
foRendez = do
putStr "szamokat kerek: "
ls <- olvasSzamok1 let sortLs = sort ls putStr "rendezve: "
print sortLs
olvasSzamok1 :: IO [Int]
olvasSzamok1 = do temp <- getLine
let ls = read temp :: [Int]
return ls
> foRendez
szamokat kerek: [12, 4, 67, 8, 9]
rendezve: [4,8,9,12,67]
Hasonlóan az előző példához, egydoblokk keretén belül afoRendez függ-vényben több akció kerül végrehajtásra.
Az olvasSzamok1 függvény számok beolvasását biztosítja, amelyben a returnsegítségével azlslistát becsomagoljuk, azaz létrehozunk egyIO monádot, így azolvasSzamok1kimenetének típusaIO [Int]lesz. Itt vi-gyáznunk kell arra, hogy helyes formátumban olvassuk be a bemenetet, azaz ha nem megfelelő helyen használunk szögletes zárójeleket, illetve vesszőket, akkor futási hibát kapunk, ahogy a következő lekérdezésből ez kitűnik:
6.3. A standard bemenet és kimenet 133
> foRendez
szamokat kerek: 12, 4, 7, 8
rendezve: *** Exception: Prelude.read: no parse...
6.6. feladat Írjunk egy Haskell-függvényt, amely valós számokat olvas be a billentyűzetről, majd a számokat az egészrészük alapján különböző csopor-tokba, azaz listákba teszi.
import Data.List (sort, groupBy) foCsoportosit :: IO ()
foCsoportosit = do
putStr "szamokat kerek: "
ls <- olvasSzamok2
let gLs = groupBy (\x y -> truncate x == truncate y)
$ sort ls putStr "csoportositva: "
print gLs
olvasSzamok2 :: IO [Double]
olvasSzamok2 = do temp <- getLine
let ls = map (read :: String -> Double) $ words temp return ls
> foCsoportosit
szamokat kerek:1.77 5.6 1.4 5.34 2.7 2.9 1.9 2.4 1.33 csoportositva:
[[1.33,1.4,1.77,1.9],[2.4,2.7,2.9],[5.34,5.6]]
Ahogy a korábbi példáknál láttuk, afoCsoportositfüggvény itt is akciók egymásutánjából áll, amelyek adatbevitelt, illetve adatfeldolgozást tesznek lehetővé. Az előző példához képest a words és map függvények használa-ta miatt a számok bevitelét azonban más formában kell megadni. Most a számokat egy sorban, szóközöket téve közéjük kell beírni, mert ellenkező esetben futási hibát kapunk. Az olvasSzamok2 függvényben a temp-be beolvasott karakterláncot a words függvénnyel előbb szavakra bontjuk, azaz létrehozunk egy String elemekből álló listát, majd a map segítsé-gével egyenként átalakítjuk őketDoubletípusúvá, így a függvény kimenete IO [Double]lesz.
Az egészrész alapján történő csoportosításhoz először a sort függ-vénnyel rendezzük azlslistát, majd agroupByfüggvénnyel csoportosítjuk
az egymás után következő, azonos egész résszel rendelkező elemeket. Az egész részt a truncatekönyvtárfüggvénnyel határozzuk meg.
A csoportok kiíratását elegánsabb formában is megoldhatjuk, ha min-den sorba csak az azonos csoportbeli elemeket írjuk ki, nem mint listákat, hanem egyenként. Ennek megfelelően módosítjuk afoCsoportositutolsó sorát, és egy saját kiíró függvényt adunk meg, a myPrintList-et:
foCsoportosit_ :: IO () foCsoportosit_ = do
putStr "szamokat kerek: "
ls <- olvasSzamok2
let gLs = groupBy (\x y -> truncate x == truncate y)
$ sort ls putStr "csoportositva: \n"
myPrintList gLs
myPrintList :: [[Double]] -> IO () myPrintList = mapM_ auxF1
where
auxF1 :: [Double] -> IO () auxF1 kLs = do
mapM_ auxF2 kLs putStrLn ""
auxF2 :: Double -> IO ()
auxF2 k = putStr $ show k ++ " "
> foCsoportosit_
szamokat kerek: 1.77 5.6 1.4 5.34 2.7 2.9 1.9 2.4 1.33 csoportositva:
1.33 1.4 1.77 1.9 2.4 2.7 2.9
5.34 5.6
AmyPrintListfüggvényben kétszer is használatra került a már korábban is alkalmazottmapM_beépített függvény. Az elsőmapM_a sorok kiíratásáért felelős, a második a sorokon belüli értékeket írja ki, szóközt téve az elemek közé.
Haskellben nincs egész, illetve valós számokat beolvasó könyvtárfügg-vény, de ez könnyen megoldható. Korábban megadtuk az olvasInt függ-vényt, amelynek visszatérési értéke Int volt, a következő olvasDouble pedigDouble típusú érték beolvasását teszi lehetővé.
6.3. A standard bemenet és kimenet 135 olvasDouble :: IO Double
olvasDouble = do str <- getLine
return (read str :: Double)
A következő függvény több valós számot olvas be a billentyűzetről, ahol minden szám beolvasása után enter-t kell nyomni. Paraméterként meg kell adni, hogy hány számot szeretnénk beolvasni, a függvény pedig egy IO [Double] típusú értéket határoz meg. A kimeneti listában a számok sorrendje megegyezik a beolvasási sorrenddel.
olvasNszam :: Int -> IO [Double]
olvasNszam n = do k <- olvasDouble
if n == 1 then return [k]
else do
ve <- olvasNszam (n-1) return (k : ve)
> olvasNszam 3 12.6
34.3 5.7
[12.6,34.3,5.7]
A következő olvasSzam függvény egész számok beolvasását végzi, és addig kéri a számokat, amíg üres sort nem adunk. A függvény által megha-tározott kimenet típusaIO [Int]lesz, és megadja a beolvasott számokat, fordított sorrendben, mint ahogy beolvastuk őket a billentyűzetről.
olvasSzam :: IO [Int]
olvasSzam = auxOlvas []
where
auxOlvas :: [Int] -> IO [Int]
auxOlvas res = do temp <- getLine
if null temp then return res else do
let k = read temp :: Int auxOlvas (k : res)
> olvasSzam 3
6
7 [7,6,3]
A következő példák számrendszerek közötti átalakításokat fognak be-mutatni, amelyeket további I/O műveleteket végző példáknál fogunk fel-használni.
6.7. feladat Írjunk egy Haskell-függvényt, amely meghatározza egy szám tetszőleges számrendszerbeli alakja alapján a szám 10-es számrendszerbeli alakját.
convToNr :: (Integral a) => [a] -> a -> a convToNr ls b = auxConv ls b 0
where
auxConv :: (Num a) => [a] -> a -> a -> a auxConv [] b res = res
auxConv (k : ve) b res = auxConv ve b (k + b * res)
> convToNr [1,1,1,0,1] 2 29
A convtToNr függvény első paramétere egy lista, amely a b számrend-szerbeli számjegyeket fogja tartalmazni. Kimenete a10-es számrendszerbeli szám. A függvény egy auxConvsegédfüggvényt használ, ami lehetővé teszi a kezdeti értékadást, illetve hogy az eredményt a harmadik paraméteré-ben akkor számoljuk, amikor megyünk be a rekurzióba, ahol az eredmény meghatározása azt jelenti, hogy a megadott számjegyekből kiszámítjuk a megfelelő hatványok összegét. Első ránézésre lehet, hogy nem egyértelmű a hatványösszeg meghatározása, ezért a fenti bemenetre bemutatjuk a lépé-senkénti műveleteket:
k : ve res --> k + 2 * res
[1,1,1,0,1] 0
[1,1,0,1] -> 1 + 2·0
[1,0,1] -> 1 + 2·(1 + 2·0)
[0,1] -> 1 + 2·(1 + 2·(1 + 2·0))
[1] -> 0 + 2·(1 + 2·(1 + 2·(1 + 2·0)))
[] -> 1 + 2·(0 + 2·(1 + 2·(1 + 2·(1 + 2·0)))) -> 1 + 21·0 + 22·1 + 23· 1 + 24·1 + 25·0 -> 29
A fordított műveletet a convFromNr függvény végzi, amelynek beme-neti paraméterként meg kell adni egy tízes számrendszerbeli számot és egy
6.3. A standard bemenet és kimenet 137 bszámrendszert, hogy eredményként meghatározza egyIntelemtípusú lis-tába a bszámrendszerbeli számjegyeket.
6.8. feladat Írjunk egy Haskell-függvényt, amely meghatározza egy 10-es számrendszerbeli szám tetszőleges bszámrendszerbeli alakját.
convFromNr :: Integral a => a -> a -> [a]
convFromNr nr b = auxConv nr b []
where
auxConv :: Integral a => a -> a -> [a] -> [a]
auxConv nr b res
| nr < b = nr : res
| otherwise = auxConv d b (r : res) where
r = rem nr b d = div nr b
> convFromNr 256 2 [1,0,0,0,0,0,0,0,0]
6.9. feladat Olvassuk be a billentyűzetről egy szám számjegyeit és a meg-felelő számrendszer értékét. Alakítsuk át a számot 10-es számrendszerbe, majd ellenőrizzük az eredményt, használjuk a korábban megírt convToNr és convFromNrfüggvényeket.
foAlakit :: IO () foAlakit = do
putStr "szamjegyek: "
temp <- getLine
let ls = map (read :: String -> Integer) $ words temp putStr "szr alap: "
str <- getLine
let b = read str :: Integer let nr = convToNr ls b putStr "eredmeny: "
print nr
let ls = convFromNr nr b putStr "ellenorzes: "
print ls
> foAlakit
szamjegyek: 11 6 14 14 1 szr alap: 16
eredmeny: 749281
ellenorzes: [11,6,14,14,1]
Megjegyzés: az eredmény helyességét a következő összefüggés alapján tudjuk leellenőrizni: 749281 = 11·164 + 6·163 + 14·162 + 14·16 + 1.
Összegzésképpen elmondhatjuk, hogy a Haskell két szempont szerint figyeli a programkódot: mikor kell kifejezést, azaz tiszta függvényt kiérté-kelni, illetve mikor kell egy adott akciót, azaz mellékhatással járó műveletet végrehajtani. A kifejezések kiértékelésekor nem parancsvégrehajtás törté-nik, ezért ezekben a függvényekben nem léphetnek fel mellékhatások, ez a tulajdonképpeni tiszta funkcionális stílusban írt programrészek megadását jelenti. AzI/Oműveletek, azaz akciók végrehajtásakor előállhatnak mellék-hatások, ez az imperatív stílusú programrészek megadását jelenti.