Sveučilište u Zagrebu Fakultet elektrotehnike i računarstva PROGRAMIRANJE U HASKELLU Ak.god. 2011/12. LEKCIJA 12: ulazno/izlazne operacije 1 v1.0 (c) 2011 Jan Šnajder ============================================================================== > import Data.Char > import Data.List > import Control.Monad == IO-AKCIJE ================================================================= Sve što smo do sada programirali bio je ČISTO FUNKCIJSKI kod (engl. purely functional). Nismo imali popratnih efekata (engl. side effects): svaka funkcija vraćala je isti rezultat neovisno o tome kada i u kojem je kontekstu pozvana, i niti jedna funkcija nije mijenjala stanje sustava. Npr., kada je trebalo dodati element u listu, funkcija nije mijenjala izvornu listu, nego je vratila novu listu s nadodanim elementom. IO-operacije su drugačije jer moraju rezultirati popratnim efektom: funkcije za čitanje (npr. čitanje s tipkovnice) ne mogu dati uvijek istu povratnu vrijednost, a funkcije za pisanje (npr. ispisivanje na ekran) moraju promijeniti stanje sustava. Dakle, sada ćemo naučiti kako pisati "nečist" kod. U Haskellu se IO-operacije ostvaruju pomoću AKCIJA. Akcije su zapravo vrijednosti tipa: IO a gdje je 'a' tip povratne vrijednosti akcije. Npr. IO String IO Int IO (Int,String) IO () Posljednji tip je za akciju koja ne vraća ništa. Tip '()' nazivamo UNIT. To je zapravo prazna n-torka ("0-torka"). Prazna n-torka se isto piše kao '()'. Dakle, vrijednost '()' je tipa '()', odnosno '() :: ()'. 'IO' je ustvari tipski konstruktor vrste '* -> *. Možemo si ga predočiti kao posebnu "kutiju" u koju pohranjujemo akcije, koje nisu funkcijski čiste. U Haskellu veliku pažnju posvećujemo tome da razdvojimo funkcijski čist kod od nečistog. Zapravo, zahvaljujući tome što su sve akcije zapakirane u tip 'IO', to se razdvajanje u Haskellu događa prirodno. (Napomena: tip 'IO' je zapravo monada. Odnosno, preciznije rečeno: tip 'IO' je instanca razreda 'Monad'. Često se jednostavno kaže 'IO monada'. Zasada međutim nije bitno što to točno znači.) Evo napokon "Hello, world!" u Haskellu: > main = putStrLn "hello, world!" Ovo je akcija. Koji je njezin tip? Koji je tip funkcije 'putStrLn'? main :: IO () putStrLn :: String -> IO () Funkcija 'putStrLn' uzima string i vraća akciju (koja ispisuje string na ekran). Ovaj program može se prevesti (pomoću 'ghc --make') i pokrenuti. Svaki program koji ima funkciju 'main' može se prevesti. Svaki program može imati najviše jednu funkciju 'main'. Kada se program pokrene, poziva se funkcija 'main'. Funkcija 'main' može pozvati druge funkcije koje također izvode akciju. Npr. mogli smo pisati: main = pozdraviSvijet pozdraviSvijet = putStrLn "hello, world!" Jedna akcija rijetko je dovoljna. Ako želimo izvesti više akcija, trebamo ih povezati u jednu. To radimo pomoću posebnoj jezičnog konstrukta 'do': > main2 = do > putStrLn "hopa" > putStrLn "cupa" U do-bloku sve naredbe trebaju biti poravnate jedna ispod druge. Ova funkcija izvodi dvije akcije, ali one su povezane u jednu akciju, pa je njezin tip opet 'IO ()'. Unutar do-bloka, funkcije se izvode slijedno, odozgo prema dolje, kao u imperativnim jezicima. Drugi primjer: > main3 = do > putStrLn "Upiši svoj sretan broj" > number <- getLine > putStrLn $ "Tvoj sretan broj je " ++ number Kojeg je tipa 'getLine' ? Operator '<-' uzima zdesna akciju i u varijablu slijeva pohranjuje rezultat te akcije. Dakle, operator '<-' "otpakirava" povratnu vrijednost tipa 'a' iz akcije koja je tipa 'IO a'. To je jedini način kako se povratna vrijednost neke akcije može prebaciti u varijablu, i to je moguće jedino unutar funkcije koja je tipa 'IO a', odnosno jedino unutar "IO monade". Bi li sljedeće funkcioniralo? foo = "Tvoje ime je " ++ getLine Ili ovo: main4 = do putStrLn "Upiši svoj sretan broj" putStrLn $ "Tvoj sretan broj je " ++ getline Funkcija 'putStrLn' očekuje string. Isto tako, funkcija (++) očekuje stringove. No funkcija 'getLine' nije tipa 'String' nego 'IO String'. Da bismo došli do Stringa, prvo ga trebamo otpakirati pomoću operatora '<-'. Evo nešto što radi: > askNumber :: IO String > askNumber = do > putStrLn "Upiši svoj sretan broj" > getLine > main5 :: IO () > main5 = do > number <- askNumber > putStrLn $ "Tvoj sretan broj je " ++ number Povratna vrijednost akcija u do-bloku je uvijek ona koju vraća zadnja akcija u do-bloku. Zbog toga je povratna vrijednost funkcije (akcije) 'askNumber' string koji vraća funkcija (akcija) getLine. Svaka akcija rezultira rezultatom koji možemo, ali ne moramo, pohraniti u varijablu. Npr. > main6 :: IO () > main6 = do > x <- putStrLn "Upiši broj" > getLine > putStrLn "Hvala" Rezultat akcije 'putStrLn' je '()', pa to zapravo nema smisla pohranjivati. Rezultat akcije 'getLine' je String, ali ga ovdje nismo pohranili (to ima smisla ako npr. samo želite pričekati da korisnik upiše bilo što na tastaturu). == VJEŽBA 1 =================================================================== 1.1. - Napišite funkciju 'main' koja učitava dva stringa i ispisuje ih konkatenirane i u reverzu. 1.2. - Napišite funkciju 'threeNumbers' koja učitava tri broja i ispisuje njihov zbroj. - Pozovite ovu funkciju iz funkcije 'main', prevedite i izvršite program. == RETURN ===================================================================== Što ako želimo izmijeniti upisani string, prije nego što ga vratimo iz akcije? To možemo učiniti ovako: > askNumber2 :: IO String > askNumber2 = do > putStrLn "Upiši svoj sretan broj" > number <- getLine > return $ number ++ "0" Funkcija 'return :: a -> IO a' uzima vrijednost i pretvara je u rezultat akcije. 'return' se ne mora pojavljivati na kraju akcije. Može se pojavljivati bilo gdje. 'return' ne uzorkuje povratak iz funkcije, nego samo zapakirava vrijednost u akciju. Npr. > askNumber3 :: IO String > askNumber3 = do > putStrLn "Upiši svoj sretan broj" > number <- getLine > return $ number ++ "0" > getLine Ovo nema nekog smisla, ali ilustrira poantu. Povratna vrijednost do-bloka je povratna vrijednost posljednje akcije, neovisno o tome je li se negdje prije pojavio 'return'. (Zapravo, "return" je vrlo, vrlo loše odabrano ime za ovu funkciju. Bilo bi bolje da se zove "zapakiraj" ili slično.) Možemo naravno granati unutar akcije: > askNumber4 :: IO String > askNumber4 = do > putStrLn "Upiši svoj sretan broj" > number <- getLine > if number == "" then return "7" > else return number Bitno je da obje grane vraćaju akciju istog tipa, čime 'if-then-else' i sam postaje akcija. Budući daje if-then-else zadnja akcija u funkciji, taj tip mora biti 'IO String', jer je to povratni tip funkcije. Mogli smo i ovako: > askNumber5 :: IO String > askNumber5 = do > putStrLn "Upiši svoj sretan broj" > number <- getLine > return $ if number == "" then "7" else number A jesmo li mogli ovako: askNumber6 :: IO String askNumber6 = do putStrLn "Upiši svoj sretan broj" number <- getLine if number == "" then "7" else number Zašto ovo nije dobro? main7 :: IO () main7 = do putStrLn "Upiši svoj sretan broj" number <- getLine return number Naravno, možemo raditi i rekurziju: > askNumber7 :: IO String > askNumber7 = do > putStrLn "Upiši svoj sretan broj" > number <- getLine > if number == "" then askNumber7 else return number Ovo je OK jer su obje if-grane opet akcije. Što ako u jednoj od grana želimo izvesti više akcija? Moramo ih opet stopiti u jednu pomoću do-bloka: > askNumber8 :: IO String > askNumber8 = do > putStrLn "Upiši svoj sretan broj" > number <- getLine > if number == "" then do > putStr "Prazan upis! " > askNumber8 > else return number == VJEŽBA 2 ================================================================= 2.1. - Napišite funkciju 'threeStrings' koja učitava tri stringa i ispisuje ih na ekran konkatenirano u jedan string, a vraća njihovu ukupnu duljinu. treeStrings :: IO Int 2.2. - Napišite funkciju 'askNumber9' koja učitava broj i vraća 'Int'. Unos treba ponavljati sve dok korisnik ne upiše baš broj (znamenke). askNumber9 :: IO Int - Napišite funkciju 'main' u kojoj pozivate funkciju 'askNumber9' i na ekran ispisujete taj broj. - Prevedite i pokrenite pogram. 2.3. - Napišite funkciju 'askUser m p' koja vraća akciju koja ispisuje poruku 'm' na ekran, učitava unos s tipkovnice, ponavlja unos sve dok uneseni string ne zadovolji funkciju 'p' i zatim vraća učitani string. askUser :: String -> (String -> Bool) -> IO String - Poopćite funkciju na askUser' :: Read a => String -> (String -> Bool) -> IO a - Napišite funkciju 'main' u kojoj na ekranu ispisujete učitanu vrijednost - Prevedite i pokrenite program. 2.4. - Napišite funkciju koja s tastature učitava stringove sve dok korisnik ne upiše prazan string, a zatim vraća listu stringova. inputStrings :: IO [String] == WHERE & LET =============================================================== Unutar IO-akcija možemo koristiti naredbu 'let' za pridruživanje izraza: > askName1 :: IO String > askName1 = do > s1 <- getLine > s2 <- getLine > let forename = map toUpper s1 > lastname = map toUpper s2 > return $ s1 ++ " " ++ s2 Primijetite: 'let' koristimo da bismo varijabli pridružili vrijednost izraza. To je vrijednost dobivena čisto funkcijskim izračunom. Operator '<-' koristimo da bismo varijabli pridružili vrijednost koja je rezultat akcije. Dakle ovo je pogrešno: askName2 :: IO String askName2 = do s1 <- getLine s2 <- getLine forename <- map toUpper s1 lastname <- map toUpper s2 return $ s1 ++ " " ++ s2 I ovo je također pogrešno: askName3 :: IO String askName3 = do let s1 = getLine s2 = getLine forename = map toUpper s1 lastname = map toUpper s2 return $ s1 ++ " " ++ s2 Možemo također koristiti i naredbu 'where', ali izvan do-bloka: > askName4 :: IO String > askName4 = do > s1 <- getLine > s2 <- getLine > return $ upperCase s1 ++ " " ++ upperCase s2 > where upperCase = map toUpper == KORISNE IO-FUNKCIJE ======================================================= putStr :: String -> IO () putStrLn :: String -> IO () putChar :: Char -> IO () print :: Show a => a -> IO () Npr. 'putStr' pomoću 'putChr': > putStr1 :: String -> IO () > putStr1 [] = return () > putStr1 (x:xs) = do > putChar x > putStr1 xs Razlika između 'print' i 'putStr' je u tome što se 'print' može primijeniti na bilo koji tip koji je iz razreda Show. Zapravo, print je definiran kao 'putStrLn . show'. Posljedica toga je da ako pozovetemo 'print' nad stringom, dobit ćemo ga zajedno s navodnicima. print "Hello, world!" U modulu Control.Monad nalaze se funkcije koje su korisne za upravljanje tijekom akcija: when :: Monad m => Bool -> m () -> m () sequence :: Monad m => [m a] -> m [a] mapM :: Monad m => (a -> m b) -> [a] -> m [b] forever :: Monad m => m a -> m b Ovdje je 'm' neki tip koji je u razredu 'Monad'. Rekli smo da je 'IO' u razredu monad, pa dakle sve ove funkcije rade s tipom 'IO'. Funkcija 'when' izvodi zadanu akciju ako je uvjet ispunjen, inače izvodi 'return ()'. > main8 = do > input <- getLine > when (input == "") $ > putStrLn "Unesen je prazan niz" što je isto kao: > main9 = do > input <- getLine > if (input == "") then > putStrLn "Unesen je prazan niz" > else return () Funkcija 'sequence' uzima listu akcija i vraća akciju u kojoj se akcije iz liste izvode slijedno: > main10 = do > putStrLn "Upiši tri broja" > xs <- sequence [getLine,getLine,getLine] > putStrLn $ "Hvala. Upisao si " ++ unwords xs Može li ovako? main11 :: IO () main11 = do xs <- sequence [putStrLn "Upiši tri broja",getLine,getLine,getLine] putStrLn $ "Hvala. Upisao si " ++ unwords (tail xs) Funkcija 'sequence' je korisna za mapiranje IO-funkcije nad listom: > main12 = do > sequence $ map print [1..10] odnosno kraće (jer imamo samo jednu akciju): > main13 = sequence $ map print [1..10] Koji je tip gornje funkcije? Možemo i ovako: > main14 = do > sequence $ map print [1..10] > return () Je li ovo u redu?: > main15 = do > sequence $ map (putStrLn . show) [1..10] > return () Budući da je obrazac 'sequence $ map' dosta čest, za njega postoji funkcija 'mapM': > main16 = mapM print [1..10] Ako ne želimo zadržati rezultate akcija, koristimo funkciju 'mapM_': > main17 = mapM_ print [1..10] Razlika između ovih dviju funkcija vidi se u signaturi: mapM :: Monad m => (a -> m b) -> [a] -> m [b] mapM_ :: Monad m => (a -> m b) -> [a] -> m () Slična je funkcija 'forM'. Zapravo, 'forM' je ista kao 'mapM', samo što su parametri zamijenjeni tako da prvo ide lista, a onda akcija. To vrlo podsjeća na "foreach" petlju i imperativnim jezicima. > main18 = forM [1..10] print I ovdje postoji 'forM_' koja odbacuje izlazne vrijednosti: > main19 = forM_ [1..10] print Vrlo često se funkcija 'forM' koristi u kombinaciji s lambda-izrazom. Npr. > main20 = do > ys <- forM [1..10] $ \x -> do > putStrLn $ "Upiši " ++ show x ++ ". broj" > y <- getLine > return $ read y > putStrLn $ "Zbroj brojeva je " ++ show (sum ys) Funkcija koja zadani broj puta ponavlja zadanu akciju: replicateM :: Monad m => Int -> m a -> m [a] replicateM_ :: Monad m => Int -> m a -> m () Jedna zanimljiva funkcija je 'forever': > main21 = forever $ > putStrLn "Forever young" Koji je tip ove funkcije i zašto je takav? == VJEŽBA 3 ================================================================= 3.1. - Definirajte funkciju koja učitava neki broj, zatim učitava toliko stringova, a onda te stringove ispisuje u obrnutom poretku. 3.2. - Definirajte funkciju koja ispisuje sve Pitagorine trojke čije su sve stranice <= 100. Svaka trojka treba biti ispisana u zasebnome retku. 3.3. - Definirajte (rekurzivno) svoje funkcije 'mapM'' i 'mapM'_'.