====== Lab: Wprowadzenie do Haskella ======
Tematyka:
* podstawy GHCi: obsługa, ładowanie modułów, wbudowane funkcje, trace
* podstawy Haskella: arytmetyka, wyrażenia logiczne, definiowanie prostych funkcji
===== GHCi =====
**ghci** jest interaktywnym środowiskiem, które pozwala na wykonywanie wyrażeń Haskella w trybie interaktywnym.\\
**ghc** jest natomiast kompilatorem języka Haskell.
:help wyświetla pomoc wewnątrz GHCi (:h,:?)
:quit wyjście z GHCi
:load [*] ... załaduj moduły/skrypty ... do GHCi (:l)
np. można załadować nasz kod:
$Prelude>:l hello.hs
:reload przeładuj załadowane moduły (pomocne przy zmianie źródeł (:r)
: powtórz ostatnie polecenie GHCi
:browse [] przeglądnij zawartość modułu
:module [+/-] [*] ... zmień kontekst wykonywania poleceń GHCi (efektywnie (:m)
ładuje funkcje z innego modułu do aktualnej przestrzeni nazw np.
$Prelude>:m Data.List
$Prelude Data.List>
:info [ ...] wyświetl informacje o danej nazwie (:i)
:type pokaż typ wyrażenia (:t)
===== Proste wyrażenia =====
W GHCi można wykonywać funkcje Haskella i wyniki są wyświetlane po ich wykonaniu, proszę przetestować:
* arytmetyka:
$Prelude> 13 + 90
$Prelude> 9 - 17
$Prelude> 6 * 5
$Prelude> 9 / 2
* algebra Boola:
$Prelude> True && False
$Prelude> True || False
$Prelude> not False
* porównania:
$Prelude> 2 == 3
$Prelude> 4.5 /= 6.7
$Prelude> "abc" == "abc"
* wywołania funkcji wbudowanych:
$Prelude> min 2 3
$Prelude> succ 8
$Prelude> max 9 7
* W jaki sposób można sprawdzić jakie mamy funkcje do dyspozycji w standardowym module Prelude :?:
* Proszę też sprawdzić jakie typy mają następujące i powyższe wyrażenia
$Prelude> 1
$Prelude> True
$Prelude> 7.2
* priorytety operatorów:
$Prelude> succ 3 + max 8 0 * 2
$Prelude> succ 1/10
$Prelude> succ 1/10 == succ 0.1
* Co zwróciły powyższe wyrażenia?
* Dlaczego ostatnie wyrażenie zwróciło False?
* Każda funkcja przyjmująca dwa argumenty, może zostać wywołana w postaci infixowej, przez zawarcie jej nazwy pomiędzy znakami %%``%%, tak samo dowolny operator może zostać wywołany w postaci prefixowej z użyciem nawiasów %%()%%
$Prelude> 2 `max` 3
$Prelude> (+) 2 3
* W Haskellu " " (spacja) ma najwyższy priorytet i ma znaczenie aplikacji argumentu do funkcji, ponadto pozostałe operatory zdefiniowane w Prelude mają następujące priorytety:
^ Precedence ^ Left associative op. ^ Non-associative op. ^ Right associative op. ^
| 9|''!!''| |''.''|
| 8| | |''%%^%%'', ''%%^^%%'', ''%%**%%''|
| 7|''*'', ''/'', ''`div`'',| | |
|:::|''`mod`'', ''`rem`'', ''`quot`''| | |
| 6|''+'', ''-''| | |
| 5| | |'':'', ''%%++%%''|
| 4| |''%%==%%'', ''/='', ''<'', ''%%<=%%'', ''>'', ''>='',| |
|:::| |''`elem`'', ''`notElem`''| |
| 3| | |''&&''|
| 2| | |''%%||%%''|
| 1|''%%>>%%'', ''%%>>=%%''| | |
W ramach ćwiczenia proszę pododawać nawiasy do poniższych wyrażeń korzystając z informacji do tej pory zdobytych. Nawiasy należy wstawiać tak, żeby znaczenie wyrażenia nie uległo zmianie i każde podwyrażenie było otoczone nawiasem:
$Prelude> True == False && True || False
$Prelude> max 1 20
$Prelude> succ 1/10
$Prelude> 12 + 4 * 7 `div` 4 ^ 3
$Prelude> elem 2 [1,2,3] : []
$Prelude> (+) 2 3 * 4
$Prelude> length [1,2,3] : 1+7:[]
$Prelude> 2:[]++[1,2,3]
$Prelude> True == False == False
===== Listy =====
Listy w Haskellu są podstawowym typem danych złożonych i jednym z najbardziej eksploatowanych przez algorytmy.
* definiowanie listy:
$Prelude> [1,2,3,89]
$Prelude> [1..100]
$Prelude> ['a'..'z']
$Prelude> [1,6..100]
$Prelude> ['a','f'..'z']
$Prelude> [1.0,1.1..5.0] -- uwaga na precyzję, raczej nie używać zmiennoprzecinkowych liczb, można to zrobić lepiej
* operacje na listach:
$Prelude> 1:[] -- wstawianie na początek listy
$Prelude> 1:[1,23,4] -- wstawianie na początek listy
$Prelude> head [1,4,5] -- pierwszy element listy
$Prelude> tail [1,4,5] -- wszystko poza pierwszym
$Prelude> last [1,4,5] -- ostatni element
$Prelude> init [1,4,5] -- wszystko poza ostatnim
$Prelude> length [1,4,5] -- długość listy
$Prelude> take 2 [1,4,5] -- wybierz dwa elementy z listy
$Prelude> elem 4 [1,4,5] -- czy 4 należy do listy
$Prelude> 4 `elem` [1,4,5] -- to samo tylko w postaci infixowej
$Prelude> [1,7,0] ++ [1,4,5] -- złączenie list
Trzeba pamiętać jednak, że implementacja opiera się o listę jednokierunkową, dlatego operacja length ma złożoność obliczeniową O(n) i złączenie list %%++%% musi przeglądnąć całą listę po lewej stronie, żeby znaleźć koniec listy. Najwydajniejsze algorytmy to te, które działają od lewej do prawej na liście.
* Haskell cechuje się leniwym wykonywaniem kodu (ang. lazy evaluation), czyli wykonuje kod dopiero jak jest to niezbędne, w przeciwnym wypadku odkłada obliczenia na później
$Prelude> head [1..] -- pobierz pierwszy element z nieskończonej(!) listy
$Prelude> take 20 ([1,2..78]++[(Debug.Trace.trace "Hej ja" 100)])
pobierz 20 pierwszych elementów z listy, utworzenie drugiej listy nie będzie potrzebne, bo pierwsza lista ma więcej elementów, nie zostanie wypisany komunikat, bo kod się nigdy nie wykona, funkcja **trace** z modułu **Debug.Trace** wypisuje łańcuch znaków podany jako pierwszy argument i zwraca drugi argument
$Prelude> take 79 ([1,2..78]++[(Debug.Trace.trace "Hej ja" 100)]) -- tym razem pierwsza lista nie wystarczyła
* Jak wygenerować dokładnie 17 pierwszych liczb nieparzystych większych od zera :?: a 19, 23 :?: (wykorzystaj leniwą ewaluację)
* Wyrażenia listowe
$Prelude> [2*x+1| x <- [0..10]] --tworzy listę elementów postaci 2*x+1, gdzie x są kolejnymi wartościami z listy [0..10]
$Prelude> [x| x <- [0..21],x `mod` 2 == 1] -- tworzy listę elementów postaci x, gdzie x są kolejnymi wartościami z listy [0..21] i dodatkowo spełniony jest warunek, że x jest nieparzysty
$Prelude> [x| x <- [0..21],odd x] -- jak wyżej tylko z gotową funkcją z Prelude
$Prelude> [if odd x then 'a' else 'b'| x <- [1..5]]
$Prelude> [x+y*z| x <- [1..5], y<-[7..10], z<-[14,17],2*x/=y]
===== Krotki =====
Listy mogą przechowywać tylko elementy tego samego typu, natomiast elementy różnych typów można grupować w krotki (które mogą być np. elementami listy)
* Krotki definiujemy
$Prelude> (2,"String")
$Prelude> (1,'a',9.1,87)
* Proszę sprawdzić typ powższych wyrażeń i np. [1,2,3], [1,0], krotki nie są jednak bardzo elastyczne, jednak istnieje kilka funkcji operujących na nich:
$Prelude> fst (2,"String")
$Prelude> snd (2,"String")
$Prelude> zip ["Maciek","Damian","Wojtek","Anna"] [203009,203017,203002,203081]
$Prelude> zip [1..] ["Intelx86","Intel64","ARM","Sparc32"] -- listy nie muszą być równej długości
$Prelude> fst (unzip [(1,"a"),(2,"b"),(3,"c")])
* Proszę wypisać listę zawierającą krotki reprezentujące poprawne trójkąty o bokach o długości x takich, że 3 int getchar(void) nie jest funkcją w matematycznym sensie, kolejne jej wywołania mogą zwrócić inne wartości mimo wciąż tego samego argumentu. Funkcje mogą być obiektami zwracanymi przez inne funkcje. Właściwie w Haskellu istnieją tylko funkcje jednoargumentowe, a możliwość przyjmowania więcej niż jednego argumentu realizuje się przez zwrócenie dla konkretnego argumentu innej funkcji. Np.
$Prelude> (2 *) 3 --aplikuje 2 do funkcji (*), a następnie do zwróconej funkcji aplikowany jest argument 3
$Prelude> (3 *) -- dostajemy komunikat o błędzie, bo GHCi nie wie jak wyświetlić rezultat, czyli funkcję przyjmującą jeden argument
$Prelude> ((2 *).(+ 3)) 1 -- kolejne funkcje można składać (tak jak w matematycznym sensie składanie funkcji) przy pomocy operatora .
$Prelude> ((take 3).(:[4..9])) 7 -- co robi ta funkcja :?:
Zdefiniujemy teraz własne funkcje. W tym celu proszę utworzyć nowy plik z rozszerzeniem .hs. Proszę w nim utworzyć następującą funkcje:
incAndTriple v = ( v + 1 ) * 3
specialCases 1 = "Hello"
specialCases 2 = " "
specialCases 3 = "World"
specialCases 4 = "!"
specialCases x = "???"
Proszę załadować kod do GHCi, (jak to zrobić:?:) i wykonać np.
$Prelude> incAndTriple 3
$Predlue> map specialCases [1..4]
$Predlue> concat (map specialCases [1..4])
$Predlue> concat (map specialCases [1..6])
**map** to specjalna funkcja wbudowana, która aplikuje jednoargumentową funkcję podaną jako pierwszy argument do każdego elementu listy podanej jako drugi argument i zwraca listę. Można też korzystać z
$Predlue> map (3*) [1..10]
**concat** natomiast spłaszcza listę, **String** w Haskellu to w rzeczywistości synonim listy znaków, dlatego spłaszczenie listy Stringów, daje listę znaków, czyli **String**.
* Proszę zdefiniować własne wersje funkcji:
* **head**,
* **length**,
* **take**,
* **map**,
* **%%++%%**
* PODPOWIEDZI:
* w Haskellu można stosować pattern matching, ale tylko z użyciem konstruktorów typów Haskellowych:
* : jest konstruktorem listy np. getSecondElement (_:x:_) = x
* (,) jest konstruktorem krotki dwuelementowej snd (_,x) = x
* (,,) jest konstruktorem krotki trójelementowej itd. trd (_,_,x) = x
* Nazwa jest konstrukotrem typu Nazwa (z wielkiej litery), konstruktory mogą być sparametryzowane np. getJust (Just x) = x
* _ (podkreślenie) nie interesuje nas dopasowanie tego argumentu
* raz utworzone dane są już niezmienne (ang. immutable, tak jak np. obiekt klasy String w Javie), żeby zwrócić fragment listy, trzeba utworzyć jej odpowiednią kopię.
* rekurencja rozwiązuje większość problemów :!:
* ' (pojedynczy apostrof) jest zwykłym znakiem i może tworzyć w Haskelu ważną nazwę identyfikatora, można np. zdefiniować funkcje length'
* w Haskellu można zdefiniować dowolny operator, przy definicji należy wykorzystać skłądnię prefixową(+++) x y = x+y
===== Debugger =====
Informacje o poziomie debug można sygnalizować wspomnianą już funkcją Debug.Trace.trace, pomocna też będzie funkcja **show**, która pokazuje reprezentację wartości w postaci łańcucha znaków.
Poza tym w GHCi jest wbudowany mechanizm do debuggowania kodu.
:break [] [] ustawia breakpoint w wzynaczonej lokalizacji moduł, linia kodu, kolumna
:break lub na funkcji o nazwie
:step wykonanie następnego kroku po zatrzymaniu się na breakpoincie
:list pokaż kod na którym się zatrzyał debugger
:print [ ...] pokaż wartości zmiennych ...
:continue wznów wykonanie do następnego breakpointu
:trace [] zacznij śledzić wykonanie od tego breakpointu, albo całego wyrażenia
:history pokaż śledzone wywołania
Jako przykład rozważmy algorytm quicksort (który w Haskellu nie najlepiej się sprawuje ze względu na listy):
import Debug.Trace -- importuje wszystkie funkcje z modułu Debug.Trace
quicksort [] = [] -- definiuje zachowanie na pustej liście
quicksort xx@(x:xs) -- @ inforumuje żeby zmienna xx była widoczna jako całość i jako poszczególne części x i xs
| trace ("qs: "++ show x ++ " " ++show xx) False = [] -- zastosowano tu składnie | case = execute | case2 = execute2, ponieważ trace zwróci False ta gałąź się nigdy nie wykona, ale zostanie wypisana wiadomość na stderr
| otherwise = quicksort low ++ x : quicksort high -- otherwise to nic innego jak True, można nawet sprawdzić True == otherwise w GHCi ta gałąź się wykona w pozostałych przypadkach (w tym wypadku zawsze), następuje konkatenacja listy posortowanej quicksortem z listą składającą się z elementu rodzielającego i listy posortowanej quicksortem
where low = [e | e <- xs, e < x] -- w bloku where następują definicje zmiennych lokalnych low to podlista xs z wartościami mniejszymi od strażnika x
high = [e | e <- xs, e >= x] -- natomiast high to lista w z wartościami większymi lub równymi strażnikowi
trace został użyty tylko w celu ukazania leniwego podejścia Haskella i zaprezentowania możliwości śledzenia wartości w kodzie:
proszę przetestować powyższy kod dla wejść:
$Prelude> quicksort [3,56,90,23,1,2,5,7,23,89,67]
$Prelude> take 4 (quicksort [1,8,3,5,7,90,34,21,45,9,17])
Proszę też przetestować kombinację poleceń GHCi:
:break Main 5 31
:trace quicksort [1..20]
:list
:step