Haskell Input and Output Źródło:

Podobne dokumenty
WEJŚCIE/WYJŚCIE HASKELL ŁUKASZ PAWLAK DARIUSZ KRYSIAK

Zatem w jaki sposób nasze programy mają komunikować się ze światem zewnętrznym?

Wskaźniki a tablice Wskaźniki i tablice są ze sobą w języku C++ ściśle związane. Aby się o tym przekonać wykonajmy cwiczenie.

Nazwa implementacji: Nauka języka Python wyrażenia warunkowe. Autor: Piotr Fiorek. Opis implementacji: Poznanie wyrażeń warunkowych if elif - else.

znajdowały się różne instrukcje) to tak naprawdę definicja funkcji main.

Wskaźnik może wskazywać na jakąś zmienną, strukturę, tablicę a nawet funkcję. Oto podstawowe operatory niezbędne do operowania wskaźnikami:

Programowanie w języku Python. Grażyna Koba

Informatyka I: Instrukcja 4.2

1 Podstawy c++ w pigułce.

TWORZENIE SWOICH TYPÓW I TYPÓW KLAS HASKELL (RODZIAŁ 8) ZAJĘCIA 4

lekcja 8a Gry komputerowe MasterMind

Systemy operacyjne. Laboratorium 9. Perl wyrażenia regularne. Jarosław Rudy Politechnika Wrocławska 28 lutego 2017

Wstęp do programowania INP001213Wcl rok akademicki 2017/18 semestr zimowy. Wykład 12. Karol Tarnowski A-1 p.

Ćwiczenie 4. Obsługa plików. Laboratorium Podstaw Informatyki. Kierunek Elektrotechnika. Laboratorium Podstaw Informatyki Strona 1.

Obsługa plików. Laboratorium Podstaw Informatyki. Kierunek Elektrotechnika. Laboratorium Podstaw Informatyki Strona 1. Kraków 2013

Podstawy programowania skrót z wykładów:

1 Podstawy c++ w pigułce.

Python jest interpreterem poleceń. Mamy dwie możliwości wydawania owych poleceń:

Bash - wprowadzenie. Bash - wprowadzenie 1/39

Widoczność zmiennych Czy wartości każdej zmiennej można zmieniać w dowolnym miejscu kodu? Czy można zadeklarować dwie zmienne o takich samych nazwach?

Laboratorium 3: Tablice, tablice znaków i funkcje operujące na ciągach znaków. dr inż. Arkadiusz Chrobot dr inż. Grzegorz Łukawski

Zasady programowania Dokumentacja

1 Powtórzenie wiadomości

Po uruchomieniu programu nasza litera zostanie wyświetlona na ekranie

Lab 9 Podstawy Programowania

1. Wypisywanie danych

Języki i metodyka programowania. Typy, operatory, wyrażenia. Wejście i wyjście.

Programowanie proceduralne INP001210WL rok akademicki 2018/19 semestr letni. Wykład 6. Karol Tarnowski A-1 p.

Skrypty powłoki Skrypty Najcz ciej u ywane polecenia w skryptach:

Narzędzia informatyczne w językoznawstwie

Podstawy programowania. Wykład Funkcje. Krzysztof Banaś Podstawy programowania 1

Strumienie, pliki. Sortowanie. Wyjątki.

Uwagi dotyczące notacji kodu! Moduły. Struktura modułu. Procedury. Opcje modułu (niektóre)

Informatyka I. Typy danych. Operacje arytmetyczne. Konwersje typów. Zmienne. Wczytywanie danych z klawiatury. dr hab. inż. Andrzej Czerepicki

Być może jesteś doświadczonym programistą, biegle programujesz w Javie,

Informatyka II. Laboratorium Aplikacja okienkowa

Warunek wielokrotnego wyboru switch... case

Niezwykłe tablice Poznane typy danych pozwalają przechowywać pojedyncze liczby. Dzięki tablicom zgromadzimy wiele wartości w jednym miejscu.

Technologie Informacyjne - Linux 2

TECHNOLOGIE INTERNETOWE WYKŁAD 6. JavaScript Funkcje i obiekty

Stałe, znaki, łańcuchy znaków, wejście i wyjście sformatowane

Jak napisać program obliczający pola powierzchni różnych figur płaskich?

1 Wskaźniki i zmienne dynamiczne, instrukcja przed zajęciami

4. Funkcje. Przykłady

Strumienie, pliki. Sortowanie. Wyjątki.

Wstęp do Informatyki i Programowania Laboratorium: Lista 0 Środowisko programowania

Zastanawiałeś się może, dlaczego Twój współpracownik,

Jak zawsze wyjdziemy od terminologii. While oznacza dopóki, podczas gdy. Pętla while jest

PROE wykład 2 operacje na wskaźnikach. dr inż. Jacek Naruniec

Argumenty wywołania programu, operacje na plikach

Tworzenie własnych typów. danych oraz klas typów

Programowanie strukturalne i obiektowe. Funkcje

Struktury, unie, formatowanie, wskaźniki

Programowanie obiektowe

Typy danych, cd. Łańcuchy znaków

Algorytm. a programowanie -

Pliki. Operacje na plikach w Pascalu

Systemy operacyjne. System operacyjny Linux - wstęp. Anna Wojak

Powłoka I. Popularne implementacje. W stylu sh (powłoki zdefiniowanej w POSIX) W stylu csh. bash (najpopularniejsza) zsh ksh mksh.

Przedrostkowa i przyrostkowa inkrementacja i dekrementacja

Ćwiczenie: JavaScript Cookies (3x45 minut)

C++ Przeładowanie operatorów i wzorce w klasach

Podstawy i języki programowania

Wstęp do Programowania, laboratorium 02

Zakład Systemów Rozproszonych

Rozdział 4 KLASY, OBIEKTY, METODY

Warsztaty dla nauczycieli

Podstawy programowania, Poniedziałek , 8-10 Projekt, część 1

1 Przygotował: mgr inż. Maciej Lasota

Ok. Rozbijmy to na czynniki pierwsze, pomijając fragmenty, które już znamy:

Podstawy programowania. Wykład: 9. Łańcuchy znaków. dr Artur Bartoszewski -Podstawy programowania, sem 1 - WYKŁAD

3. Instrukcje warunkowe

Rekurencja (rekursja)

Część XVII C++ Funkcje. Funkcja bezargumentowa Najprostszym przypadkiem funkcji jest jej wersja bezargumentowa. Spójrzmy na przykład.

Uniwersytet Zielonogórski Instytut Sterowania i Systemów Informatycznych. Ćwiczenie 3 stos Laboratorium Metod i Języków Programowania

PROE wykład 3 klasa string, przeciążanie funkcji, operatory. dr inż. Jacek Naruniec

Python wprowadzenie. Warszawa, 24 marca PROGRAMOWANIE I SZKOLENIA

Informatyka I. Klasy i obiekty. Podstawy programowania obiektowego. dr inż. Andrzej Czerepicki. Politechnika Warszawska Wydział Transportu 2018

Blockly Kodowanie pomoc.

Wprowadzenie do programowania w języku Visual Basic. Podstawowe instrukcje języka

Warto też w tym miejscu powiedzieć, że w C zero jest rozpoznawane jako fałsz, a wszystkie pozostałe wartości jako prawda.

Program 6. Program wykorzystujący strukturę osoba o polach: imię, nazwisko, wiek. W programie wykorzystane są dwie funkcje:

Podstawy Pythona. Krzysztof Gdawiec. Instytut Informatyki Uniwersytet Śląski

6. Pętle while. Przykłady

Programowanie funkcyjne wprowadzenie Specyfikacje formalne i programy funkcyjne

Temat 1: Podstawowe pojęcia: program, kompilacja, kod

Języki programowania obiektowego Nieobiektowe elementy języka C++

Lekcja 5 - PROGRAMOWANIE NOWICJUSZ

Nazwa implementacji: Nauka języka Python pętla for. Autor: Piotr Fiorek

Przygotowanie własnej procedury... 3 Instrukcja msgbox wyświetlanie informacji w oknie... 6 Sposoby uruchamiania makra... 8

Programowanie w C++ Wykład 1. Katarzyna Grzelak. 26 luty K.Grzelak (Wykład 1) Programowanie w C++ 1 / 28

