This is an old revision of the document!
Haskell: Introducere
Scopul acestui laborator este de a familiariza studenții cu limbajul Haskell.
Exerciții
1. Scrieți o funcție constantă; indiferent de argument, întoarce 5.
2. Scrieți o implementare în Haskell pentru funcția: f(x,y) = x
(funcția proiecție)
3. Scrieți o funcție, cu tot cu semnătura de tip, care să implementeze AND boolean.
4. Care sunt semnăturile funcțiilor de la exercițiile 1 și 2?
:t
în ghci pentru a afla.
'a' înseamnă 'anything'!
5. Implementați: myIf :: Bool → a → a → a
6. Scrieți o funcție care primește trei întregi și-l întoarce pe cel mai mare. Hint - uneori parantezele sunt folositoare.
7. Care este tipul funcției: f x y z = if x then y else z
f x y z | x == True = y | otherwise = z
8. Rezolvați exercițiul 6 folosind &&
(aflați tipul folosind :t (&&)
)
9. Care este tipul expresiei [1,2,3]
? (:t [1,2,3]
)
E 1:[2,3]
același lucru ca 1:2:3:[]
?
E 1:(2:[])
același lucru ca (1:2):[]
?
10. Rezolvați exercițiul 8 folosind funcția sort
. Pentru a avea acces la ea trebuia să importați modulul Data.List
: import Data.List
.
11. Implementați o funcție care primește o listă și întoarce lista cu elementele în ordine inversă.
12. Implementați o funcție care se uită la antepenultimul element dintr-o listă și întoarce True
dacă este impar (hint: funcția mod
).
[3,4,5,2,3,9] - False [3,4,2,1,4,4] - True
13. Implementați o funcție care calculează suma elementelor dintr-o listă.
14. Implementați o listă care primește o listă de booleene și întoarce False
dacă cel puțin o booleană din listă este False
.
15. Implementați o funcție care primește o listă și întoarce lista fără elementele impare (nu de pe poziții impare, ci elementele să fie impare).
[1,3,4,6,8,3,2,5] - [4,6,8,2] [1,3,5,7] - []
16. Implementați o funcție care primește o listă de booleene și o convertește la o listă de întregi (True
devine 1
, False
devine 0
).
17. Uneori e util să definim funcții auxiliare care nu sunt folosite în altă parte (astfel nu aglomerăm spațiul de nume din program).
f :: [[Integer]] -> [Bool] f [] = [] f l = (g (head l)):(f (tail l)) where g [] = True g l = h (tail l) h [] = True h l = False
Ce face funcția f
?
18. Implementați o funcție care primește o listă de booleene și întoarce numărul de valori True
.
19. Implementați insertion sort.
Introducere
Haskell este un limbaj de programare pur-funcțional. În limbajele imperative, programele sunt secvențe de instrucțiuni pe care calculatorul le execută ținând cont de stare, care se poate modifica în execuție. În limbajele funcționale nu există o stare, iar funcțiile nu au efecte secundare (e.g. nu pot modifica o variabilă globală), garantând că rezultatul depinde doar de argumentele date: aceeași funcție apelată de două ori cu aceleași argumente, întoarce același rezultat. Astfel, demonstrarea corectitudinii unui program este mai facilă.
Platforma Haskell
Pentru început, avem nevoie de un mod de a compila cod. Puteți descărca platforma Haskell de aici (Windows/OS X/Linux) sau puteți instala pachetul “ghc”.
Vom lucra în modul interactiv, care ne permite să apelăm funcții din consolă și să vedem rezultatul lor. Pentru modul interactiv, rulați ghci
.
Pentru informații suplimentare legate de GHCi, vă recomandăm să citiți pagina de wiki despre mediul de lucru în Haskell.
Tipuri de date în Haskell
Haskell este un limbaj tipat static (statically typed) care poate face inferență de tip (type inference). Asta înseamnă că tipul expresiilor este cunoscut la compilare; dacă nu este explicit, compilatorul îl deduce. Deasemenea, Haskell este tare tipat (strongly typed), ceea ce înseamnă că trebuie să existe conversii explicite între diferite tipuri de date.
Tipuri de bază
Haskell are tipuri asemănătoare celor din alte limbaje studiate până acum, ca C, Java etc: Int, Integer (întregi de dimensiune arbitrară), Float, Double, Char (cu apostrofuri, e.g. 'a'), Bool.
Prelude> :t 'a' 'a' :: Char Prelude> :t True True :: Bool Prelude> :t 0 == 1 0 == 1 :: Bool Prelude> :t 42 42 :: Num a => a
Tipul lui 42
nu pare să fie ceva intuitiv, ca Int
. Deocamdată e suficient să menționăm faptul că, în Haskell, constantele numerice pot să se comporte ca diferite tipuri, în funcție de context. 42
poate să fie considerat întreg, în expresii ca 42 + 1
, sau numărul în virgulă mobilă 42.0
în expresii ca 42 * 3.14
.
Liste
Listele sunt structuri de date omogene (i.e. pot conține doar elemente de același tip). O listă este delimitată de paranteze pătrate, cu elementele sale separate prin virgulă:
[1, 2, 3, 4]
Lista vidă este []
.
Tipul unei liste este dat de tipul elementelor sale; o listă de întregi are tipul [Int]
. Putem avea liste de liste de liste de liste … atâta timp cât respectăm condiția de a avea același tip.
[['H', 'a'], ['s'], ['k', 'e', 'l', 'l']] -- corect, are tipul [[Char]] [['N', 'u'], [True, False], ['a', 's', 'a']] -- greșit, conține și elemente de tipul [Bool] și de tipul [Char]
String
care este un alias peste [Char]
.Astfel,
“String”
este doar un mod mai lizibil de a scrie['S', 't', 'r', 'i', 'n', 'g']
, care, la rândul său, este un mod mai lizibil de a scrie'S':'t':'r':'i':'n':'g':[]
Tupluri
Tuplurile sunt structuri de date eterogene (i.e. pot conține elemente de diferite tipuri). Un tuplu este delimitat de paranteze rotunde, cu elementele sale separate prin virgulă:
(1, 'a', True)
Tipul unui tuplu este dat de numărul, ordinea și tipul elementelor sale:
Prelude> :t (True, 'a') (True, 'a') :: (Bool, Char) Prelude> :t ('a', True) ('a', True) :: (Char, Bool) Prelude> :t ("sir", True, False) ("sir", True, False) :: ([Char], Bool, Bool)
Funcții în Haskell
Apelare
În Haskell, funcțiile pot fi prefixate sau infixate. Cele prefix sunt mai comune, au un nume format din caractere alfanumerice și sunt apelate prin numele lor și lista de argumente, toate separate prin spații (fără paranteze sau virgule):
Prelude> max 1 2 2
Funcțiile infixate sunt cele cu nume formate din caractere speciale, de forma operand1 operator operand2
Prelude> 1 + 2 3
Pentru a prefixa o funcție infixată, folosim operatorul
()
. Astfel, următoarele expresii sunt echivalente:
3 * 23 (*) 3 23
Dacă o funcție are două argumente, putem să o infixăm cu operatorul `
(backticks - dacă nu aveți un layout exotic, ar trebui să fie pe tasta de dinainte de 1). Astfel, următoarele două expresii sunt echivalente:
mod 10 3 10 'mod' 3
În loc de ' (apostrof) trebuie folosit simbolul `
(backtick) - pagina de wiki nu permite inserarea `
într-o secțiune de cod.
Definirea unei funcții
Creați, în editorul de text preferat, un fișier cu extensia .hs
în care vom defini prima funcție:
-- intoarce modulul dublului celui mai mare dintre cele doua argumente myFunc x y = abs (2 * max x y)
Puteți testa funcția din ghci:
Prelude> :l first.hs [1 of 1] Compiling Main ( first.hs, interpreted ) Ok, modules loaded: Main. *Main> myFunc 10 11 22 *Main> myFunc (-10) (-11) 20 *Main> myFunc 3.14 2.71 6.28
Prelude>let myFunc x y = abs (2 * max x y)
Observăm că funcția noastră merge și pentru numere întregi și pentru numere în virgulă mobilă. Am precizat mai devreme că orice expresie trebuie să aibă un tip, cunoscut la compilare. Cum noi nu am precizat tipul funcției noastre, compilatorul l-a dedus ca fiind:
Prelude> :t myFunc myFunc :: (Num a, Ord a) => a -> a -> a
Fără a intra în detalii, funcția noastră ia ca argumente două numere de același tip care pot fi ordonate și întoarce un rezultat de același tip. Metoda prin care se realizează această deducție va fi studiată în continuare la PP.
-- intoarce modulul dublului celui mai mare dintre cele doua argumente myFunc :: Int -> Int -> Int myFunc x y = abs (2 * max x y)
Observați că, acum, tipul arătat de :t
este cel specificat și primiți o eroare dacă încercați să pasați ca argumente numere în virgulă mobilă.
Pattern matching
Vom scrie acum o altă funcție prin care ne propunem să calculăm, în mod recursiv, suma tuturor elementelor dintr-o listă. Pentru o listă vidă aceasta este 0, altfel este capul listei plus suma celorlalte elemente.
{- - if este o expresie; cum toate expresiile - trebuie sa intoarca o valoare, ramura else - este necesara - - exista deja o functie "sum" predefinita si - vrem sa evitam ambiguitatea apelului, motiv - pentru care folosim un nume nou. - (e interesat de mentionat ca, in Haskell, - apostroful este un caracter valid in numele - unei functii, i.e. am putea defini o functie - sum'; din pacate, aici, pe wiki, acest lucru - nu este suportat). -} mySum l = if null l then 0 else head l + mySum (tail l)
Deși funcționează, implementarea de mai sus nu este foarte elegantă. Putem să ne folosim de pattern matching (ca la TDA-uri).
mySum2 [] = 0 mySum2 (x:xs) = x + mySum2 xs
La primul pattern match se va evalua expresia, iar următoarele pattern-uri nu vor mai fi verificate.
O excepție de la acest caz este utilizarea guard-urilor care nu conțin otherwise. Mai multe detalii aici.
Dacă lista dată ca argument e vidă, atunci se face match pe primul pattern și rezultatul este 0. Altfel, se trece la pattern-ul următor; aici se fac două legări - capul listei este legat de numele x
, iar restul de numele xs
(parantezele din jurul x:xs
sunt necesare) și este apelată funcția în mod recursiv.
Tail recursion
Pentru a știi unde să întoarcă execuția după apelul unei funcții, un program ține adresele apelanților pe o stivă. Astfel, după ce execuția funcției se termină, programul se întoarce la apelant pentru a continua secvența de instrucțiuni.
În exemplul nostru, apelul mySum2 [1, 2, 3, 4, 5]
se va evalua în felul următor:
mySum2 [1, 2, 3, 4, 5] 1 + mySum2 [2, 3, 4, 5] 1 + (2 + mySum2 [3, 4, 5]) 1 + (2 + (3 + mySum2 [4, 5])) 1 + (2 + (3 + (4 + mySum2 [5]))) 1 + (2 + (3 + (4 + (5 + mySum2 [])))) 1 + (2 + (3 + (4 + (5 + 0)))) 15
Astfel, pentru a aduna capul listei la suma cozii, trebuie mai întâi calculată această sumă, iar apoi să se revină în funcția apelantă.
Putem folosi un acumulator transmis ca parametru la funcția apelată, eliminând nevoia de a mai ține pe stivă informați despre apelant.
Modul în care Haskell asigură tail-recursion o să fie mai clar când vom discuta despre modul de evaluare al funcțiilor. Tot atunci vom vedea și capcanele acestuia.
-- Folosim sintaxa "let ... in" pentru a ascunde functia -- auxiliara folosita si a pastra forma functiei sum (un singur -- argument) mySum3 l = let sumAux [] acc = acc sumAux (x:xs) acc = sumAux xs (x + acc) in sumAux l 0