Edit this page Backlinks This page is read only. You can view the source, but not change it. Ask your administrator if you think this is wrong. ====== Tipuri în Haksell ====== Scopul laboratorului: * introducerea conceptului de Tipuri de Date Algebrice * introducerea conceptului de Tipuri de Date Abstracte * clarificarea distincției dintre acestea * introducerea conceptului de tipuri de date monomorfice/polimorfice * definirea și lucrul cu propriile tipuri de date în Haskell ===== Tipuri de Date Algebrice ===== [[https://wiki.haskell.org/Algebraic_data_type|Tipurile de date algebrice]] sunt tipurile pentru care putem specifica //forma// elementelor. Acestea pot avea //unul sau mai mulți// constructori, iar constructorii pot avea //zero sau mai multe// câmpuri. În Haskell, definirea unui tip se face folosind cuvântul cheie ''data'' urmat de numele tipului, semnul egal, apoi constructorii: <code haskell> -- Tipul de date "Color" are doi constructori posibili; bara "|" indică -- faptul că o culoare poate fi ori "White", ori "Black" (similar cu tipul "Bool"). data Color = White | Black -- Constructorul "Point" este practic doar un tuplu cu nume; un (Float, Float) mai specific. data Point = Point Float Float </code> <note> Observați că, în cel de-al doilea caz, tipul de date și constructorul au același nume, "Point". Acest lucru e perfect ok, deoarece reprezintă concepte diferite. </note> Desigur, putem combina aceste două concepte (multiplii constructori, multiple câmpuri ale unui singur constructor): <code haskell> -- O formă este fie un cerc definit de centru și rază, fie un -- dreptunghi definit de două colțuri opuse. data Shape = Circle Point Float | Rectangle Point Point </code> <note warning> Numele tipurilor de date și numele constructorilor trebuie scrise cu literă mare. </note> ==== Pattern matching ==== Aduceți-vă aminte de mecanismul de "pattern matching" din Haskell (pe care până acum l-ați folosit predominant pentru liste). Acestea functionează pe //constructori//, deci îl putem folosi pentru tipurile definite de noi: * putem avea definiții diferite pentru constructori diferiți * putem asocia valori constituente cu nume, pe care să le folosim în expresia funcției <code haskell> colorToString :: Color -> String colorToString White = "White" colorToString Black = "Black" pointsDistance :: Point -> Point -> Float pointsDistance (Point x1 y1) (Point x2 y2) = sqrt ((x1 - x2) ** 2 + (y1 - y2) ** 2) area :: Shape -> Float area (Circle _ r) = pi * (r ** 2) area (Rectangle (Point x1 y1) (Point x2 y2)) = (y2 - y1) * (x2 - x1) </code> ===== Tipuri de Date Abstracte ===== [[https://wiki.haskell.org/Abstract_data_type|Tipurile de date abstracte]] sunt tipurile care au asociată o //interfață//, dar nu și o //implementare//. De exemplu, o listă poate fi văzută ca o structură care suportă anumite operații: ''isEmpty'', ''length'', ''head'' etc. Utilizatorul listei poate fi preocupat doar de comportamentul acestei interfețe (i.e. colecția de funcții), nu și de detalii de implementare. De exemplu, o listă poate fi reprezentată ca o zonă contiguă de memorie ([[https://en.wikipedia.org/wiki/Dynamic_array|Array List]]), sau ca o colecție de celule care conțin o valoare și adresa următoarei celule ([[https://en.wikipedia.org/wiki/Linked_list|Linked List]]). Unul din avantajele acestei separări - între comportament și implementare - este că dezvoltatorul unei biblioteci poate modifica implementarea unei structuri, lăsând interfața neschimbată; ceea ce nu necesită modificări în codul client. Conceptele de "tip de date abstract" și "tip de date algebric" sunt ortogonale, ele vizând trăsături diferite. ===== Tipuri polimorfice ===== Vrem să definim propria noastră listă care poate conține numere întregi: <code haskell> data List = Empty | Cons Int List </code> Acesta este un tip de date //monomorfic//, deoarece operează cu un singur tip, anume ''Int''. Foarte multe operații pe listă nu sunt preocupate de tipul sau de valorile elementelor: verificarea dacă lista este goală, calculul lungimii listei, inversarea listei etc. Redefinirea listei, precum și a tuturor acestor operații pentru fiecare tip pentru care am avea nevoie de o listă, ar necesita multă muncă. Soluția este să definim un tip de date //polimorfic//, parametrizat cu tipul elementelor pe care le conține: <code haskell> data List a = Empty | Cons a (List a) </code> Simbolul ''a'' reprezintă o //variabilă de tip//. O variabilă de tip trebuie reprezentată de un nume care începe cu literă mică. Prezența variabilelor de tip reprezintă o formă de //abstracție//; ele pot fi înlocuite de un tip propriu-zis pentru a obține un tip concret, e.g. ''List Int'', ''List Char'', ''List (List Char)'' etc. Putem astfel să scriem funcții polimorfice care operează pe lista noastră și care nu sunt preocupate de tipul conținutului: <code haskell> myLength :: List a -> Int myLength Empty = 0 myLength (Cons _ t) = 1 + myLength t myHead :: List a -> a myHead (Cons a _) = a </code> Observați că, înlocuind ''List a'' cu ''[a]'', semnăturile acestor funcții se potrivesc cu cele ale ''length'' și ''head'' din ''Prelude''. <note> Ce este ''List''? În definirea unei liste polimorfice, am scris un nou tip generic ''List a''. Dar ce anume reprezintă pentru Haskell ''List''? ''List'' este un **constructor de tip**, nu un tip de date. Constructorii de tip primesc ca parametrii //tipuri// și întorc un tip (sau un alt constructor de tip dacă nu primesc suficienți parametrii). Asta înseamnă că ''List'' nu este un //tip//, dar ''List Int'', ''List Char'' etc. sunt tipuri. </note> ==== Constante polimorfice ==== Care este tipul listei vide? <code> Prelude> :t [] [] :: [a] </code> Observăm că tipul listei vide este general. Asta înseamnă că lista vidă poate fi tratată ca o listă de orice tip. <code> Prelude> [1,2,3] ++ "123" -- eroare, tipuri diferite Prelude> [1,2,3] ++ [] [1,2,3] Prelude> "123" ++ [] "123" </code> Spunem că ''[]'' este o **constantă polimorfică**. <note> O altă constantă polimorfică este, de exemplu ''1''. De aceea putem scrie: <code> Prelude> 1 + 2 3 Prelude> 1 + 2.71 3.71 </code> Dacă forțăm 1 să fie întreg, vom primi o eroare la a doua adunare: <code> Prelude> (1 :: Int) + 2.71 <interactive>:60:14: No instance for (Fractional Int) arising from the literal '2.71' In the second argument of '(+)', namely '2.71' In the expression: (1 :: Int) + 2.71 In an equation for 'it': it = (1 :: Int) + 2.71 </code> ''1'' are însă o constrângere despre care vom discuta în laboratorul următor. </note> ===== Exemple ===== ==== Maybe ==== ''Maybe'' este un tip //polimorfic// care modelează prezența unei anumite valori, sau absența acesteia: <code haskell> data Maybe a = Nothing | Just a </code> Probabil ați scris și folosit funcții care ar trebui să se descurce cu anumite cazuri în care nicio valoare din codomeniu nu se potrivește ca rezultat. Să considerăm, de exemplu, o funcție care calculează suma primului și al ultimului element dintr-o listă de numere întregi. Dacă tipul acesteia ar fi: <code haskell> sumExtremities :: [Int] -> Int </code> ce am putea întoarce pentru lista vidă? O idee ar fi să întoarcem o valoare default (e.g. ''0''); sau o valoare ca ''-1'' și să impunem ca lista primită să conțină doar numere pozitive. O soluție mai elegantă este să îmbogățim domeniul valorilor returnate cu o valoare specială, care semnalează că nu putem calcula această sumă: <code haskell> sumExtremities :: [Int] -> Maybe Int sumExtremities [] = Nothing sumExtremities [_] = Nothing sumExtremities l = Just (head l + last l) </code> <note> Pentru o listă cu un singur element, ar putea avea sens să-l considerăm și primul și ultimul, deci să returnăm dublul lui. Abordarea noastră e doar o convenție. </note> Definim o funcție care întoarce un șir descriptiv al sumei calculate: <code haskell> describeSum :: [Int] -> String describeSum l = case sumExtremities l of Nothing -> "Can't calculate sum" Just x -> "Sum is " ++ show x </code> Putem vedea cum funcționează, din ''ghci'': <code> *Main> describeSum [] "Can't calculate sum" *Main> describeSum [8] "Can't calculate sum" *Main> describeSum [8, 12, 35] "Sum is 43" *Main> </code> Concepte similare se regăsesc și în alte limbaje de programare moderne: * C%%+%%%%+%%17 - [[https://en.cppreference.com/w/cpp/utility/optional|std::optional]] * Java8 - [[https://docs.oracle.com/javase/8/docs/api/java/util/Optional.html|Optional]] * Swift - [[https://developer.apple.com/documentation/swift/optional|Optional]] * Scala - [[https://www.scala-lang.org/api/current/scala/Option.html|Option]] * Rust - [[https://doc.rust-lang.org/std/option/index.html|std::option]] ==== Either ==== Un TDA similar este ''Either'', care modelează valori cu două tipuri posibile: <code haskell> data Either a b = Left a | Right b </code> Prin convenție, constructorul ''Left'' reprezintă o eroare, iar constructorul ''Right'' o valoare corectă. Astfel putem extinde mecanisme de error-handling pentru a conține mai multe informații despre ce a cauzat eroarea (spre deosebire de ''Maybe'', care doar indică **apariția** unei erori): Adaptăm cele două funcții de mai sus: <code haskell> sumExtremities :: [Int] -> Either String Int sumExtremities [] = Left "Can't sum on empty list" sumExtremities [_] = Left "Can't sum on singleton list" sumExtremities l = Right (head l + last l) describeSum :: [Int] -> String describeSum l = case sumExtremities l of Left e -> e Right x -> "Sum is " ++ show x </code> <code> *Main> describeSum [] "Can't sum on empty list" *Main> describeSum [8] "Can't sum on singleton list" *Main> describeSum [8, 12, 35] "Sum is 43" *Main> </code> Concepte similare se regăsesc și în alte limbaje de programare moderne: * C%%+%%%%+%%17 - [[https://en.cppreference.com/w/cpp/utility/variant|std::variant]] * Scala - [[https://www.scala-lang.org/api/2.9.3/scala/Either.html|Either]] ===== Înregistrări ===== Vrem să definim un tip de date pentru a descrie un student, folosind următoarele câmpuri: nume, prenume, an de studiu, medie. <code haskell> -- tipul de date și constructorul pot avea același nume, -- pentru că reprezintă concepte diferite. data Student = Student String String Int Float </code> Observăm că semnficația câmpurilor nu este evidentă din definiție. Dacă am vrea să putem extrage câmpurile unei valori de tip ''Student''? Definim funcțiile: <code haskell> nume :: Student -> String nume (Student n _ _ _) = n prenume :: Student -> String prenume (Student _ p _ _) = p an :: Student -> Int an (Student _ _ a _) = a medie :: Student -> Float medie (Student _ _ _ m) = m </code> Astfel de cod (necesar, neinteresant, laborios) poartă denumirea de [[https://en.wikipedia.org/wiki/Boilerplate_code|"boilerplate"]]. Adesea, limbajele de nivel înalt oferă construcții sintactice pentru a evita scrierea "de mână". În Haskell, putem scrie: <code haskell> data Student = Student { nume :: String , prenume :: String , an :: Int , medie :: Float } </code> Avem astfel aceeași funcționalitate în mult mai puține linii. Există și alte avantaje ale acestei sintaxe, precum și [[https://wiki.haskell.org/Name_clashes_in_record_fields|capcane]]. Citiți mai multe [[https://en.wikibooks.org/wiki/Haskell/More_on_datatypes#Named_Fields_(Record_Syntax)|aici]]. ===== Exerciții ===== ==== 1. Arbori binari ==== Fie următorul tip care modelează arbori binari polimorfici: <code haskell> data BTree a = Nil | Node a (BTree a) (Btree a) </code> a. definiți o funcție ''foldrT'', echivalentul lui ''foldr'' pentru arbori\\ b. definiți o funcție ''mapT'' echivalentul lui ''map'' pentru arbori\\ c. definiți o funcție ''zipWithT'', echivalentul lui ''zipWith'' pentru arbori <note> Orientați-vă după tipurile funcțiilor prezente în schelet. </note> ==== 2. Tabele asociative ==== O [[https://en.wikipedia.org/wiki/Associative_array|tabelă asociativă]] este o colecție de perechi (cheie, valoare). O cheie prezentă în tabelă are asociată o anumită valoare și putem folosi tabela pentru a accesa valori pe baza unei chei. Vom reprezenta o tabelă asociativă ca o listă de perechi, folosind cuvântul cheie ''type'' pentru a defini un [[https://wiki.haskell.org/Keywords#type|alias]]: <code haskell> type Assoc k v = [(k, v)] </code> a. definiți o funcție care primește o cheie, o valoare, o tabelă asociativă și întoarce o nouă tabelă care conține noua asociere (fie adăugând-o, fie modificând-o)\\ b. definiți o funcție care primește o cheie și o tabelă asociativă și întoarce valoarea asociată cheii, împachetată într-un ''Maybe''\\ c. definiți o funcție care primește o cheie și o tabelă asociativă și întoarce tabela fără cheia dată\\ d. definiți o funcție care primește o tabelă asociativă și întoarce o listă de chei ==== 3. Numere naturale extinse ==== Fie mulțimea de numere naturale extinse cu un punct la infinit ($ \hat{\mathbb{N}} = \mathbb{N} \cup \{ \infty \}$). <code haskell> data Extended = Infinity | Value Integer </code> a. definiți o funcție ''extSum'' care să evalueze suma a două numere naturale extinse\\ b. definiți o funcție ''extDiv'' care să evalueze raportul a două numere naturale extinse (împărțirea la 0 și la infinit are rezultat definit)\\ c. definiți o funcție ''extLess'' care spune dacă un număr natural extins e mai mic decât un altul {{:pp:lab4_-_schelet.zip|Laborator 4 - Schelet}}\\ ===== Recommended Reading ===== * [[http://learnyouahaskell.com/types-and-typeclasses|Learn you a Haskell for Great Good - Chapter 2: Types and Typeclasses]] * [[http://learnyouahaskell.com/making-our-own-types-and-typeclasses|Learn you a Haskell for Great Good - Chapter 8: Making Our Own Types and Typeclasses]] * [[http://book.realworldhaskell.org/read/defining-types-streamlining-functions.html|Real World Haskell - Chapter 3: Defining Types, Streamlining Functions]] * [[http://book.realworldhaskell.org/read/types-and-functions.html|Real World Haskell - Chapter 4: Types and Functions]]