Skip to content

Latest commit

 

History

History
280 lines (205 loc) · 6.88 KB

07io.md

File metadata and controls

280 lines (205 loc) · 6.88 KB

IO

Przejrzystość

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żne 2+2
  • let f x = x + x in f 2 jest równoważne 2+2
  • let x = g 2 in x + x jest równoważne g 2 + g 2 dla dowolnej funkcji g (odpowiedniego typu).

Podobnie jest (w pewnym zakresie) w innych językach (np. ML, Lisp).

Efekty uboczne

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?

Obliczenia

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

Łączenie obliczeń

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.

Lukier syntaktyczny - notacja do

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

Ważniejsze funkcje IO

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.

Dialogi

main = do
  putStrLn "Hej, co powiesz?"
  input <- getLine
  putStrLn $ "Powiedziałeś: " ++ input
  putStrLn "Do widzenia"

Pułapka - buforowanie

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

Dialog w pętli

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

Zadanie

ASCII/ANSI Sokoban: https://github.com/jnp3-haskell-2021/sokoban-5 , oddawanie przez https://classroom.github.com/a/NHQBhSUa Termin: 2022-12-07 18:00