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ć:

  • 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<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.

  • 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 [<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]
:line
:step
pl/dydaktyka/pp/haskell/lab-intro.txt · ostatnio zmienione: 2018/05/08 08:20 przez kkutt
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0