WEJŚCIE/WYJŚCIE HASKELL ŁUKASZ PAWLAK DARIUSZ KRYSIAK
W Haskellu funkcje nie mogą zmieniać stanów (w tym np. zmieniać wartości zmiennej). Funkcja wywołana z pewnymi ustalonymi argumentami musi zwracać zawsze tą samą wartość niezależnie od tego ile razy i w jakim kontekście zostanie wywołana. Gdyby funkcje wejścia/wyjścia zwracały wartość np. odczytaną z klawiatury to zaprzeczały by idei Haskella, gdyż dla dwóch różnych wywołań z tymi samymi wartościami parametrów otrzymywalibyśmy różne wyniki. Więc w jaki sposób nasze programy mają komunikować się ze światem zewnętrznym? Haskell rodzi sobie z tym problemem definiując typ wejściowo - wyjściowy, którego wartości zwracają funkcję wejścia wyjścia np. funkcja getline odczytuje linię ze standardowego wejścia: ghci> :t getline getline :: IO String
Ze względu na fakt zmiany stanu jako efekt uboczny działania funkcji w Hasklellu funkcje dzielimy na czyste i nieczyste: Funkcje czyste: Zawsze zwracają ten sam wynik przy tych samych argumentach. Nigdy nie posiadają efektów ubocznych. Nie zmieniają stanów. Funkcje nieczyste: Mogą zwracać różne wyniki przy tych samych argumentach. Mogą posiadać efekty uboczne. Mogą zmieniać globalny stan programu lub systemu.
Rodzina typów IO, zawiera zestaw funkcji takich jak: putchar, putstr, putstrln, print, getchar, getline. Sygnatury: putchar :: Char -> IO () putstr :: String -> IO () putstrln :: String -> IO () print :: Show a => a -> IO () getchar :: IO Char getline :: IO String Zauważmy, że funkcje wyjściowe zwracają wynik typu IO (), gdzie () oznacza typ pusty, zaś funkcje wejściowe zwracają wynik typu IO a, gdzie a jest typem wczytywanej wartości.
Pierwszy program Hello world!!! Na początek przygotujmy klasyczny program Witaj świecie. W edytorze tekstowym piszemy: main = putstrln Witaj świecie!" Uruchamiamy naszą funkcję i wyświetla nam się nasz tekst.
Wydaje się ograniczające aby cały program był tylko jedną akcją I/O. Dlatego też możemy użyć składni do do sklejenia razem pewnej liczby akcji I/O w jedną: putstrln "Podaj imię:" imię <- getline putstrln ("Witaj " ++ imię ++ ".") Wartości zwracane przez funkcje nieczyste (w tym funkcje wejścia wyjścia odczytujemy przez wiązanie wartości: imię <- getline Należy przez to rozumieć: wykonaj akcję wejścia/wyjścia getline i zwiąż jej wynik z imię.
Przypisanie: imię = getline spowodowałoby nadanie funkcji getline nowej nazwy! Wiązanie jest odpowiednikiem let dla funkcji czystych. Taki kod przypomina nieco programowanie imperatywne. Użycie do pozwala na związanie wielu kroków wejścia wyjścia w jedną operację. Użycie do tworzy akcję wejścia wyjścia, której typ jest taki sam jak typ ostatniej operacji. W związku z tym funkcja main ma sygnaturę main :: IO <coś>, gdzie <coś> jest konkretnym typem.
Funkcje IO putstr pobiera string jako parametr i zwraca akcję wejścia/wyjścia, która pisze do terminala (nie przechodzi do nowej linii). putstrln przechodzi do nowej linii po przeczytaniu słowa. putstr "Witam," putstr " co " putstrln "słychać!"
Funkcje IO putchar pobiera znak jako parametr i zwraca akcję wejścia/wyjścia, która pisze ten znak do terminala. putchar 'W' putchar 'M' putchar 'i' putchar 'I'
Funkcje IO print bierze wartość która jest instancją show (co znaczy że wiemy jak ją reprezentować w formie tekstowej), wywołuje show. W zasadzie jest to putstrln. show. i Najpierw jest uruchomiony show z daną wartością, a później putstrln, co zwraca I/O akcje która wyświetli wartość. print True print 3 print xd" print [1,2,3]
Funkcje IO getchar czyta znak ze standardowego wejścia. c <- getchar if c /= ' ' then do putchar c main else return ()
Operatory klasy Monad Operator >>= służy do przekazywania wartości typu IO getchar >>= putchar Operator >>= przekazuje wynik pierwszej operacji jako argument dla drugiej. Jeśli wynik ten nie jest interesujący (np. jest pusty) używa się operatora >>. Można zatem powiedzieć, że operatory >>= i >> spełniają podobną rolę jak średnik w językach imperatywnych. Funkcję when można odnaleźć w Control.Monad. Jest ona interesująca z tego względu, że w bloku do wygląda jak wyrażenie sterujące przepływem. Przyjmuje ona wartość logiczną i w przypadku fałszu zwraca return () zaś dla prawdy akcję wejścia/wyjścia. import Control.Monad c <- getchar when (c /= ) $ do putchar c main
sequence pobiera listę akcji wejścia/wyjścia i zwraca te akcje wykonywane jedna po drugiej. a <- getline b <- getline c <- getline print [a, b, c] Można zapisać jako: rs <- sequence [getline, getline, getline] print rs Widać że sequence [getline, getline, getline] tworzy akcję I/O która wykona trzy razy getline. Jeśli powiążemy tę akcję z jakąś nazwą, wynikiem będzie lista wszystkich rezultatów, a w powyższym przykładzie lista trzech rzeczy które użytkownik wpisał z klawiatury.
forever pobiera akcję wejścia/wyjścia i zwraca tę akcję powtarzając ją. main = forever $ do putstr Wpisz coś: " l <- getline putstrln $ map toupper l
Pisanie z i do plików Do tej pory poznaliśmy funkcję getchar, która czytała pojedynczy znak, getline, która zaczytywała całą linię. Ale istnieje jeszcze funkcja o nazwie getcontens która czyta wszystkie znaki dopóki nie zostanie osiągnięty koniec pliku. contents <- getcontents putstr (map toupper contents) Uchwyty do plików: Istnieją również funkcje odpowiadające dotychczas poznanym, lecz operujące na uchwytach do plików: hgetcontents, działa jak getcontents, ale dla konkretnego pliku, istnieje również hgetline, hputstr, hputstrln, hgetchar, itp. Działają po prostu jak ich odpowiedniki bez h, tylko przyjmują uchwyt jako parametr i działają na tym konkretnym pliku zamiast działać na standardowym wejściu lub standardowym wyjściu. Przykład: putstrln to funkcja, która pobiera ciąg znaków i zwraca operacje I/O, która wydrukuje ten ciąg na terminal i znak nowej linii po tym. hputstrln bierze uchwyt i ciąg znaków i zwraca działanie I/O, które zapisze ten ciąg do pliku powiązanego z uchwytem, a następnie umieści znak nowej linii po tym. hgetline bierze uchwyt i zwraca I/O akcje odczytującą linię z pliku.
Funkcja readfile ma sygnaturę typu: readfile :: FilePath -> IO String. Funkcja readfile bierze ścieżkę dostępu razem z nazwą pliku jako pierwszy parametr i zwraca akcję I/O która wczyta zawartość pliku i powiąże zawartość do jakiegoś string-u. Użycie owej funkcji jest poręczniejsze niż używanie openfile, powiązywanie uchwytu pliku do nazwy, po czym wywoływanie hgetcontents. Przygotujmy plik zadanie.txt: Linia pierwsza Druga Trzecia Czwarta import System.IO uchwyt <- openfile zadanie.txt" ReadMode zawartosc <- hgetcontents uchwyt putstr zawartosc hclose uchwyt Zauważmy, że sami musimy zamknąć uchwyt do pliku. contents <- readfile "zadanie.txt" putstr contents
Funkcja writefile ma typ:writefile :: FilePath -> String -> IO (). Bierze ścieżkę dostępu wraz z nazwą pliku jako pierwszy parametr, String do zapisania do pliku jako drugi parametr i zwraca akcję I/O która wykona zapis do pliku. Jeśli plik już istnieje, zostanie skrócony do długości 0, przed zapisaniem danych. Oto jak zmienić plik zadanie.txt w wersje zapisaną dużymi literami i zapisanie go jako zadaniecaps.txt: import System.IO import Data.Char contents <- readfile "zadanie.txt" writefile "zadaniecaps.txt" (map toupper contents) Sygnatury: openfile :: FilePath -> IOMode -> IO Handle readfile :: FilePath -> IO String writefile :: FilePath -> String -> IO () appendfile :: FilePath -> String -> IO () withfile :: FilePath -> IOMode -> (Handle -> IO a) -> IO a
Funkcja appendfile ma sygnaturę typu taką samą jak writefile. appendfile nie skraca pliku do długości zero przed zapisaniem jeśli plik już istnieje, ale dodaje nowe rzeczy na końcu pliku. Zróbmy program który wczytuje linie ze standardowego wejścia i dodaje do pliku zadanie.txt. import System.IO handle <- getline appendfile "zadanie.txt" (handle ++ "\n") Sygnatury: openfile :: FilePath -> IOMode -> IO Handle readfile :: FilePath -> IO String writefile :: FilePath -> String -> IO () appendfile :: FilePath -> String -> IO () withfile :: FilePath -> IOMode -> (Handle -> IO a) -> IO a
Możliwe wartości trybu wejścia/wyjścia: Tryb IO Czytanie Pisanie Pozycja startowa w pliku Uwagi ReadMode TAK NIE początek Plik musi istnieć WriteMode NIE TAK początek Jeżeli plik istnieje jest czyszczony ReadWriteMode TAK TAK początek jeżeli plik nie istnieje jest tworzony; w przeciwnym wypadku dane pozostają nienaruszone AppendMode NIE TAK koniec jeżeli plik nie istnieje jest tworzony; w przeciwnym wypadku dane pozostają nienaruszone
Przekazywanie argumentów w linii poleceń Służą do tego funkcje: oraz getargs (getargs :: IO [String]) getprogname (getprogname :: IO String) Pierwsza z tych akcji wejścia/wyjścia pobiera argumenty, z którymi został uruchomiony program i przechowuje je w postaci listy zaś druga zwraca nazwę wykonywanego programu. import System.Environment import Data.List argumenty <- getargs nazwa <- getprogname putstrln "Podano argumenty:" mapm putstrln getargs putstrln "Nazwa programu to:" putstrln nazwa
Losowe dane W każdym języku programowania bywa przydatna możliwość generowania losowych danych. Taką opcję dostarcza nam również haskell dzięki pakietowi System.Random. random (generator) :: (Typ, StdGen) zwraca losową wartość typu Typ oraz nowy generator losowy. mkdstdgen (liczba) tworzy generator o podanej wartości seed/ziarna. randoms (generator) :: [Typ] zwraca nieskończoną listę typu Typ. randomr (min, max) (generator) jak random, tylko w przedziale min-max randomrs (min, max) (generator) :: [Typ] połączenie randoms i randomr Przykład: losuj = do gen <- newstdgen putstrln $ take 4 (randomrs ('1','6') gen)