Paradygmaty programowania

Przedstawię teraz tzw. podstawowe symbole wyrażenia regularne (BRE, Basic Regular Expression)

Podstawy Programowania Podstawowa składnia języka C++

VII. Ciągi znaków łańcuchy

Naukę zaczynamy od poznania interpretera. Interpreter uruchamiamy z konsoli poleceniem

INFORMATYKA Studia Niestacjonarne Elektrotechnika

Linux Polecenia. Problem nadpisywania plików. Zmienna noclobber i noglob. Filtry i metaznaki. Problem nadpisywania plików. Opracował: Andrzej Nowak

Transkrypt:

Haskell Input and Output Źródło: http://learnyouahaskell.com/input-and-output Brak stanu i czystość funkcji Wstęp - Wejście i Wyjście Wspomnieliśmy że Haskell jest czysto funkcyjnym językiem. W językach imperatywnych rzeczy są robione przez dawanie komputerowi serii kroków do wykonania, a w językach funkcyjnych programowanie w większości jest definiowaniem czym rzeczy są. W Haskell-u, niemożliwe jest zmienienie jakiegoś stanu przez funkcję, np. zmienienie wartości jakiejś zmiennej. (zmianę jakiegoś stanu przez funkcję nazywamy efektem-ubocznym; funkcję która ma efekt uboczny nazywamy funkcja nieczystą). Jedyną rzeczą jaką funkcja w Haskellu może zrobić to zwrócenie jakiejś wartości, być może wyliczonej na podstawie wartości parametru. Jeśli funkcja Haskell-a będzie wywołana dwa razy, z taką samą wartością parametru, to musi zwrócić taką samą wartość. Mimo że wydaje się to trochę ograniczające, jeśli zna się tylko imperatywne języki programowania, to my zauważyliśmy że to jest jednak całkiem fajne i przydatne. W językach imperatywnych, nie ma gwarancji, że prosta funkcja, która powinna tylko coś wyliczyć, nie zmieni wartości jakiejś zmiennej. Na przykład kiedy (w Haskell-u) tworzone jest drzewo binarne, element nie jest dokładany do drzewa przez modyfikacje drzewa w miejscu. Funkcja w Haskellu w rzeczywistości zwraca nowe drzewo z nowym elementem gdyż nie jest możliwa modyfikacja/zmiana początkowego drzewa. Niemożliwość zmiany stanu przez funkcję jest zaletą, ponieważ pomaga rozumować o działaniu programów, ale rodzi jeden problem. Jeśli niemożliwe jest zmienienie czegokolwiek przez funkcję to jak ma zakomunikować ludziom to co wyliczyła. Aby zakomunikować wynik obliczeń, trzeba zmienić stan urządzenia wyjścia (zazwyczaj stan ekranu), który emituje fotony, które podróżują do oczu, i zmieniają stan ludzkiego umysłu. Okazuje się że Haskell ma naprawdę mądry system do obchodzenia się z funkcjami mającymi efekty uboczne, który w czysty sposób oddziela część programu która jest czysta, od nieczystej i który wykonuje cała nieczystą pracę np. komunikacją z ekranem lub klawiaturą. Z tymi częściami oddzielonymi, można nadal łatwo rozumować o czystej części, i czerpać korzyści z braku efektów ubocznych, takich jak lenistwo, zwiększona niezawodność (odporność na błędy), modularność, a zarazem efektywnie komunikować się z światem zewnętrznym. 1

Program Hello, world! Wpisz w edytorze tekstowym: main = putstrln "hello, world" Właśnie zdefiniowaliśmy nazwę main, a w niej wywołujemy funkcję nazywaną putstrln z parametrem Witaj świecie!. Wygląda zupełnie standardowo, ale nie jest zobaczymy za chwilę. Zapisz plik jako helloworld.hs. A teraz skompilujemy nasz program! Otwórz terminal, przejdź do katalogu gdzie helloworld.hs się znajduje i wykonaj to co poniżej: $ ghc --make helloworld [1 of 1] Compiling Main ( helloworld.hs, helloworld.o ) Linking helloworld... Okay!. Jeśli mamy trochę szczęścia, to dostaliście coś podobnego i teraz możecie uruchomić program poprzez. 2$./helloworld hello, world Nasz pierwszy skompilowany program, który wyświetla coś w terminalu. Przyjrzyjmy się temu co napisaliśmy. Najpierw spójrzmy na typ funkcji putstrln ghci> :t putstrln putstrln :: String -> IO () ghci> :t putstrln "hello, world" putstrln "hello, world" :: IO () Możemy przeczytać typ putstrln tak: putstrln bierze string i zwraca I/O akcje która zawiera coś typu () (i.e. pusty, tuple (n-tkę) pustą, także nazywaną jednostką (po angielsku unit)). Akcja I/O, kiedy wywołana spowoduje powstanie efektu ubocznego (zazwyczaj wczytanie czegoś z urządzenia wejścia, lub wyświetlenie czegoś na ekranie) a także będzie zawierać (nie całkiem zwracać) jakąś wartość w sobie. Wyświetlanie na ekranie nie powoduje zwracania jakiejś wartości, toteż nic nieznacząca wartość () jest składowana w akcji (zwracana ale nie całkiem). Pusta tuple jest wartością () i ma też typ (). Kiedy akcja I/O będzie wykonana? W grę wkracza main. Akcja I/O będzie wykonana kiedy nazwiemy ją main i uruchomimy/wykonamy program. 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ą. Spójrzcie na następujący przykład. putstrln "Hello, what's your name?" name <- getline 2

putstrln ("Hey " ++ name ++ ", you rock!") Interesująca nowa składnia! I w dodatku wygląda całkiem jak program napisany w języku imperatywnym. Jeśli skompilujecie ten program i wypróbujecie, to będzie pewnie działał tak jak oczekujecie. Zauważcie że tam jest napisane do a potem wypisana seria kroków zupełnie jak w programie imperatywnym. Każdy z tych kroków jest akcją I/O. Poprzez składanie ich razem przez użycie składni do stworzyliśmy (skleiliśmy) je w jedną akcje I/O. Akcja, którą otrzymaliśmy ma typ IO (),ponieważ takiego typu jest ostania akcja. Z tego powodu, main zawsze ma sygnature typu main :: IO coś, gdzie coś jest jakimś konkretnym typem. Jest przyjęte że zazwyczaj nie piszemy deklaracji typu do main. Nową i interesującą rzeczą, której wcześniej nie napotkaliśmy znajduje się w linii trzeciej name <- getline. Wygląda jakby czytała linie z urządzenia wejścia i przypisywała do zmiennej nazwanej name. Ale czy naprawdę. Sprawdźmy typ getline. ghci> :t getline getline :: IO String Aha getline jest akcją I/O która zawiera we wnętrzu rezultat typu String. Ma to sens, gdyż ta akcja poczeka aż użytkownik wprowadzi jakieś dane przy użyciu klawiatury i te dane będą reprezentowane jako string. A co z name <- getline? Można ten kawałek programu interpertować(rozumieć) tak: wykonaj akcje I/O getline i potem powiąż(wyciągnis z wnętrza akcji) wczytaną wartość z nazwą name. getline ma typ IO String, więc name będzie miało typ String. Można myśleć o I/O akcji jako o pudełku/kontenerze z nogami, które wniosą pudełko (I/O akcje) w rzeczywisty świat i coś w nim zrobią (np. namalują jakiś rysunek na ścianie) i może powrócą z jakimiś danymi w pudełku. Jak już pudełko z danymi powróciło, to jedynym sposobem na otwarcie i wyciągnięcie danych z tego pudełka jest użycie konstrukcji <-. A jeśli już chcemy się dobierać do danych w I/O akcji, to musimy robić to we wnętrzu innej I/O akcji. W taki oto sposób Haskell-owi udaje się starannie oddzielić czyste i nieczyste kawałki naszego kodu (programu). getline jest nieczyste bo zwracana wartość może być inna za każdym wywołaniem. Dlatego getline jest skażonym konstruktorem typu IO i można dostać się do danych tylko w kodzie I/O. I dlatego że kod I/O też jest skażony, a wszelkie obliczenia które zależą od danych I/O które są skażone, będą miały skażone wyniki. Kiedy mówię skażone, nie mam na myśli że naznaczone w sensie że nigdy nie możemy użyć danych powstałych w wyniku użycia akcji IO w czystym kodzie. Wcale nie, tymczasowo odkażamy dane w środku akcji I/O kiedy powiązujemy je (dane) z nazwą, to jest kiedy wykonujemy name <- getline, name to zwykły string, reprezentuje on to co jest w środku pudełka. Możemy mieć naprawdę skomplikowaną funkcję, która np. Bierze imię (zwykły string) jako parametr i mówi ci twój horoskop, całą przyszłość twojego życia, bazując tylko na twoim imieniu. Możemy zrobić to tak: putstrln "Hello, what's your name?" name <- getline 3

