Haskell jest językiem czystym, w którym obowiązuje zasada przejrzystości:
- każde obliczenie wyrażenia daje ten sam wynik
- zastąpienie wyrażenia innym wyrażeniem o tej samej wartości daje równoważny program
Na przykład
let x = 2 in x+x
jest równoważne2+2
let f x = x + x in f 2
jest równoważne2+2
let x = g 2 in x + x
jest równoważneg 2 + g 2
dla dowolnej funkcji g (odpowiedniego typu).
Podobnie jest (w pewnym zakresie) w innych językach (np. ML, Lisp).
Sytuacja komplikuje się w obecności efektów ubocznych, np. I/O.
Powiedzmy, że mamy funkcję readInt :: Handle -> Int
wczytującą liczbę ze strumienia (np. stdin
). Czy
let x = readInt stdin in x+x
jest równoważne readInt stdin + readInt stdin
?
Efekty uboczne są w konflikcie z zasadą przejrzystości. Różne języki rozwiązują to na różne sposoby. W ML niektóre funkcje nie są przejrzyste. W C prawie żadne funkcje nie są przejrzyste.
📝 Rozważmy funkcję w C:
int f(int x){ return x + x }
Czy potrafisz podać przykład funkcji g()
takiej, że
f(g())
daje inny wynik niż g() + g()
?
A bez użycia I/O?
W Haskellu przejrzystość jest zasadą nadrzędną, dlatego nie może być funkcji takiej jak readInt :: Handle -> Int
.
Funkcja spełniająca podobną rolę będzie miała typ Handle -> IO Int
.
Różnica wydaje się kosmetyczna, ale jest w istocie fundamentalna: wyrażenie readInt stdin
nie daje teraz
wartości typu Int, ale obliczenie, którego wykonanie da wartość typu Int
(przepis na uzyskanie wartości typu Int
).
Dzięki temu zachowujemy przejrzystość - każde wywołanie da takie samo obliczenie (wczytaj liczbę z stdin).
Program w Haskellu generuje obliczenie - funkcja main
jest typu IO ()
. Wykonanie funkcji main
przez system wykonawczy realizuje to obliczenie.
Obliczenie to może być dowolnie skomplikowane, ale zaczniemy od bardzo prostego - skorzystamy z funkcji (bibliotecznej)
putStrLn :: String -> IO ()
main :: IO ()
main = putStrLn "Hello"
To jest kompletny program w Haskellu. Umieśćmy go w pliku, dajmy na to, hello.hs
i uruchommy go (na dwa sposoby):
$ runhaskell hello.hs
Hello!
$ ghc hello.hs
[1 of 1] Compiling Main ( hello.hs, hello.o )
Linking hello ...
$ ./hello
Hello!
Jesli mamy GHC zainstalowane przez stack, zamiast runhaskell
i ghc
możemy użyć stack exec runhaskell
i stack ghc
:
$ stack exec -- runhaskell --version
runghc 8.8.4
$ stack ghc -- --version
The Glorious Glasgow Haskell Compilation System, version 8.8.4
Użyteczną sztuczką jest też stack exec bash
Pisanie putStrLn
jest trochę niewygodne, możemy dodać definicje
write, writeln :: String -> IO ()
write = putStr
writeln = putStrLn
Proste sekwencjonowanie:
main = write "Hello," >> writeln "world!"
...nie wystarczy jeżeli chcemy użyć nie tylko efektu obliczenia, ale także jego wyniku, np. dla funkcji
getLine :: IO String
Dlatego właściwy operator sekwencjonowania to
(>>=) :: IO a -> (a -> IO b) -> IO b
Bierze on obliczenie o wyniku (typu) a
oraz funkcję która na podstawie a
tworzy obliczenie o wyniku b
i łaczy je w obliczenie o wyniku b
, np.
main = getLine >>= putStrLn
-- (IO String) (String -> IO ())
Czasami przydają się obliczenia, które dają wynik bez efektów ubocznych. Mozna je tworzyć przy użyciu funkcji
return :: a -> IO a
Tak naprawdę typy sekwencjonowania i return
są ogólniejsze - wrócimy jeszcze do tej kwestii.
Przy budowaniu obliczeń często pojawia się kod typu
obliczenie1 >>= (\x ->
obliczenie2 >>= \y ->
obliczenie3))
Dla usprawnienia zapisu oraz dla podkreślenia potencjalnej imperatywności możemy uzyć notacji do
:
do {
x <- obliczenie1;
y <- obliczenie2;
obliczenie3
}
a nawet
do
x <- obliczenie1
y <- obliczenie2
obliczenie3
Uwaga:
do
jest tylko równoważną notacją, nie powoduje wykonania efektu.
Konstrukcja
do { fragment1; let x=e; fragment2 }
jest równoważna
do { fragment1; let x = e in do { fragment2 }}
Przykład:
main = do
input <- getLine
let output = map toUpper input
putStrLn output
...oczywiście moglibyśmy to zapisać krócej:
main = getLine >>= putStrLn . map toUpper
print :: Show a => a -> IO ()
putStrLn :: String -> IO ()
putChar :: Char -> IO ()
putStr :: String -> IO ()
getChar :: IO Char
getLine :: IO String
getContents :: IO String
getArgs :: IO [String] -- import System.Environment
Funkcja getContents
daje całą zawartość wejścia jako leniwą listę (strumień).
📝 Jak bedzie działał program
main = getContents >>= putStr
Pomyśl, a potem wypróbuj. NB ghci
nie bardzo się do tego nadaje, lepiej użyć ghc
lub runghc
.
main = do
putStrLn "Hej, co powiesz?"
input <- getLine
putStrLn $ "Powiedziałeś: " ++ input
putStrLn "Do widzenia"
Jeśli w poprzednim przykładzie użyjemy putStr
zamiast putStrLn
,
przeważnie efekt będzie inny niż oczekiwany.
Nie ma to związku z Haskellem, a tylko ze standardowym buforowaniem terminala. Możemy ten problem rozwiązać np. tak:
import System.IO
promptLine :: String -> IO String
promptLine prompt = do
putStr prompt
hFlush stdout
getLine
main = do
input <- promptLine "Prompt>"
putStrLn $ "Powiedziałeś: " ++ input
Innym rozwiązaniem jest wyłączenie buforowania:
import System.IO
promptLine :: String -> IO String
promptLine prompt = do
putStr prompt
getLine
main = do
hSetBuffering stdout NoBuffering
input <- promptLine "Prompt> "
putStrLn $ "Powiedziałeś: " ++ input
doesQuit :: String -> Bool
doesQuit "q" = True
doesQuit "quit" = True
doesQuit _ = False
promptLine :: String -> IO String
promptLine prompt = do
putStr prompt
getLine
main = mainLoop
mainLoop :: IO()
mainLoop = do
input <- promptLine "> "
if doesQuit input
then return ()
else processInput input >> mainLoop
processInput :: String -> IO ()
processInput input =
putStrLn $ "Powiedziałeś: " ++ input
📝 Napisz program, który będzie reagował na klawisze WASD, wypisując odpowiadające im kierunki. A może warto wyłączyć buforowanie na stdin
?
📝 A teraz żeby reagował na kursory (strzałki) - patrz https://en.wikipedia.org/wiki/ANSI_escape_code#Terminal_input_sequences
ASCII/ANSI Sokoban: https://github.com/jnp3-haskell-2021/sokoban-5 , oddawanie przez Github Classroom Termin: 2023-12-01 18:00