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 [*]<module> ... załaduj moduły/skrypty <module>... 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 [<module>] przeglądnij zawartość modułu <module>
:module [+/-] [*]<mod> ... 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 [<name> ...] wyświetl informacje o danej nazwie (:i)
:type <expr> pokaż typ wyrażenia <expr> (:t)
Proste wyrażenia
W GHCi można wykonywać funkcje Haskella i wyniki są wyświetlane po ich wykonaniu, proszę przetestować:
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<x<17
Proszę z tych trójkątów wybrać trójkąty prostokątne
Funkcje
Haskell jest językiem funkcyjnym, czyli zorientowanym na funkcje, ponadto funkcje są pozbawione „efektów ubocznych” i są funkcjami w matematycznym sensie, czyli dla ustalonego parametru funkcja zwaraca zawsze tą samą wartość. Np. w C funkcja wczytająca pojedynczy znak:
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.
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 [<mod>] <l> [<col>] ustawia breakpoint w wzynaczonej lokalizacji <mod> moduł, <l> linia kodu, <col> kolumna
:break <name> lub na funkcji o nazwie <name>
:step wykonanie następnego kroku po zatrzymaniu się na breakpoincie
:list pokaż kod na którym się zatrzyał debugger
:print [<name> ...] pokaż wartości zmiennych <name> ...
:continue wznów wykonanie do następnego breakpointu
:trace [<expr>] zacznij śledzić wykonanie od tego breakpointu, albo całego wyrażenia <expr>
: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