putstrln $ "Read this carefully, because this is your future: " ++ tellfortune name i funkcja tellforune (lub każda inna funkcja do której przekażemy name ) nie musi nic wiedzieć o I/O, jest zwykłą funkcją typu String -> String! Spójrzmy na ten kawałek kodu? Czy jest poprawny? nametag = "Hello, my name is " ++ getline Jeśli powiedziałeś nie, idź w nagrodę zjeść ciasteczko, miałeś racje. Jeśli powiedziałeś tak,, napij się miski wypełnionej roztopioną lawą wulkaniczną żartuję nie pij! Przyczyną błędu jest to że ++ wymaga aby oba parametry (ten przed ++ oraz ten po ) były listami z tymi samymi typami w środku. Parametr po lewej ma typ String (czyli [Char]), a getline zwraca IO String. Nie można połączyć string-u i akcji I/O. Najpierw trzeba wyciągnąć dane z akcji I/O dostając wartość typu String, a mamy tylko jeden sposób na to. name <- getline we wnętrzu jakiejś akcji I/O. Jeśli posługujemy się nieczystymi danymi, musimy to zrobić w skażonym środowisku. Więc skażenie rozprzestrzenia się jak grupa zombi, i w naszym najlepszym interesie jest to aby część kodu z I/O była jak najmniejsza. Każda akcja I/O która jest wykonana, zawiera jakieś dane w środku. Dlatego poprzedni program może być zapisany tak: foo <- putstrln "Hello, what's your name?" name <- getline putstrln ("Hey " ++ name ++ ", you rock!") Można zauważyć że zmienna foo będzie miała przypisaną wartość (), więc przypisywanie do zmiennej wartości z putstrln jest zbędne. W kolejnym putstrln nie wyciągamy wartości z akcji I/O i nie powiązujemy z jakąś zmienną. Robimy tak gdyż w sekcji/bloku do, ostatnia akcja nie może być powiązana/przypisana z nazwą tak jak pierwsze dwie w powyższym przykładzie. Dowiemy się dlaczego tak jest trochę później, kiedy poznamy monad-y. Na razie, możecie myśleć o tym że sekcja/blok do automatycznie wyciąga wartość z ostatniej akcji i powiązuje/przypisuje do własnej zwracanej wartości. Poza pierwszą linią, każda linia w bloku/sekcji do,co do której nie ma potrzeby przypisywania/powiązywania może być zapisana tak: _ <- putstrl coś coś. Ale to jest zbędnie wiec nie piszemy <- dla akcji I/O która nie zawiera przydatnej wartości, tak jak putstrln jakis tekst Poczontkujący czasami myślą o zrobieniu czegoś takiego: name = getline czytają z wejścia i potem przypisują wartość z akcji do zmiennej name. To nie zadziała. To po prostu daje akcji I/O nową nazwę: name. Pamiętajcie, aby wydostać wartość z akcji I/O, trzeba to zrobić w środku/wewnątrz innej akcji I/O poprzez powiązanie z nazwą zmiennej operatorem <- 4

Akcje I/O są wykonywane tylko kiedy nazwie się je main albo kiedy one są w środku większej akcji I/O którą składamy poprzez użycie bloku/sekcji do. Używamy bloku/sekcji do w celu sklejenia razem kilku akcji I/O, w celu użycia w innym większym bloku/sekcji do i tak dalej. Mamy jeszcze jeden przypadek gdzie akcje I/O są wykonywane. Kiedy wypiszemy akcje I/O w interpreterze GHCI i naciśniemy Enter to akcja zostanie wykonana/ ghci> putstrln "HEEY" HEEY Nawet kiedy możemy wpisać liczbę lub wywołać funkcję w GHCI a potem nacisnąć return, obliczenia zostaną wykonane, na wynikach zostanie wywołane show, a na końcu wypisany tekst w terminalu przy niejawnym użyciu putstrln Pamiętacie komendę let? Przeczytajcie sekcje o niej. Ma formeę let bindings in expression, gdzie bindings są nazwami nadawanymi wyrażeniom a expression to wyrażenie które ma być wykonane. Powiedzieliśmy że w lsit comprehensions część in nie jest potrzebna,. Można ich używać w sekcjach do prawie tak samo jak w list comprehensions. Przeanalizujmy to: import Data.Char putstrln "What's your first name?" firstname <- getline putstrln "What's your last name?" lastname <- getline let bigfirstname = map toupper firstname biglastname = map toupper lastname putstrln $ "hey " ++ bigfirstname ++ " " ++ biglastname ++ ", how are you?" Widać jak akcje I/O w skecji do są ustawione. Można też zauważyć jak let są ustawione z akcjami I/O i nazwy w let są ustawione razem? To dobra praktyka, gdyż wcięcia są ważne w Haskell-u. Wykonaliśmy map toupper firstname, co np. zamienia John w JOHN. Powiązaliśmy ciągi znaków składające się z dużych liter do nazwy i użyliśmy w ciągach znaków wyświetlanych na ekranie. Zastanawiacie się kiedy używać a kiedy let? Pamiętajcie, jest między innymi do wykonywania akcji I/O i powiązywania ich wyników do nazw zmiennych. map toupper firstname, nie jest akcją I/O. Jest czystym wyrażeniem w Haskell-u. Używamy aby powiązać wynik akcji I/O do nazwy a wyrażenia let w celu powiązania czystych wyrażeń do nazw. Robiąc coś let firstname = getline nazwalibyśmy I/O akcje getline dodatkową nazwą firstname i nadal musielibyśmy użyć aby ją wykonać. Teraz zrobimy program który cały czas wczytuje linie i wypisuje ze słowami w których litery są odwrotnej kolejności. Program się zatrzymuje po wpisaniu pustej linii. Oto ten program: line <- getline if null line then return () 5

else do putstrln $ reversewords line main reversewords :: String -> String reversewords = unwords. map reverse. words Aby poczuć co program robi, możecie wykonać go przed omówieniem kodu. WSKAZOWKA: Aby uruchomić program, można go skompilować i wykonać plik wykonywalny ghc make helloworld i potem./hellowold lub można użyć komendy runhaskell tak: runhaskell helloworld.hs Najpierw spójrzmy na funkcje reversewords. To zwykła czysta funkcja biorąca string np. hey there man i potem wykonująca na nim words aby wyprodukować listę słów [ hey, there, man ] Potem używamy map reverse na litej liście otrzymując [ yeh, ereht, nam ] i na końcu z listy tworzymy jeden string przy użyciu unwords otrzymując yeh ereht nam Przyjrzyjcie jak użyliśmy składania funkcji. Bez kompozycji funkcji trebaby było napisać reversewords st = unwords(map reverse (words st)). A co z main? Najpierw wczytujemy linie z terminala poprzez wywołanie getline. Poniżej mamy wyrażenie warunkowe if. Pamiętajcie że w Haskell-u każde if musi mieć odpowiadające else gdyż każde wyrażenie musi zwracać jakąś wartość. Konstruujemy wyrażenie if tak że kiedy warunek jest prawdą (true) (w tym przypadku kiedy wpisana linia jest pusta) wykonujemy jedną akcje I/O a kiedy warunek nie jest spełniony akcja IO pod else jest wykonywana. Z tych powodów w I/O sekcji do if-y muszą mieć taką formę: if condition then I/O action else I/O action Popatrzmy co się dzieje w członie else Skoro musimy mieć dokładnie jedną akcje I/O po else używamy do do sklejenia dwóch akcji I/O w jedną. Można by było zapisać tą cześć tak: else (do putstrln $ reversewords line main) Co się dzieje kiedy null line jest prawdą? Co jest wykonywane po then? Widzimy że tam jest napisane then return (). Jeśli znacie języki imperatywne takie jak C, Java czy Python pewnie myślicie że wiecie co robi return. W Haskell-u return to coś innego niż w większości innych języków i dlatego nazwa return dezorientuje ludzi. W językach imperatywnych, return zazwyczaj kończy wykonywanie metody, lub podprogramu i zazwyczaj zwraca jakąś wartość do miejsca wywołania.. W Haskell-u return, (zwłaszcza w akcji I/O) tworzy akcje I/O z czystej wartości. Jeśli użyjemy analogii pudełka to return bierze wartość i opakowuje ją pudełkiem. Nowo powstała akcja nic nie robi,ma tylko jakąś wartość. Więc w kontekście I/O return haha będzie typu IO String. Poco zamieniać czystą wartość w akcje I/O która nic nie robi? Po co plamić program dodatkową akcją I/O - bo potrzeba jakiejś akcji I/O w przypadku pustej linii na wejściu. Dlatego tworzymy pustą akcje, która nic nie robi poprzez return (). Użycie return nie skutkuje w zakończeniu wykonywania I/O sekcji do. Następujący program wykona wszystkie linie aż do końca. 6

