OPERACJE WEJŚCIA I WYJŚCIA W HASKELL

Transkrypt

OPERACJE WEJŚCIA I WYJŚCIA W HASKELL
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
print
print
print
Prawda
3
"HaHa"
[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
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
Ć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
