Wprowadzenie Haskell jest językiem czysto funkcyjnym. W uproszczeniu oznacza to, że w przeciwieństwie do języków imperatywnych (w których podajemy komputerowi ciąg kroków do wykonania) definiujemy co, a nie w jaki sposób ma zostać wykonane.
W haskellu funkcje nie mogą zmieniać stanów (np. zmienić zawartości zmiennej)! Oznacza to, że funkcje w haskellu nie posiadają tzw. efektów ubocznych. Jedyną rzeczą, którą mogą wykonywać funkcje w haskellu jest zwrócenie pewnego wyniku opartego o parametry podane tejże funkcji. Dzięki temu za każdym razem kiedy wywołamy funkcję z ustalonymi parametrami otrzymamy taki sam wynik. Zatem w jaki sposób nasze programy mają komunikować się ze światem zewnętrznym?
Haskell radzi sobie z tym problemem w dosyć sprytny sposób. Otóż oddziela czyste części programu od tych, które ze względu na wspomniane wcześniej efekty uboczne nie są traktowane jako czyste (np. czytanie z klawiatury i pisanie na ekran). Pomysł na zrealizowanie operacji wejścia/wyjścia w haskellu jest następujący: zamiast mówić o wypisywaniu na ekran lub czytaniu z klawiatury, mówimy o wartościach specjalnego wejściowowyjściowego typu IO.
Czyste Zawsze zwraca ten sam wynik przy tych samych argumentach. Nigdy nie posiada efektów ubocznych. Nie zmienia stanów. Nieczyste Może zwracać różne wyniki przy tych samych argumentach. Może posiadać efekty uboczne. Może zmieniać globalny stan programu lub systemu.
Idea ta jest realizowana za pomocą monady wejścia-wyjścia. Praktycznie jest to rodzina typów IO, dla której określono 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 Na początek przygotujmy klasyczny program Witaj świecie. Piszemy plik helloworld.hs: main = putstrln "Hello, world!" Kompilujemy: ghc --make helloworld.hs Uruchamiamy: helloworld.exe
Prosty przykład Piszemy plik name.hs: main = do putstrln "Podaj imie:" imie <- getline putstrln ( Witaj " ++ imie ++ ".") Uruchamiamy poleceniem runhaskell name.hs
Zapis w konwencji do Zauważmy, że w poprzednim przykładzie użyto składni, która przypomina programowanie imperatywne. Użycie do pozwala niejako na sklejenie wielu kroków będących akcją wejścia/wyjścia w jedną operację wejścia/wyjścia. Utworzona w wyniku zastosowania do akcja posiada typ ostatniej z wykonanych w jej obrębie operacji. W związku z tym funkcja main posiada sygnaturę main :: IO <coś>, gdzie <coś> jest pewnym konkretnym typem.
Strzałka <- Część kodu imie <- getline czytamy następująco: Wykonaj akcję wejścia/wyjścia getline, a następnie zwiąż jej wartość wynikową z imie. getline posiada typ IO String zatem imie będzie miało typ String! Czy w takim razie poprawny jest kod imie = "Moje imie to: " ++ getline?
Strzałka <- jest w pewien sposób odpowiednikiem let dla przypisywania nazw do wyników akcji wejścia/wyjścia. import Data.Char main = do putstrln "Jak masz na imie?" imie <- getline putstrln "Jak masz na nazwisko?" nazwisko <- getline let duzeimie = map toupper imie duzenazwisko = map toupper nazwisko putstrln $ "Witaj " ++ duzeimie ++ " " ++ duzenazwisko ++ ", jak sie masz?"
Pozostałe funkcje putstr pobiera string jako parametr i zwraca akcję wejścia/wyjścia, która pisze do terminala (nie przechodzi do nowej linii). main = do putstr "Hej," putstr " jestem " putstrln "Hermenegilda!"
Pozostałe funkcje c.d. putchar pobiera znak jako parametr i zwraca akcję wejścia/wyjścia, która pisze ten znak do terminala. main = do putchar L putchar O putchar L
Pozostałe funkcje c.d. print najpierw wykonuje Show na argumencie po czym przekazuje wynik do putstrln i zwraca akcję wejścia/wyjścia, która pisze do terminala. main = do print Prawda print 3 print "HaHa" print [3,6,9]
Pozostałe funkcje c.d. getchar czyta znak ze standardowego wejścia. main = do 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. return rolę tego operatora objaśnimy w kontekście ciągów operacji wejścia-wyjścia zapisywanych z do. Załóżmy, że chcemy zdefiniować funkcję readln, która czyta i zwraca wiersz z klawiatury, czyli ciąg znaków zakończonych znakiem \n. Definicja mogłaby wyglądać tak: readln :: IO String readln = do c <- getchar if c == '\n' then return [] else do cs <- readln return (c:cs) Jak widać, return służy do zwrócenia wartości, natomiast strzałka <- pozwala związać wartość ze zmienną.
Funkcje ułatwiające życie 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 main = do c <- getchar when (c /= ) $ do putchar c main
Funkcje ułatwiające życie c.d. sequence pobiera listę akcji wejścia/wyjścia i zwraca te akcje wykonywane jedna po drugiej. main = do a <- getline b <- getline c <- getline print [a,b,c] Można zapisać np. jako main = do rs <- sequence [getline, getline, getline] print rs
Funkcje ułatwiające życie c.d. forever pobiera akcję wejścia/wyjścia i zwraca tę akcję powtarzając ją. import Control.Monad import Data.Char main = forever $ do putstr "Give me some input: " l <- getline putstrln $ map toupper l
Ćwiczenie 1 Napisać program grę, który prosi użytkownika o podanie liczby z zakresu 0-99 i podpowiada czy wprowadzona liczba jest większa, czy mniejsza od ustalonej liczby. Program kończy działanie w przypadku odgadnięcia liczby lub po 10 próbach.
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 kolejna funkcja o nazwie getcontens która czyta wszystkie znaki dopóki nie zostanie osiągnięty koniec pliku. import Data.Char main = do contents <- getcontents putstr (map toupper contents)
Należy zauważyć, że getcontens czyta ze standardowego wejścia (póki nie napotka EOF). A tutaj program, który wypisuje tylko linie krótsze niż 10 znaków: main = do contents <- getcontents putstr (shortlinesonly contents) shortlinesonly :: String -> String shortlinesonly input = let alllines = lines input shortlines = filter (\line -> length line < 10) alllines result = unlines shortlines in result
Uchwyty do plików Istnieją również funkcje odpowiadające dotychczas poznanym, lecz operujące na uchwytach do plików: hgetcontents,, hputstr, hputstrln, hgetchar, hgetline. Oraz funkcje openfile, readfile, writefile, appendfile, withfile.
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
Przykłady Przygotujmy plik gf.txt: Hey! Hey! You! You! I don t like your girlfriend! No way! No way! I think you need a new one!
Przykłady c.d. import System.IO main = do uchwyt <- openfile "gf.txt" ReadMode zawartosc <- hgetcontents uchwyt putstr zawartosc hclose uchwyt Zauważmy, że sami musimy zamknąć uchwyt do pliku!
Przykłady c.d. import System.IO main = do inh <- openfile gf.txt" ReadMode outh <- openfile biggf.txt" WriteMode inpstr <- hgetcontents inh hputstr outh (map toupper inpstr) hclose inh hclose outh
Możliwe wartości trybu wejścia/wyjścia Tryb IO Czyta nie Pisa nie Pozycja startowa w pliku ReadMode TAK NIE początek plik musi istnieć Uwagi 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
Ćwiczenie 2 Napisać program, który pobiera treść pliku tekstowego, tworzy lustrzane odbicie i zapisuje zaszyfrowaną wiadomość w innym pliku.
Ćwiczenie 3 Napisać program implementujący szyfr Cezara. Program otwiera plik, szyfruje jego treść i zapisuje ją w innym pliku.
Przekazywanie argumentów w linii poleceń Służą do tego funkcje: getargs (getargs :: IO [String]) oraz 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.
Przykład import System.Environment import Data.List main = do 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. Oto dwa przykłady: import System.Random import Control.Monad (replicatem) main = replicatem 10 (randomio :: IO Float) >>= print Zapewnienie losowości za każdym wywołaniem: import System.Random main = dog <- getstdgen print $ take 10 (randoms g :: [Double])
Bibliografia Miran Lipovaca Learn You a Haskell for Great Good!, Bryan O Sullivan, John Goerzen, Don Stewart Real World Haskell, http://en.wikibooks.org/wiki/haskell http://wazniak.mimuw.edu.pl/index.php?t itle=paradygmaty_programowania