return () return "HAHAHA" line <- getline return "BLAH BLAH BLAH" return 4 putstrln line Te wszystkie return -y nic nie robią poza kapsułkowaiem wartości. Owe wartości nie są użyte, nie są powiązane z jakąś nazwą. Jest możliwe użycie return oraz do związania jakiejś wartości z nazwą. a <- return "hell" b <- return "yeah!" putstrln $ a ++ " " ++ b Jak widać return jest czymś w rodzaju odwrotności do. Wiadomo że return bierze wartość i opakowuje ją pudełkiem, a bierze pudełko (i wykonuje je (jakąś akcje)), wyjmuje wartość z pudełka i wiąże z nazwą. W powyższym przykładzie użycie return i zaraz potem jest zbędne. Można użyć let w środku sekcji do tak jak poniżej. let a = "hell" b = "yeah" putstrln $ a ++ " " ++ b Podczas używania I/O sekcji do najczęściej używamy return bo trzeba stworzyć akcję I/O która nic nie robi, albo nie chcemy aby akcja I/O tworzona przez sekcje do miała wartość ostatniej akcji zawartej w sekcji do, a chcemy inną wartość którą tworzymy przez return z potrzebną wartością w środku. Owe return umieszczamy jako ostatnią linie w sekcji do. Sekcja do może mieć tylko jedną akcję I/O, a wtedy można zastąpić sekcje do właśnie tą akcją I/O. Niektórzy preferują napisanie then do return () bo w jednym z powyższych przykładów po else było do. Przed rozpatrywaniem operacji na plikach, spójrzmy na kilka przydatnych funkcji przy pracy z I/O. PutStr jest prawie takie same jak putstrln, bo bierze string jako parametr a zwraca akcję I/O która wyświetla string na ekranie ale nie przechodzi do nowej linii.. putstr "Hey, " putstr "I'm " putstrln "Andy!" $ runhaskell putstr_test.hs Hey, I'm Andy! Ma sygnaturę typu putstr :: String -> IO () więc wartością opakowaną jest () (unit), wartość bezużyteczna więc nie potrzeba powiązywania jej a nazwą. 7

putchar bierze znak i zwraca I/O akcje która wyświetli go na ekranie putchar 't' putchar 'e' putchar 'h' $ runhaskell putchar_test.hs teh putstr jest definiowany rekurencyjnie przy użyciu putchar. Graniczny warunek putstr-a jest pusty string, więc kiedy wyświetlamy pusty string, akcja tworzona przez return () jest zwracana. Jeśli string nie jest pusty, wyświetl pierwszy znak przy użyciu putchar, a resztę przy użyciu putstr. putstr :: String -> IO () putstr [] = return () putstr (x:xs) = do putchar x putstr xs Widzimy że można używać rekurencji w I/O, tak jak w czystym kodzie. Tak jak w czystym kodzie definiujemy brzegowy przypadek i potem myślimy czym jest wynik. To akcja wyświetlająca pierwszy znak, a po nim wyświetlająca resztę string-u. 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 2 print "haha" print 3.2 print [3,4,3] $ runhaskell print_test.hs True 2 "haha" 3.2 [3,4,3] Pamiętacie kiedy rozmawialiśmy o akcjach I/O wykonywanych tylko wtedy kiedy są w main, lub kiedy wykonujemy je w GHCI. Kiedy wpiszemy wartość (np. 3 lub [1,2,3]) i naciśniemy klawisz return, GHCI użyje print aby wyświetlić wartość na ekranie. ghci> 3 3 ghci> print 3 3 ghci> map (++"!") ["hey","ho","woo"] ["hey!","ho!","woo!"] 8

ghci> print (map (++"!") ["hey","ho","woo"]) ["hey!","ho!","woo!"] Kiedy chcemy wyświetlić string, zazwyczaj używamy putstrln, gdyż nie chcemy mieć tekstu w cudzysłowach. Do innych typów używamy print. Akcja getchar czyta jeden znak z wejścia i ma następującą sygnaturę typu: getchar :: IO Char, bo wartość w tej akcji ma typ Char. Zauważcie że z powodu buforowania, wczytanie znaku nie nastąpi dopóki klawisz return nie zostanie wciśnięty. c <- getchar if c /= ' ' then do putchar c main else return () Po przyjrzeniu się programowi wygląda na to że powinien wczytać znak, sprawdzić czy jest to spacja jeśli tak to zakończyć wykonywanie programu, w przeciwnym wypadku wyświetli c literę na ekranie, po czym zrobić to samo od nowa. Ten program prawie tak działa: $ runhaskell getchar_test.hs hello sir hello W drugiej linii widać co wpisane zostało z klawiatury (hello sir). Z powodu buforowania wykonywanie programu rozpocznie się kiedy naciśniemy enter a nie po pierwszej literze. Ale kiedy wciśniemy enter, to działa z tym co zostało wpisane. Funkcja when znajduje się w Control.Monad (aby dostać dostęp do niej trzeba wpisać import Control.Monad) Interesujące jest to że sekcja do wygląda jak instrukcja kontroli przepływu, lecz jest zwykłą funkcją która bierze wartość typu boolean oraz akcje I/O. Jeśli wartość boolean jest True ta sama akcja I/O jest zwracana, w przeciwnym przypadku zwraca return () czyli akcję która nic nie robi. Moglibyśmy powyższy program zapisać tak: import Control.Monad c <- getchar when (c /= ' ') $ do putchar c main Jak widać można użyć when w przypadku wzorca: if coś then do jakaś kacja IO esle return () sequence bierze listę akcji I/O i zwraca akcje I/O wykonującą akcje z listy jedna po drugiej. Wartością wewnątrz z tej nowej akcji I/O będzie lista wartości z każdej akcji na liście. Sygnatura typu to: sequence :: [IO a] IO [a]. Dwa poniższe programy są równoważne: 9

a <- getline b <- getline c <- getline print [a,b,c] 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. Często używamy sequence kiedy używamy funkcji map z print lub putstrln na listach, np. map print [1,2,3,4] nie stworzy akcji I/O, ale taką listę akcji I/O: [print 1, print 2, print 3, print 4]. Jeśli chcemy zmienić tę listę akcji I/O w akcję I/O to musimy użyć sequence. ghci> sequence (map print [1,2,3,4,5]) 1 2 3 4 5 [(),(),(),(),()] A co z [(),(),(),(),()] na koncu? Kiedy GHCI wykona akcje I/O, wyświetla ich wynik chyba że wynikiem jest (), wtedy nie jest wyświetlany. Dlatego wykonanie putstrln hehe w GHCI wyświetla tylko hehe (gdyż wartością w środku jest ()), a kiedy wykonamy getline w GHCI to wynikiem jest akcja I/O której wartość jest wyświetlona.. getline ma typ IO String. Skoro użycie map z funkcją zwracającą akcję I/O na liście jest powszechne, stworzono funkcję mapm, która bierze funkcję i listę, wykonuje map funkcja na liście a na wyniku używa sequence. mapm_ robi to samo, lecz ignoruje wynik. ghci> mapm print [1,2,3] 1 2 3 [(),(),()] ghci> mapm_ print [1,2,3] 1 2 3 forever bierze akcję I/O i zwraca akcję która powtarza początkową akcje w nieskończoność, a znajduje się w Control.Monad. Następujący program zawsze będzie się pytał o wpisanie czegoś, po czym wyświetli owy tekst dużymi literami. import Control.Monad 10

