A Haskell különválasztja a mellékhatásos és a mellékhatásmentes számításokat.
A mellékhatásmentes számításokat tiszta számításoknak nevezzük.
Az esetleg mellékhatásos számítások az akciók.
Az akciók típusa egy IO
előtaggal különbözik a tiszta számítások típusától. Például:
"ab" ++ "cd" :: String -- tiszta számítás
getLine :: IO String -- sor beolvasása
Az akciók paraméterezhetőek. Például egy adott
elérési útvonalról a readFile
akcióval tudunk beolvasni egy
állományt:
readFile :: FilePath -> IO String
A readFile
tiszta függvény, mivel a
kiértékelése közben nem történik mellékhatás. Mellékhatás a végrehajtás
közben történik.
Ha egy akció eredménye nem lényeges,
akkor ezt egy nullás (void
, unit
)
használatával kell jeleznünk. Például:
putChar :: Char -> IO () -- karakter kiírása
putStr :: String -> IO () -- szöveg kiírása
A do
kifejezés lehetővé teszi
akciók végrehajtását és komponálását (blokkok létrehozását), mivel
önmagukban nem végrehajthatóak.
Egy do
-blokkban
szerepelhet:
Mellékhatásos értékadás: v <- a
,
ahol v egy változó, a egy akció. Az így bevezett
változó csak innentől használható.
Mellékhatásmentes
értékadás: let v = e
, ahol v egy változó, e
egy tetszőleges kifejezés.
Akció végrehajtása. A blokk utolsó sora csak ilyen lehet.
A blokk típusa
mindig az utolsó sorának típusával egyezik meg. Ezt a return :: a
-> IO a
függvény segítségével irányíthatjuk. Ekkor egy tiszta
számításból hozunk létre akciót.
Írjunk egy unit típusú akciót, amely bekér egy sort és kétszer visszaírja!
Típus:
echoTwice :: IO ()
Segítség: getLine
, putStrLn
Megjegyzés: Próbáljuk ki az értelmezőben. Az értelmező az akciókat előbb kiértékeli, majd végre is hajtja.
Írjunk egy unit típusú akciót, amely bekér
egy sort, majd visszaírja a "You wrote"
szöveggel. Ha üres
sort kap, írja ki a "Blank line"
szöveget.
Típus:
echo :: IO ()
Segítség: Mintaillesztés a case
segítségével.
Fejlesszük tovább az előző feladatot úgy,
hogy egészen addig ismételje a sorok bekérését, amíg üres sort nem adunk
meg. Üres sor esetén írja ki, hogy "Good bye"
és fejezze
be a működést.
Típus:
echoUntilEmptyLine :: IO ()
Segítség: Alkalmazzunk rekurziót.
type Name = String
type Number = String
type Entry = (Name, Number)
type PhoneBook = [Entry]
emptyBook :: PhoneBook
emptyBook = []
findEntry :: PhoneBook -> Name -> Maybe Number
findEntry [] name = Nothing
findEntry ((e_name,e_number):es) name
| (e_name == name) = Just e_number
| otherwise = findEntry es name
addEntry :: PhoneBook -> Entry -> PhoneBook
addEntry book entry = entry : book
Írjunk egy olyan akciót, amely kiírja a következő üzenetet:
Commands:
? - search
+ - new entry
s - save phonebook
l - load phonebook
n - print names
q - quit
Típus:
printHelp :: IO ()
Írjunk egy akciót, amely kiír egy szöveget soremelés nélkül, majd bekér egy sort.
Típus:
prompt :: String -> IO String
Teszteset:
*Main> prompt ">>> "
>>> hello
"hello"
A korábbi gyakorlaton szereplő findEntry
segítségével írjunk egy olyan akciót, amely a prompt
függvényen keresztül bekér egy nevet és egy előre megadott
telefonkönyvben megkeresi a hozzátartozó számot.
Típus:
findAction'' :: PhoneBook -> IO ()
(folyt.)
Tesztesetek:
*Main> findAction'' [("Teszt","1")]
name> Teszt
number: "1"
*Main> findAction'' [("Teszt","1")]
name> X
The name is not in the book.
Módosítsuk úgy az előző megoldást, hogy ha üres nevet adunk meg, akkor ne kísérelje meg a keresést.
Típus:
findAction' :: PhoneBook -> IO ()
Segítség: case
, return
Módosítsuk úgy az előző megoldást, hogy folytassa a keresést egy új névvel egészen addig, amíg üres nevet nem adunk meg.
Típus:
findAction :: PhoneBook -> IO ()
Segítség: Alkalmazzunk rekurziót.
Készítsünk egy akciót (a prompt
felhasználásával), amely hozzáad egy bejegyzést egy meglevő
telefonkönyvhöz. Addig lehessen benne bejegyzéseket megadni, amíg nem
üres nevet adunk meg.
Típus:
addAction :: PhoneBook -> IO PhoneBook
(folyt.)
Teszteset:
*Main> addAction [("Teszt", "1")]
name> X
number> 2
name>
[("X","2"),("Teszt","1")]
Készítsünk egy olyan akciót, amely parancsokat vár és a következő műveleteket végzi el a paraméterként megadott telefonkönyvvel:
?
parancsra keres
benne (findAction
);+
parancsra
hozzáad egy bejegyzést (addAction
);q
parancsra befejezi a működést.Típus:
eventLoop' :: PhoneBook -> IO ()
(folyt.)
Teszteset:
*Main> eventLoop' emptyBook
command> +
name> X
number> 1
name>
command> ?
name> X
number: "1"
name> Y
The name is not in the book.
name>
command> q
*Main>
Egészítsük ki az előző feladatot úgy, hogy
az s
paranccsal el lehessen menteni egy állományba egy
adott telefonkönyvet.
Típus:
saveAction :: PhoneBook -> IO ()
Segítség: prompt
, writeFile
,
show
Megjegyzés: Az elmentett könyv
kiterjesztése .book
legyen!
Folytassuk az előző feladatot úgy, hogy az l
paranccsal be is lehessen tölteni a telefonkönyvet. A betöltés után
írjuk ki, hogy a betöltött telefonkönyvben mennyi elem található ("Loaded
N entries."
, ahol N a bejegyzések konkrét száma).
Típus:
loadAction :: IO PhoneBook
Segítség: prompt
, readFile
,
read
, putStrLn
, return
Megjegyzés:
A betöltendő könyvet mindig .book
kiterjesztéssel
keressük!
Egészítsük ki az előző
feladat megoldását úgy, hogy az n
paranccsal ki lehessen
íratni a telefonkönyv neveit.
Típus:
printNames :: PhoneBook -> IO ()
Folytassuk az evenLoop'
függvényhez tartozó megoldást úgy, hogy az ismeretlen parancsokra (a printHelp
függvény meghívásával) adjunk segítséget a felhasználónak.
Típus:
eventLoop :: PhoneBook -> IO ()
Az eddig megoldott feladatok
összefoglalásához készítsünk egy unit típusú main
akciót.
import System.IO
...
main :: IO ()
main = do
hSetBuffering stdout NoBuffering
putStrLn "== Phone Book Register =="
printHelp
eventLoop emptyBook
A main
akció megírás után a
program le is fordítható és önállóan futtatható:
ghc --make PhoneBook.hs