IB015 Neimperativní programování Práce se vstupy a výstupy Jiří Barnat Libor Škarvada Oprávněná stížnost studentů Úryvek diskuse mezi studenty ... myslela jsem, že se budeme učit programovat ... ... zatím si jen hrajeme s ňákým blbým GHCI ... ... akorát obskurně zapisujeme funkce ... IB015 Neimperativní programování – 08 str. 2/39 Oprávněná stížnost studentů Úryvek diskuse mezi studenty ... myslela jsem, že se budeme učit programovat ... ... zatím si jen hrajeme s ňákým blbým GHCI ... ... akorát obskurně zapisujeme funkce ... WTF ??? IB015 Neimperativní programování – 08 str. 2/39 Oprávněná stížnost studentů Úryvek diskuse mezi studenty ... myslela jsem, že se budeme učit programovat ... ... zatím si jen hrajeme s ňákým blbým GHCI ... ... akorát obskurně zapisujeme funkce ... WTF ??? Náplň dnešní lekce Jak realizovat komunikace mezi programem a jeho okolím. Napíšeme, přeložíme a spustíme první program v Haskellu. IB015 Neimperativní programování – 08 str. 2/39 Co vlastně dělá takový program? Samostaně spustitelný program Posloupnost akcí, při kterých dochází k interakci programu s okolním světem. Interakce s okolím programu Interakce s uživatelem skrze terminál. Manipulace se soubory, čtení a zápis do souboru. Interakce skrze grafické rozhraní. Komunikace s operačním systémem. ... IB015 Neimperativní programování – 08 str. 3/39 Výpočty obalují vstup-výstupní akce Práce s interpretem Doposud naše programy v Haskellu žádné akce nepřipouštěly. Žádný pokyn k výpisu výsledku jsme do programu nevkládali. GHCI může za to, že po skončení výpočtu se vypíše výsledná hodnota na terminál. Samostatný program Explicitní pokyny ke komunikaci s uživatelem. Při akci čtení vstupu z terminálu, se výpočet zastaví a čeká, až uživatel vloží text. Dochází k prokládání výpočtu, tak jak jsme jej znali doposud, a realizace vstup-výstupních akcí. IB015 Neimperativní programování – 08 str. 4/39 Sekce Vstup/výstup IB015 Neimperativní programování – 08 str. 5/39 Typy vstup/výstupních akcí Typování vstup-výstupních akcí Unární typový konstruktor IO a . Typy vytvořené pomocí typového konstruktoru IO mají jednu jedinou hodnotu, a to vstup-výstupní akci. Tato hodnota nemá textovou reprezentaci (nelze ji vypsat). Výstupní akce Výstupní akce mají typ IO () . putStrLn "Ahoj!":: IO () Vstupní akce Vstupní akce mají typ IO a , kde typová proměnná a nabývá hodnoty (typu) podle typu objektu, který vstupuje. getLine :: IO String IB015 Neimperativní programování – 08 str. 6/39 Základní funkce pro výstup putChar :: Char -> IO () Zapíše znak na standardní výstup. putStr :: String -> IO () Zapíše řetězec na standardní výstup. putStrLn :: String -> IO () Zapíše řetězec na standardní výstup a přídá znak konec řádku. print :: Show a => a -> IO () Vypíše hodnotu jakéhokoliv tisknutelného typu na standardní výstup, a přidá znak konec řádku. Tisknutelné typy jsou instancí třídy Show . Uživatelem definované typy nutno označit přídomkem deriving Show . IB015 Neimperativní programování – 08 str. 7/39 Základní funkce pro vstup getChar :: IO Char Načte znak ze standardního vstupu. getLine :: IO String Načte řádek ze standardního vstupu. getContents :: IO String Čte veškerý obsah ze standardního vstupu jako jeden řetězec. Obsah je čten líně, tj. až když je potřeba. interact :: (String -> String) -> IO () Argumentem funkce interact je funkce, která zpracovává řetězec a vrací řetězec. Veškerý obsah ze standardního vstupu je předán této funkci a výsledek vytištěn na standardní výstup. IB015 Neimperativní programování – 08 str. 8/39 Vstup/výstup a referenční transparentnost Referenční transparentnost Daný výraz se vždy vyhodnotí na stejnou hodnotu, bez ohledu na okolí (kontext), ve kterém je použit. Programovací jazyk Haskell je referenčně transparentní. Dopady na vstup-výstupní chování Nelze napsat program, který by zpracoval vstup uživatele a vyhodnotil se podle zadaného vstupu na různé hodnoty. Lze napsat program, který zpracuje vstup a podle vstupu vypíše na výstup různé výsledky. Hodnoty předávané skrze vstup-výstupní akce nesouvisí s hodnotou výrazu, který tuto vstup-výstupní akci realizuje. IB015 Neimperativní programování – 08 str. 9/39 Vnitřní výsledek a vedlejší efekt Otázka Jestliže getLine načte řetězec ze vstupu a přitom má hodnotu vstup-výstupní akce, což je hodnota typu IO a , konkrétně zde IO String , tak kde je ten načtený řetězec? IB015 Neimperativní programování – 08 str. 10/39 Vnitřní výsledek a vedlejší efekt Otázka Jestliže getLine načte řetězec ze vstupu a přitom má hodnotu vstup-výstupní akce, což je hodnota typu IO a , konkrétně zde IO String , tak kde je ten načtený řetězec? Odpověď Načtený řetězec se uchová jako tzv. vnitřní výsledek provedení této vstupní akce. Skutečné načtení řetězce a zapamatování si vnitřního výsledku je realizováno jako vedlejší efekt vyhodnocení výrazu getLine . IB015 Neimperativní programování – 08 str. 10/39 Operátor >>= Přístup k hodnotě vnitřního výsledku Pomocí binárního operátoru >>= . Ve výrazu f >>= g funguje operátor >>= tak, že vezme vnitřní výsledek vstupní akce f a na tento aplikuje unární funkci g , jejímž výsledkem je ovšem vstup-výstupní akce. Výraz f >>= g tedy znamená, že: f :: IO a g :: a -> IO b f >>= g :: IO b Operátor »= (>>=) :: IO a -> ( a -> IO b ) -> IO b Následující zápis je ekvivalentní: getLine >>= putStr getLine >>= (\x -> putStr x) IB015 Neimperativní programování – 08 str. 11/39 Nesprávné použití operátoru >>= Otázka Operátor (>>=) nelze použít ke spojení vstup-výstupní akce a funkce, která není vstup-výstupní akce, proč? Příklad, takto nelze: getLine >>= length getLine >>= (\ x -> length x) IB015 Neimperativní programování – 08 str. 12/39 Nesprávné použití operátoru >>= Otázka Operátor (>>=) nelze použít ke spojení vstup-výstupní akce a funkce, která není vstup-výstupní akce, proč? Příklad, takto nelze: getLine >>= length getLine >>= (\ x -> length x) Odpověď Hodnota výrazu je závislá na zadaném vstupu! Porušuje referenční transparentnost. Typově nesprávně. „Jakmile jste v IO , nelze utéct“. Správné použití: getLine >>= print . length getLine >>= (\ x -> print (length x)) IB015 Neimperativní programování – 08 str. 12/39 Funkce return Funkce return jako vstupní akce Prázdná akce, jejíž provedení má za cíl pouze naplnit hodnotu vnitřního výsledku. return :: a -> IO a return [’A’,’h’,’o’,’j’] :: IO String Možné použití: return [’A’,’h’,’o’,’j’] >>= putStr Funkce return jako výstupní akce Funkce return , může sloužit i jako výstupní akce, která nemá žádný efekt. return :: a -> IO a return () :: IO () IB015 Neimperativní programování – 08 str. 13/39 Řazení vstup-výstupních akcí Řazení akcí, operátor >> Binární operátor, který řadí vstup-výstupní akce. Zapomíná/ničí hodnotu vnitřního výsledku. Výraz má hodnotu poslední (druhé) vstup-výstupní akce. (>>) :: IO a -> IO b -> IO b Jak se chovají následující programy? putStr "Jeje" >> putChar ’!’ getLine >> putStr "nic" putStrLn "Napiš mi něco pěkného." >> getLine >>= ( \x -> putStr "Napsal jsi:" >> putStrLn x ) IB015 Neimperativní programování – 08 str. 14/39 Funkce sequence Dávkové spuštění akcí Máme-li seznam vstup-výstupních akcí stejného typu, lze funkce z tohoto seznamu spustit a provést jako dávku. sequence :: [IO a] -> IO [a] sequence [] = return [] sequence (a:s) = do x<-a t <- sequence s return (x:t) Příklady použití V případě výstupních akcí je výsledkem vyhodnocení výrazu posloupnost výstupů, viz: sequence [ putStr "Ahoj", putStr " ", putStr "světe!"] V případě vstupních akcí, je výsledkem vyhodnocení výrazu seznam vstupů, který je uložený jako vnitřní výsledek vstup-výstupní akce, viz: sequence [ getLine, getLine, getLine ] >>= print IB015 Neimperativní programování – 08 str. 15/39 Funkce mapM Mapování IO akce na seznam argumentů Aplikuje unární vstup-výstupní akci na seznam hodnot a vzniklý seznam vstup-výstupních akcí provede. mapM :: (a -> IO b) -> [a] -> IO [b] mapM f = sequence . map f Příklady použití mapM putStr ["Den","Noc"] vypíše DenNoc mapM (\t -> putStr "Aa") [1,2,3,4,5] vypíše AaAaAaAaAa mapM (\x -> getLine) "aa»>= print po zadání dvou řádků s obsahem radek1 a radek2 vypíše ["radek1","radek2"] IB015 Neimperativní programování – 08 str. 16/39 Jednoduchá práce se soubory type FilePath = String Definuje typový alias. readFile :: FilePath -> IO String Načte obsah souboru jako řetězec. Soubor je čten líně. writeFile :: FilePath -> String -> IO () Zapíše řetězec do daného souboru (existující obsah smaže). Hodnoty jiného typu než String lze konvertovat funkcí show . appendFile :: FilePath -> String -> IO () Připíše řetězec do daného souboru. Hodnoty jiného typu než String lze konvertovat funkcí show . IB015 Neimperativní programování – 08 str. 17/39 Zápis pomocí do „do notace“ Pozorování Syntaktická konstrukce do slouží k alternativnímu zápisu výrazu s operátory >>= a >> . Následující zápis je ekvivalentní putStr "vstup?" >> getLine >>= \ x -> putStr "výstup?" >> getLine >>= \ y -> readFile x >>= \ z -> writeFile y (map toUpper z) do putStr "vstup?" x <- getLine putStr "výstup?" y <- getLine z <- readFile x writeFile y (map toUpper z) IB015 Neimperativní programování – 08 str. 18/39 Další IO funkce Další předdefinované funkce Existuje řada dalších funkcí, jejich definice však nejsou automaticky zavedeny při startu programu, nebo interpretu. Tyto funkce jsou definovány v tzv. modulech. Například System.IO System.Directory System.Time ... IB015 Neimperativní programování – 08 str. 19/39 Sekce Moduly IB015 Neimperativní programování – 08 str. 20/39 Moduly v Haskellu Modul v Haskellu Kolekce spolu souvisejících funkcí, typů a typových tříd. Program v Haskellu je tvořen hlavním modulem Main , který následně importuje a používá funkce z jiných modulů. Prelude je implicitně zavedený modul, který obsahuje definice funkcí, které jsme doposud běžně používali. Moduly umožňují strukturovat kód projektu, oddělit a případně později znovupoužívat ucelené části kódu. Příklady předdefinovaných modulů Modul pro práci se seznamy – Data.List Modul pro práci se znaky – Data.Char Modul pro práci s komplexními čísly – Data.Complex ... IB015 Neimperativní programování – 08 str. 21/39 Moduly a jejich použití 1/2 Použití modulu Před použitím funkcí a typů z modulu je třeba explicitně požádat o zavedení tohoto modulu, tzv. importovat modul. Klíčové slova import . Možnosti importu Importovat pouze vybrané, nebo skrýt některé funkce modulu. Vynutit při použití funkce povinnou identifikaci modulu, v podobě jméno_modulu.jméno_funkce (tzv. kvalifikace). Přejmenovat kvalifikaci. Příklady import Data.Char import qualified Data.Char import qualified Data.Char as C import qualified Data.Char as C (toUpper) import qualified Data.Char as C hiding (toUpper) IB015 Neimperativní programování – 08 str. 22/39 Moduly a jejich použití 2/2 Pozorování Kvalifikace funkcí z modulů zavede nový význam znaku tečka. Příklad import qualified Data.Char as C import System.Directory main = getDirectoryContents ".." »= print . map (\x -> (C.toUpper.head) x : tail x) IB015 Neimperativní programování – 08 str. 23/39 Definice Modulu v Haskellu Obecná definice module Jméno (export1, ..., exportn ) where    import M1 spec1 ... import Mm specm    globální_deklarace Automatické doplnění definice Main Není-li uvedena hlavička, doplní se module Main (main) where Nevyskytuje-li se mezi importovanými moduly M1,...Mm modul Prelude , doplní se import Prelude IB015 Neimperativní programování – 08 str. 24/39 Příklad modulu – Datový typ Fifo Datový typ Fifo Datový kontejner (struktura, která uchovává prvky) přistupovaný operacemi vlož prvek a vyber prvek. Prvky jsou z datové struktury odebírány v tom pořadí, ve kterém byly vkládány. First-In-First-Out = FIFO Operace by měly mít konstantní časovou složitost. Realizace v Haskellu Definice modulu Fifo Použití modulu: import Fifo IB015 Neimperativní programování – 08 str. 25/39 Příklad Modulu – Datový typ Fifo module Fifo (FifoTyp, emptyq, headq, enqueue, dequeue) where data FifoTyp a = Q [a] [a] emptyq :: FifoTyp a emptyq = Q [] [] enqueue :: a -> FifoTyp a -> FifoTyp a enqueue x (Q h t) = Q h (x:t) headq :: FifoTyp a -> a headq (Q (x:_) _) = x headq (Q [] []) = error "headq: prázdná fronta" headq (Q [] t) = headq (Q (reverse t) []) dequeue :: FifoTyp a -> FifoTyp a dequeue (Q (_:h) t) = Q h t dequeue (Q [] []) = error "dequeue: prázdná fronta" dequeue (Q [] t) = dequeue (Q (reverse t) []) IB015 Neimperativní programování – 08 str. 26/39 Sekce Samostatně stojící programy IB015 Neimperativní programování – 08 str. 27/39 Kompilace programu Připomenutí Interpretace vs. kompilace kódu. Dosud jsme využívali interpret ghci . Kód lze přeložit do samostatně spustitelného programu. Překladač ghc . Výhody přeloženého kódu Přeložený kód je rychlejší než interpretovaný. Na cílovou platformu není třeba instalovat interpret, ani vývojové prostředí. IB015 Neimperativní programování – 08 str. 28/39 Nazdar světe Hlavní funkce – main Spustitelný program musí obsahovat funkci main :: IO () . Funkce je automaticky spuštěna po zavedení programu do paměti. Nazdar světe main = putStrLn "Čaute lidi!" Překlad Kód umístěn v souboru hello.hs . Překlad pomocí ghc hello.hs -o hello Vytvořen spustitelný soubor hello . Vzniknou pomocné soubory hello.hi a hello.o , lze smazat. IB015 Neimperativní programování – 08 str. 29/39 Funkce show a read Změna typu Vstup/Výstup je obvykle realizován v objektech typu String . Převést objekty na řetězec a zpět je možné pomocí funkcí: show :: Show a => a -> String read :: Read a => String -> a Asymetrie použití Použití show a read není symetrické, při použití read je třeba uvést do jakého typu chceme řetězec transformovat. show 123 ∗ "123" read "123" ∗ ERROR (read "123")::Int ∗ 123 IB015 Neimperativní programování – 08 str. 30/39 Příklad s parsováním vstupu Parsování vstupu Převedou se pouze 100% správně strukturované řetězce. (read "[1,2,3]")::[Char] ∗ ERROR (read "[1,2,3]")::[Int] ∗ [1,2,3] (read "[1,2,3]")::[Float] ∗ [1.0,2.0,3.0] Program plus1 Tento program vyzve uživatele, aby zadal celé číslo, pak toto číslo přečte, převede na objekt typu Int , přičte jedničku a zvýšené číslo vytiskne. main = do putStrLn "Napis cele cislo:" x <- getLine print (1 + read x::Int) IB015 Neimperativní programování – 08 str. 31/39 Sekce Užitečné konstrukce Haskellu IB015 Neimperativní programování – 08 str. 32/39 Syntaktická konstrukce case Pozorování Víceřádkové definice funkcí realizují větvení kódu. Více řádkovou definici lze ekvivalentně přepsat s využitím klíčového slova case . Syntaktická konstrukce case case expression of pattern1 -> expression1 ... patternn -> expressionn Textové zarovnání vzorů je nutné. Všechny výrazy na pravých stranách musí být stejného typu. IB015 Neimperativní programování – 08 str. 33/39 Syntaktická konstrukce case – příklady Úkol 1 Definujte funkci take s využitím konstrukce case . Řešení: take m ys = case (m,ys) of (0,_) -> [] (_,[]) -> [] (n,x:xs) -> x : take (n-1) xs Úkol 2 Zapište pomocí case výraz if e1 then e2 else e3 . Řešení: case (e1) of True -> e2 False -> e3 IB015 Neimperativní programování – 08 str. 34/39 Strážené definice funkcí Stráž Stráž je výraz typu Bool přidružený k definičnímu přiřazení. Při výpočtu bude tato definice funkce realizována, pouze pokud bude asociovaný výraz vyhodnocen na True . function args | guard1 = expression1 ... | guardn = expressionn | otherwise = default expression Pozorování Konstrukce nerozšiřuje výrazové možnosti jazyka, ale je pohodlná (syntaktický cukr). IB015 Neimperativní programování – 08 str. 35/39 Strážené definice funkcí – příklady Příklad 1 f x | (x>3) = "vetsi nez 3" | (x>2) = "vetsi nez 2" | (x>1) = "vetsi nez 1" f 2 ∗ "vetsi nez 1" f 1 ∗ ERROR Příklad 2 g (a:x) | x==[] = "Almost empty." | x/=[] = "At least 2 members." | otherwise = "Unreachable code." g _ = "Nothingness." g [] ∗ "Nothingness." g "Ahoj" ∗ "At least 2 members." IB015 Neimperativní programování – 08 str. 36/39 Monáda a typ IO Pozorování Při vypisování typů programů pracující s IO můžete narazit na typovou třídu Monad , která je zobezněním typu IO . Například: (>>) :: Monad m => m a -> m b -> m b Monadické typy jsou v pokročilém Haskellu běžné, ale jsou za hranicí, která je stanovena pro tuto přednášku. Pro účely této přednášky je v pořádku otypovat pomocí IO : (>>) :: IO a -> IO b -> IO b Wanna more? IB016 Seminář z funkcionálního programování IB015 Neimperativní programování – 08 str. 37/39 Checkpoint Vstup/výstup Napište program, který vyzve uživatele, aby zadal 16 čísel oddělených mezerou, a poté tato čísla vypíše v matici velikosti 4x4. Napište program, který ve vhodně formátovaném výstupu (např. rozbalený souborový strom) vypíše obsah aktuálního adresáře včetně všech jeho podúrovní. Vstup-výstupní programy vytvářejte jako samostatně spustitelné programy. IB015 Neimperativní programování – 08 str. 38/39 USS Haskell IB015 Neimperativní programování – 08 str. 39/39