import Data.Char main = forever $ do putstr "Give me some input: " l <- getline putstrln $ map toupper l form z Control.Monad jest jak mapm, z tą różnicą że ma parametry w odwrotnej kolejności. Pierwszym parametrem jest lista, drugim funkcja. Dlaczego jest to przydatne? W niektórych kreatywnych użyciach lambdy i notacji do można zrobić coś takiego: import Control.Monad colors <- form [1,2,3,4] (\a -> do putstrln $ "Which color do you associate with the number " ++ show a ++ "?" color <- getline return color) putstrln "The colors that you associate with 1, 2, 3 and 4 are: " mapm putstrln colors Funkcja (\a -> do... ) bierze liczbę i zwraca akcję I/O. Musieliśmy otoczyć ją nawiasami, w przeciwnym wypadku lambda myślałaby że dwie ostatnie akcję należą do niej. Zwróćcie uwagę na return color w bloku do. Robimy tak aby akcja I/O definiowana przez sekcję do zawierała w sobie color. Tak naprawdę nie trzeba by tego robić. Zamiast color getline return color można by użyć tylko samej getline. Odpakowywanie koloru przez i pakowanie go przez return jest zbędne. form (wywołane z dwoma parametrami) produkuje akcję I/O, których wynik wiążemy z nazwą colors. colors to zwykła lista z string-ami. Na końcu wyświetlamy kolory z listy przez użycie mapm putstrln colors Można myśleć o form tak: stwórz akcję I/Odla każdego elementu w liście. Co każda akcja I/O zrobi zależy na elemencie użytym do stworzenia akcji..na koniec wykonaj owe akcje, i zwiąż wyniki do jakieś nazwy. Nie musimy wiązać, można wyrzucić $ runhaskell form_test.hs Which color do you associate with the number 1? white Which color do you associate with the number 2? blue Which color do you associate with the number 3? red Which color do you associate with the number 4? orange The colors that you associate with 1, 2, 3 and 4 are: white blue 11

red orange Można by było powyższy program napisać bez użycia form, ale taki jest bardziej czytelny. Zazwyczaj używamy form kiedy chcemy zmapować sekwencje jakiś akcji które definiujemy w miejscu przy użyciu notacji do. Mogliśmy też ostatnią linię zapisać tak: colors putstrln W tej sekcji nauczyliśmy się podstaw wejścia i wyjścia. Dowiedzieliśmy się czym są akcje I/O, jak one umożliwiają wykonanie operacji wejścia/wyjścia podczas ich wywoływania. W skrócie, akcje I/O są wartościami takimi samymi jak każde inne wartości w Haskell-y. Mozna ich użyć jako parametrów do funkcji, funkcję mogą je (akcje I/O) zwracać jako wyniki. Co je odróżnia to że jeśli wpadną do funkcji main (lub jako wynik w GHCI), są wtedy wykonywane, i wtedy one wyświetlą rzeczy na ekranie, grają muzykę przez głośniki. Każda akcja I/O opakowuje także wartość którą pobrała ze świata zewnętrznego. Nie myślcie o funkcji putstrln i podobnych, jako o funkcji która bierze string i wyświetla go na ekranie. Myślcie jako o funkcji która bierze string a zwraca akcję I/O. Ta akcja I/O, kiedy wykonana wyświetli coś na ekranie np. piękną poezje. 12

Pliki i strumienie getchar to akcja I/O czytająca pojedynczy znak z terminala, a getline to akcja I/O czytająca linię. Są to proste funkcje i większość języków programowania ma funkcje lub wyrażenia o podobnym działaniu lub efekcie. Teraz spotkamy się z getcontent akcje I/O która czyta wszystko ze standardowego wejścia, aż do napotkania znaku końca pliku. Jej typ to getcontents :: IO String. Fajne jest to że getcontents wykonuje leniwe I/O. Wykonanie foo<- getcontents nie skutkuje wczytaniem całego wejścia naraz, przechowania w pamięci i powiązania z nazwą foo. Skoro jest leniwe to przeczyta dane z wejścia później,kiedy będą potrzebne. getcontents jest użyteczne kiedy łączymy wyjście jakiegoś programu z wejściem innego. Jeśli nie wiesz jak potoki działają na unix-owych systemach, tutaj jest krótki elementarz. Stwórzmy plik tekstowy z następującym haiku. I'm a lil' teapot What's with that airplane food, huh? It's so small, tasteless Tak, ten haiku jest do bani. Jeśli ktoś zna dobry wstęp do haiku, powiedzcie mi o tym. Przypomnijmy program wprowadzający funkcje forever. Pytał użytkownika o wprowadzenie linii, po czym wypisywał ale używając dużych liter, i tak w nieskończoność. import Control.Monad import Data.Char main = forever $ do putstr "Give me some input: " l <- getline putstrln $ map toupper l Zapiszcie ten program jako capslocker.hs i skompilujcie go. Użyjemy unixowych potoków aby wysłać zawartość pliku tekstowego do naszego krótkiego programu. Pomoże nam GNU cat program, który wyświetla zawartość pliku podanego jako argument. $ ghc --make capslocker [1 of 1] Compiling Main ( capslocker.hs, capslocker.o ) Linking capslocker... $ cat haiku.txt I'm a lil' teapot What's with that airplane food, huh? It's so small, tasteless $ cat haiku.txt./capslocker --program cat wyswietla zawartosc pliku na standardowym wyjściu zazwyczaj ekranie a kreska przekierowuje wyjście jednego programu do wejścia drugiego I'M A LIL' TEAPOT WHAT'S WITH THAT AIRPLANE FOOD, HUH? IT'S SO SMALL, TASTELESS 13

capslocker <stdin>: hgetline: end of file Jak widać, wysłanie wyjścia jednego programu (w tym przypadku cat) do wejśćia innego (capsloker) jest wynikiem użycia znaku. To co zrobiliśmy jest równoważne uruchomieniu capsloker i wpisaniu haiku na klawiaturze a potem wpisanie znaku końca pliku (zazwyczaj przez naciśnięcie ctrl-d ). To jest jak uruchomienie cat haiku.txt i powiedzenie Czekaj nie wyświetlaj tego w terminalu, przekaz to do capsloker-a. Widać że to co robimy przy użyciu forever to wczytywanie z wejścia, przemienianie tego, a następnie wysyłanie na wyjście. Z tego powodu możemy użyć getcontents aby bardziej skrócić nasz program. import Data.Char contents <- getcontents putstr (map toupper contents) Wykonujemy I/O akcje getcontents i powiązujemy string z nazwą contents. Potem używamy map touppper na tym string-u i wyświetlamy na ekranie. Miejcie na uwadze że skoro string-i są listami, które są leniwe, oraz getcontents jest I/O leniwe nie spróbuje wczytać wszystkiego naraz do pamięci przed wyświetleniem przy użyciu tylko dużych liter na ekranie, będzie wyświetlał zmienione dane podczas wczytywania, ponieważ przeczyta tylko kiedy będzie potrzebował. $ cat haiku.txt./capslocker I'M A LIL' TEAPOT WHAT'S WITH THAT AIRPLANE FOOD, HUH? IT'S SO SMALL, TASTELESS Fajnie to działa. Co jeśli uruchomimy capsloker i spróbujemy sami wpisywać linie.? $./capslocker hey ho HEY HO lets go LETS GO Kończymy działanie programu przez wciśnięcie ctrl-d. Całkiem miłe. Jak widać, wyświetlają się linie napisane tylko dużymi literami, linia po linii. Kiedy wynik z getcontents jest powiązany z contents, nie jest reprezentowany w pamięci jako string, lecz jako obietnica że string będzie wyprodukowany/dostarczony. Kiedy używamy map toupper na contents, to jest to też obietnicą że te funkcje w końcu kiedyś zostaną wykonane. W końcu putstr mówi: Hej potrzebuję linii do wyświetlenia, i mówi do contents : hej wczytaj wreszcie linię z terminala. Dopiero wtedy getcontetns naprawdę wczyta coś z terminala i da linie tekstu kodowi który faktycznie o to prosił. Ten kod używa wtedy map toupper i przekazuje wynik do putstr aby był wyświetlony. Po tym putstr mówi: Dajcie mi kolejna linię.cała sytuacja się powtarza dopóki są jakieś dane na wejściu, czyli dopóki nie napotka się znaku końca pliku. Napiszmy program który czyta jakieś dane, i wyświetla tylko linie krótsze niż 10 znaków: 14

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 Zrobiliśmy sekcję z I/O tak krótką jak tylko to możliwe. Skoro program ma wczytywać jakieś dane z wejścia i wyświetlać coś bazując na tym co wczytane,to można go zaimplementować tak aby wczytywał dane, wykonywał jakąś funkcję na nich, a potem wyświetlał to co funkcja zwróciła. Funkcja shortlinesonly działa w nastepujący sposób. Bierze string np. short\nlooooooooooooong\nshort again składający się z trzech linii dwie z nich są krótkie, środkowa długa. Omawiana funkcja wywołuje funkcję lines na tym string-u, która zamienia string na listę : ["short", "looooooooooooooong", "short again"] nazwaną alllines. Ta lista jest następnie filtrowana aby linie krótsze niż 10 liter zostały w liście ["short", "short again"]. Na końcu użyta jest funkcja unlines aby połączyć elementy listy w jeden string dając "short\nshort again". i'm short so am i i am a loooooooooong line!!! yeah i'm long so what hahahaha!!!!!! short line loooooooooooooooooooooooooooong short $ ghc --make shortlinesonly [1 of 1] Compiling Main ( shortlinesonly.hs, shortlinesonly.o ) Linking shortlinesonly... $ cat shortlines.txt./shortlinesonly i'm short so am i short Jak widać wysyłamy zawartość pliku shortlines.txt poprzez użycie unixowych strumieni do wejścia programu shortlines i w rezultacie wyjściowymi liniami są te krótkie. Widać że wzorzec wczytywania jakiś string-ów z wejścia, zmieniania ich funkcją, i potem wyprowadzania rezultatów działania funkcji na wyjście jest tak często spotykany że stworzono funkcje nazywaną interact, która bierze funkcję String -> String jako parametr i zwraca akcję I/O która pobierze coś z wejścia, wykona funkcję, a potem wydrukuje wynik działania funkcji. Zmodyfikujmy program: 15

main = interact shortlinesonly shortlinesonly :: String -> String shortlinesonly input = let alllines = lines input shortlines = filter (\line -> length line < 10) alllines result = unlines shortlines in result Powyższy program można jeszcze skrócić (ale będzie mniej czytelny) poprzez użycie złożenia funkcji. : main = interact $ unlines. filter ((<10). length). lines Ale fajnie skróciliśmy program do jednej linii! Funkcja interact może być użyta do napisania programów do których wejścia zostaną przesłane jakieś dane, a potem coś (dane) zostaną wysłane na wyjście, lub do napisania programów, które wydają się wczytywać linie od użytkownika, potem zwracające rezultaty bazujące na tej linii, po czym wczytujące kolejną linię i tak dalej. Tak naprawdę nie ma różnicy między takimi programami,wszystko zależy od tego jak użytkownik użyje programu. Napiszmy program który cały czas czyta linie potem mówi nam czy linia jest palindromem. Moglibyśmy użyć getline do wczytania linii, powiedzieć czy linia jest palindromem i uruchomić main jeszcze raz ale jest prościej użyć interact. Kiedy używacie interact, myślcie co potrzeba zrobić aby zmienić dane wejściowe na dane wyjściowe. W rozważanym programie trzeba zamienić linie na palindrome lub not a palindrome Trzeba więc napisać funkcję która zmieni "elephant\nabcba\nwhatever" w "not a palindrome\npalindrome\nnot a palindrome". Zróbmy to! respondpalindromes contents = unlines (map (\xs -> if ispalindrome xs then "palindrome" else "not a palindrome") (lines contents)) where ispalindrome xs = xs == reverse xs A teraz napiszmy w stylu zwanym point free czyli bez użycia nazwy parametru. respondpalindromes = unlines. map (\xs -> if ispalindrome xs then "palindrome" else "not a palindrome"). lines where ispalindrome xs = xs == reverse xs Całkiem proste. Najpierw zamieniamy "elephant\nabcba\nwhatever" w ["elephant", "ABCBA", "whatever"] a potem mapujemy lambdę na tym otrzymując ["not a palindrome", "palindrome", "not a palindrome"] a potem używamy unlines aby połączyć listę w jeden string ze znakiem nowa linia oddzielającym wiersze. A teraz można: main = interact respondpalindromes Przetestujmy: 16

$ runhaskell palindromes.hs hehe not a palindrome ABCBA palindrome cookie not a palindrome Mimo że stworzyliśmy program który zmienia jeden duży string z wejścia w inny to działa jak program robiący to samo linia po linii. Tak jest ponieważ Haskell jest leniwy, i chce wydrukować pierwszą linie wynikowego stringu, ale nie może gdyż nie ma jeszcze pierwszej linii z wejścia. Ale jak tylko podamy mu pierwsza linię to wypisze pierwszą linie rezultatu. Wyjdziemy z programu poprzez wpisanie znaku końca linii. Możemy też użyć tego programu strumieniując dane na wejście. Powiedzmy że mamy plik words.txt. dogaroo radar rotor madam A teraz użyjmy unixowych strumieni. $ cat words.txt runhaskell palindromes.hs not a palindrome palindrome palindrome palindrome Znowu otrzymalibyśmy taki sam wynik jeśli byśmy wpisywali słowa sami z klawiatury (standardowe wejście). Nie widać tylko danych wejściowych bo pochodzą z pliku a nie z pisania na klawiaturze. Wiec pewnie teraz wiesz jak działa leniwe I/O i możesz użyć tego dla własnych korzyści. Umiesz myśleć w takich kategoriach: jaki powinien być wynik mając określone dane wejściowe i napisać funkcje do potrzebnej zmiany danych wejściowych. W leniwym I/O nic nie jest zjedzone z wejścia dopóki nie jest całkowicie potrzebne, bo co chcemy wyświetlić teraz, zależy na tych danych. Jak na razie pracowaliśmy z I/O poprzez wyświetlanie czegoś na ekranie lub czytanie z klawiatury. A co z czytaniem i zapisywaniem plików. Już to robiliśmy. Jeden ze sposobów myślenia o czytaniu z klawiatury jest wyobrażenie sobie, że to jest takie same jak czytanie z (trochę specjalnego) pliku. To samo dotyczy wyświetlania jakiegoś tekstu na ekranie. To jest rodzaj zapisywania do pliku. Nazywamy owe pliki stdin i stdout, znaczy standardowe wejście i standardowe wyjście. Mając to na uwadze widzimy że zapisywanie i czytanie z plików jest bardzo podobne do czytania i zapisywania do standardowego wejścia i wyjścia. 17

Zacznijmy z bardzo prostym programem otwierającym plik nazwany girlfriend.txt, który zawiera wiersze z hitu piosenkarki Avril Lavigne i wyświetla je na ekranie. Oto plik girlfriend.txt Hey! Hey! You! You! I don't like your girlfriend! No way! No way! I think you need a new one! A tu nasz program: import System.IO handle <- openfile "girlfriend.txt" ReadMode contents <- hgetcontents handle putstr contents hclose handle Po uruchomieniu widać oczekiwany rezultat. $ runhaskell girlfriend.hs Hey! Hey! You! You! I don't like your girlfriend! No way! No way! I think you need a new one! ---------- Omówmy program linia po linii! Nasz program ma kilka akcji I/O sklejonych razem przez sekcję do. W pierwszej linii bloku do, zauważamy nową funkcję openfile. Jej sygnatura to openfile :: FilePath -> IOMode -> IO Handle, czyli openfile bierze ścieżkę dostępu wraz z nazwą pliku, IOMode i zwraca akcję I/O która otworzy plik, i będzie miała uchwyt do pliku zakapsułkowany w sobie. FilePath jest synonimem String definiowanym type FilePath = String IOMode to typ zdefiniowany tak: data IOMode = ReadMode WriteMode AppendMode ReadWriteMode Tak jak nasz typ reprezentujący siedem możliwych wartości dla dni tygodni, ten typ jest typem wyliczeniowym reprezentującym co możemy zrobić z naszym otwartym plikiem. Bardzo proste. Zauważcie tylko że ten typ jest IOMode a nie IO Mode. IO Mode byłby typem akcji I/O zawierającym w sobie wynik będący wartością jakiegoś typu Mode. IOMode to tylko prosty typ wyliczeniowy. Zwrócona akcja I/O otworzy odpowiedni plik w odpowiednim trybie. Jeśli powiążemy tą akcję do jakieś nazwy to otrzymamy (uchwyt pliku) Handle. Wartość typu Handle reprezentuje lokalizację pliku. Używamy tego uchwytu do operacji na pliku. Byłoby głupotą czytanie pliku ale nie 18

powiązanie z uchwytem pliku bo to by nic nie dało. W naszym przykładzie nazwa handle ma odpowiednia wartość typu Handle W kolejnej linii widzimy funkcję zwaną hgetcontents biorącą uchwyt pliku jako parametr, więc wie z którego pliku czytać dane i zwraca IO String akcje I/O zawierającą w sobie zawartość pliku. Ta funkcja jest prawie jak getcontents. Jedyną różnicą jest to że getcontent automatycznie czyta ze standardowego wejścia (z klawiatury). We wszystkich innych aspektach owe funkcje działają tak samo. Tak jak getcontents, hgetcontents nie pokusi się na wczytanie całego pliku naraz do pamięci, lecz będzie czytać tylko jak zajdzie potrzeba. To całkiem Fajne. Gdyż możemy traktować contents jako całą zawartość pliku, lecz nie jest tak naprawdę w pamięci. Więc jeśli mamy naprawdę duży plik, użycie hgetcontents nie zużyje naszej pamięci, lecz wczyta to co jest potrzebne w danym momencie. Zwróćcie uwagę na różnice między uchwytem do pliku używanym do zidentyfikowania pliku a zawartością pliku. Uchwyt do pliku jest czymś dzięki czemu wiemy czym nasz plik jest. Jeśli wyobrazicie sobie że cały system plików jest dużą książką, każdy plik rozdziałem, uchwyt pliku jest zakładką pokazującą skąd obecnie czytamy (lub piszemy), w którym rozdziale jesteśmy, a zawartość to cały rozdział. Funkcja putstr contents wyświetla zawartość na standardowym wyjściu, potem wykonujemy hclose biorącym handle jako parametr co zwraca akcje I/O zamykającą plik. Trzeba zamknąć plik po otwarciu przez openfile. Innym sposobem robienia tego co właśnie zrobiliśmy jest użycie funkcji withfile mającej sygnature: withfile :: FilePath -> IOMode -> (Handle -> IO a) -> IO a. Owa funkcja bierze ścieżkę z nazwa pliku (typ FilePath) oraz IOMode potem funkcję biorącą uchwyt pliku i zwracającą jakąś akcje I/O. Funkcja withfile zwraca akcję I/O otwierającą określony plik, robiącą to co chcemy z plikiem po czym zamykającą go. Wynik jest zakapsułkowany w akcji I/O która jest zwrócona, jest taki sam jak wynik I/O akcji funkcji będącej trzecim parametrem. To może wyglądać trochę skomplikowanie, ale jest całkiem proste, zwłaszcza z lambdami. Oto nasz poprzedni program napisany przy użyciu withfile import System.IO withfile "girlfriend.txt" ReadMode (\handle -> do contents <- hgetcontents handle putstr contents) Jak widać ten program jest bardzo podobny do poprzedniego. (\handle ->...) to funkcja biorąca uchwyt do pliku i zwracająca akcje I/O. Zazwyczaj robi się to jak tu przy użyciu lambda. Powodem na branie jako parametr funkcji biorącej uchwyt pliku i zwracającej akcję I/O zamiast brani akcji I/O jest to że aby stworzyć akcje I/O działającą na określonym pliku jest potrzebny uchwyt do tego pliku. Funkcja withfile otwiera plik, potem przekazuje uchwyt do funkcji która podaliśmy jako parametr, otrzymuje akcję I/O z owej funkcji. Potem tworzy nową akcję prawie taką samą jak ta stworzona prze funkcje, ale zamykającą plik. Oto jak byśmy zdefiniowali własne withfile 19

withfile' :: FilePath -> IOMode -> (Handle -> IO a) -> IO a withfile' path mode f = do handle <- openfile path mode result <- f handle hclose handle return result Wiemy że wynikiem będzie akcją I/O więc można zacząć z do. Najpierw otwieramy plik i powiązujemy uchwyt pliku z nazwą handle. Potem wywołujemy naszą funkcję dając mu uchwyt pliku jako parametr, dostając akcję I/O która zrobi całą pracę. Powiązujemy to co w akcji z nazwą result, zamykamy plik przez wywołanie hclose handle a Na końcu tworzymy akcję zwracaną przez sekcje do (return result). Jak pamiętacie użycie return result Jakaś_nazwa zwraca akcję I/O z zakapsułkowanymi jakimiś danymi, w tym przypadku danymi wyciągniętym z akcji zwróconej przez funkcję f. Widać więc że f handle zwróci akcję która przeczyta linie z standardowego wejścia i zapisze je do pliku i będzie miała zakapsułkowaną liczbę linii wczytanych ze standardowego wejścia. Jeśli użylibyśmy funkcję f z withfile, to końcowa akcja I/O także by miała w sobie liczbę linii wczytanych z wejścia. Tak jak mamy hgetcontents działający z plikami tak samo jak getcontents ze standardowym wejściem są funkcje hgetline, hputstr, hputstrln, hgetchar, itp. Działają tak jak odpowiedniki bez h, ale potrzebują parametru będącego uchwytem pliku, aby wiedzieć z jakim plikiem działają. I tak putstrln to funkcja biorąca string i zwracająca akcję I/O która wyświetli ten string na ekranie oraz przejdzie do nowej linii. Analogicznie hputstrln bierze uchwyt pliku oraz string i zwraca akcję I/O która zapisze string do pliku skojarzonego z uchwytem, zapisze znak nowej linii za stringiem. I tak samo hgetline bierze uchwyt pliku i zwraca akcję I/O czytającą linię z pliku, Czytanie i pisanie zawartości pliku jako string-ów jest tak częste że mamy funkcje robiące nasze życie łatwiejszym. Funkcja readfile ma sygnature typu: readfile :: FilePath -> IO String. Pamiętajcie ze FilePath to inna nazwa typu 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 (oczywiście w leniwy sposób), 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. Oto jak możemy zapisać poprzedni program używając readfile. import System.IO contents <- readfile "girlfriend.txt" putstr contents Skoro nie dostajemy uchwytu do pliku, sami nie możemy zamknąć pliku, Haskell i readfile zamykają plik automatycznie. 20

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 grilfirend.txt w wersje zapisaną dużymi literami i zapisanie go jako girlfriendcaps.txt: import System.IO import Data.Char contents <- readfile "girlfriend.txt" writefile "girlfriendcaps.txt" (map toupper contents) $ runhaskell girlfriendtocaps.hs $ cat girlfriendcaps.txt HEY! HEY! YOU! YOU! I DON'T LIKE YOUR GIRLFRIEND! NO WAY! NO WAY! I THINK YOU NEED A NEW ONE! Funkcja appendfile ma sygnaturę typu taką samą jak writefile i appendfile nie skraca pliku do długości zero przed zapisanie jeśli plik już istnieje, ale dodaje nowe rzeczy na końcu pliku. Powiedzmy że mamy plik todo.txt który ma jedną rzecz do zrobienia w każdej linii. Zróbmy program który wczytuje linie ze standardowego wejścia i dodaje do listy to-do. import System.IO todoitem <- getline appendfile "todo.txt" (todoitem ++ "\n") $ runhaskell appendtodo.hs Iron the dishes $ runhaskell appendtodo.hs Dust the dog $ runhaskell appendtodo.hs Take salad out of the oven $ cat todo.txt Iron the dishes Dust the dog Take salad out of the oven Potrzeba dodać \n na koniec każdej linii ponieważ getline nie daje nam znaku nowej linii na końcu. Mówiliśmy wcześniej o contents <- hgetcontents handle nie wczytuje całego pliku naraz jest leniwe więc robiąc to: 21

withfile "something.txt" ReadMode (\handle -> do contents <- hgetcontents handle putstr contents) Jest jak łączenie strumienia danych z pliku do standardowego wyjścia. Tak jak można myśleć o strumieniach to można myśleć o plikach jako strumieniach. Powyższy program będzie czytać jedną linię, wypisywać na ekranie i tak dalej. Zadacie pytanie, jak szeroki jest strumień, jak często czyta się dane z dysku. Zazwyczaj pliki tekstowe domyślnie są buforowane linia po linii, co oznacza że najmniejsza cześć pliku tekstowego czytana na raz to linia. To jest powodem dlaczego program czyta jedna linię i wyświetla ją na ekranie, po czym czyta następną i tak dalej. Dla plików binarny zazwyczaj buforuje się jeden blok co oznacza ze plik będzie czytany kawałek po kawałku. Wielkość owego kawałka jest ustawiana przez system operacyjny. Można kontrolować jak dużo danych jest buforowanych przy użyciu funkcji hsetbuffering, biorącej uchwyt do pliku i zmienną typu BufferMode, Ta funkcja zwraca akcje I/O która ustawia buforowanie. BufferMode to typ wyliczeniowy z następującymi wartościami: NoBuffering, LineBuffering lub BlockBuffering (Maybe Int). Typ Maybe Int ustawia wielkość wczytywanych oraz zapisywanych kawałków w bajtach. Jeśli ma wartość Nothing, to wtedy system operacyjny ustawia wielkość. NoBuffering znaczy że plik będzie czytany znak po znaku. NoBuffering jest zazwyczaj mało przydatny bo powoduje bardzo dużo operacji dyskowych. Oto poprzedni program ale czytający plik w kawałkach o wielkości 2048 bajtów. withfile "something.txt" ReadMode (\handle -> do hsetbuffering handle $ BlockBuffering (Just 2048) contents <- hgetcontents handle putstr contents) Czytanie plików przy użyciu większych kawałków powoduje minimalizację liczby dostępów do dysku, lub zasobu sieciowego. Można używać hflush funkcji biorącej uchwyt pliku i zwracającej akcję I/O, która opróżni bufor pliku skojarzonego z uchwytem. Kiedy buforujemy każda linię, opróżniamy bufory co każdą linię. Kiedy używamy buforowania blokowego, opróżniamy po przeczytaniu kawałka. Bufor też jest opróżniany po zamknięciu pliku. Kiedy doszliśmy do znaku nowej linii, mechanizm zapisu(odczytu) zatwierdza wszystkie dane. Można użyć hflush do wymuszenia zatwierdzenia danych wczytanych lub zapisanych. Po użyciu hflush dane są dostępne dla innych działających programów w tym samym czasie. Myślcie o czytaniu pliku buforowanego blok po bloku tak: Sedes będzie spuszczał wodę automatycznie po uzbieraniu 4 litrów w zbiorniku. Wiec jeśli zaczniesz wlewać wodę i kiedy nalejesz 4 litry, ta woda automatycznie zostanie spuszczona i dane w tej wodzie właśnie wlanej będą przeczytane. Można spuścić wodę ręcznie przez wciśniecie dźwigni - metafora hflush. Nie jest zbyt dobrą analogia. Mamy już program do dodania nowej rzeczy do listy to-do w todo.txt. Stwórzmy program do usunięcia rzeczy z listy. Użyjemy nowych funkcji z System.Directory i System.IO 22

import System.IO import System.Directory import Data.List handle <- openfile "todo.txt" ReadMode (tempname, temphandle) <- opentempfile "." "temp" contents <- hgetcontents handle let todotasks = lines contents numberedtasks = zipwith (\n line -> show n ++ " - " ++ line) [0..] todotasks putstrln "These are your TO-DO items:" putstr $ unlines numberedtasks putstrln "Which one do you want to delete?" numberstring <- getline let number = read numberstring newtodoitems = delete (todotasks!! number) todotasks hputstr temphandle $ unlines newtodoitems hclose handle hclose temphandle removefile "todo.txt" renamefile tempname "todo.txt" Najpierw otwieramy plik todo.txt w trybie do odczytu i powiązujemy uchwyt pliku z nazwą handle. Następnie używamy funkcji z System.IO opentempfile. Nazwa wyjaśnia wszystko otwiera plik tymczasowy. Jej parametrami są nazwa katalogu i nazwa pliku tymczasowego. Użyliśmy. Jako nazwy katalogu bo. oznacza obecny katalog w większości systemów operacyjnych. Użyliśmy temp jako szablonu nazwy pliku tymczasowego co oznacza ze nazwa pliku tymczasowego będzie maiła nazwę temp plus jakieś losowe znaki. Owa funkcja zwraca akcje I/O tworzącą plik tymczasowy. Wynikiem w tej akcji jest para wartości, nazwa pliku tymczasowego oraz uchwyt do niego. Można by było otworzyć zwykły plik nazwany todo2.txt ale lepiej jest użyć OpenTempFile aby mieć pewność że nie modyfikujemy już istniejącego pliku, Powód nie użycia getcuttentdirectory do dowiedzenia się jaki jest obecny katalog i potem podania go do opentempfile, ale użycie. jest taki że. oznacza obecny katalog w Windowsie i w unix-ie. Potem powiązujemy zawartość todo.txt do zmiennej contents. Później tworzymy ze string-u listę string-ów, każdy będący jedną linią. W wyniku mamy coś takiego : ["Iron the dishes", "Dust the dog", "Take salad out of the oven"]. Używamy funkcji zip z liczbami od 0 do nieskończoności listą i z funkcją biorącą liczbę np. 3 i string np.: hey i zwracającą string 3 hey wiec tworzymy listę nazwaną numberedtasks ["0 - Iron the dishes", "1 - Dust the dog".... Następnie łączymy tą listę przez użycie unlines i wyświetlamy na ekranie. Można by było użyć mapm putstrln numberedtasks Pytamy użytkownika o numer linii którą chcemy usunąć, i czekamy na wpisanie liczby. Powiedzmy że chcemy usunąć linie o numerze 1 czyli Dust the dog, czyli wpisujemy 1.Nazwa numberstring ma wartość 1, a ponieważ chcemy liczbę a nie string to wykonujemy read dostając liczbę 1 i nazywając ją number. 23

Pamiętacie funkcje delete oraz!! z Data.List.!! zwraca element z listy o określonym indeksie a delete usuwa pierwsze wystąpienie elementu w liście i zwraca nową listę bez tego elementu. (todotasks!! number) (number to 1) zwraca Dust the dog. Nazywamy todotask bez Dust the dog nazwą newtodolist i łączymy przy użyciu unlines w jeden string potem zapisujemy do pliku tymczasowego otwartego wcześniej. Poprzedni plik jest niezmieniony a tymczasowy zawiera wszystkie linie poza linią o psie. Potem zamykamy oba pliki, usuwamy oryginalny przy użyciu removefile, który jak widzicie bierze ścieżkę dostępu z nazwą pliku. Po usunięciu, używamy renamefile aby zmienić nazwę tymczasowego pliku na todo.txt. Pamiętajcie że funkcje removefile oraz renamefile nie biorą uchwytu pliku tylko nazwę pliku ze ścieżką jako parametr. I to wszystko. Moglibyśmy napisać ten program przy użyciu mniejszej ilości linii, ale byliśmy ostrożni aby nie nadpisać czegoś i poprosiliśmy system operacyjny o stworzenie pliku tymczasowego. $ runhaskell deletetodo.hs These are your TO-DO items: 0 - Iron the dishes 1 - Dust the dog 2 - Take salad out of the oven Which one do you want to delete? 1 $ cat todo.txt Iron the dishes Take salad out of the oven $ runhaskell deletetodo.hs These are your TO-DO items: 0 - Iron the dishes 1 - Take salad out of the oven Which one do you want to delete? 0 $ cat todo.txt Take salad out of the oven 24