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. ====== Clase de tipuri (Typeclasses) ====== Scopul laboratorului: * introducerea conceptului de clase de tipuri * introducerea conceptului de constrângeri de tip * clarificarea distincției dintre "polimorfism parametric" și "polimorfism ad hoc" * înrolarea tipurilor în clase, în Haskell * definirea propriilor clase în Haskell ===== Polimorfism ===== Polimorfismul constă în ideea de a oferi //o interfață comună// pentru //entități de tipuri diferite//. Interfața poate fi o funcție sau un simbol (amintiți-vă de constantele polimorfice). Comportamentul din spatele acestei interfețe poate să fie același, sau poate varia în funcție de tipul pe care operează. Ne interesează două feluri importante de polimorfism: **polimorfism parametric** și **polimorfism ad hoc**. ==== Polimorfism parametric ==== Pentru polimorfism parametric, există //un singur comportament// pentru toate tipurile. Polimorfismul parametric este util atunci când dorim abstractizarea unui tip, neavând nevoie de operații specifice pe acesta, sau de a-i cunoaște structura. Multe dintre funcțiile întâlnite până acum în Haskell prezintă polimorfism parametric. De exemplu, o implementare posibilă pentru lista ''length'' este următoarea: <code haskell> length :: [a] -> Int length [] = 0 length (_:xs) = 1 + length xs </code> Observați că tipul elementelor din listă este irelevant și nu aplicăm vreo funcție pe aceste elemente (nici nu le legăm la un nume). Tipurile concrete ce pot fi conținute într-o listă sunt abstractizate în semnătura de tip a lui ''length'', prin variabila de tip ''a''. Astfel putem apela funcția ''length'' pe orice tip concret ca: ''[Int]'', ''[Char]'', ''[Float]'', ''[%%[%%Int%%]%%]'' etc., implementarea fiind aceeași. În Java, polimorfismul parametric este realizat prin [[https://en.wikipedia.org/wiki/Generics_in_Java|genericitate]]. ==== Polimorfism ad hoc ==== Pentru polimorfism ad hoc, există //multiple comportamente//. Un comportament concret este selectat pe baza tipurilor pe care se operează. În Java, acest lucru se poate obține prin "overloading": definirea multiplor funcții cu același nume, dar tipuri diferite pentru parametrii: <code java> // File: Main.java public class Main { static void function(int a) { System.out.println("int function!"); } static void function(double a) { System.out.println("double function!"); } public static void main(String[] args) { function(3.0); function(3); } } </code> <code> $ javac Main.java && java Main double function! int function! </code> Compilatorul poate distinge între cele două funcții pe baza tipurilor prezente în semnătura lor și poate alege în mod corect funcția apelată pe baza tipului argumentului din apelul funcției. Această formă de overloading nu este posibilă în Haskell, în mod direct; i.e. astfel de definiție nu va compila, pentru că nu putem avea două funcții distincte cu același nume: <code haskell> function :: Int -> String function _ = "int function!" function :: Double -> String function _ = "double function!" </code> <code> Prelude> :l Main.hs [1 of 1] Compiling Main ( Main.hs, interpreted ) test.hs:4:1: error: Duplicate type signatures for ‘function’ [...] </code> Polimorfismul ad hoc se realizează în Haskell cu ajutorul //claselor de tipuri//. ===== Typeclasses ===== O **clasă de tipuri** (**typeclass**) este o colecție de funcții care specifică un comportament. Un tip de date poate fi //înrolat// într-o clasă, furnizând implementări ale funcțiilor, particularizate pentru acest tip. <note important> În ciuda numelui, clasele Haskell nu corespund claselor din limbaje de programare orientate pe obiecte (e.g. Java, C%%+%%%%+%%). Clasele Haskell //nu conțin date// și //nu pot fi instanțiate//. O comparație mai pertinentă este cu interfețele din Java. </note> Una dintre cele mai comune clase Haskell este ''Eq'': clasa tipurilor ale căror valori pot fi comparate pentru egalitate. Aceasta conține doar două funcții: ''=='' și ''/=''. O posibilă definiție a acesteia (observați sintaxa Haskell): <code haskell> class Eq a where (==) :: a -> a -> Bool (/=) :: a -> a -> Bool x == y = not (x /= y) x /= y = not (x == y) </code> ''a'' este o variabilă de tip cu rol de placeholder pentru tipuri concrete care pot fi înrolate in ''Eq'' și are //acceași semnificație în tot scope-ul clasei//; i.e. ''a''-ul din semnătura funcțiilor se referă la același tip. De exemplu, pentru înrolarea tipului ''Int'' la clasa ''Eq'', funcția ''=='' va avea tipul ''Int -%%>%% Int -%%>%% Bool''. Spre deosebire de interfețele din Java ce oferă doar //declarații// de funcții, clasele de tipuri pot specifica și //definiții//. Observăm că, în mod bizar, definițiile funcțiilor ''=='' și ''/='' depind una de cealaltă. Acesta este un mecanism care ne ușurează munca atunci când vrem să înrolăm un tip acestei clase: este suficient să suprascriem definiția unei singure funcții, cealaltă păstrându-și implementarea default. Vom defini un tip simplu și îl vom înrola în ''Eq'': <code haskell> data Color = Red | Green | Blue instance Eq Color where Red == Red = True Green == Green = True Blue == Blue = True -- observați cum "_" ne salvează de la a adăuga explicit încă 6 cazuri _ == _ = False </code> Am redefinit ''=='', iar definiția lui ''/='' rămâne neschimbată (''x /= y = not (x == y)''); acum putem folosi ambii operatori cu valori de tip ''Color'': <code> *Main> Red == Red True *Main> Red /= Red False *Main> Red == Blue False *Main> Red /= Blue True </code> O altă clasă des întâlnită este ''Show'', care ne oferă o funcție ''show'' pentru a obține o reprezentare sub formă de șir de caractere a unei valori (similar cu ''toString'' din Java): <code haskell> class Show a where show :: a -> String instance Show Color where show Red = "Red" show Green = "Green" show Blue = "Blue" </code> ==== Cuvântul cheie "deriving" ==== Observați că, înrolând tipul ''Color'' în clasele ''Eq'' și ''Show'', definițiile oferit pentru ''=='' și ''show'' sunt simple și evidente (e.g. pentru ''=='', orice constructor e egal cu el însuși și diferit de ceilalți). Astfel de implementări pot fi derivate automat, fără a le explicita; pentru asta, Haskell ne oferă cuvântul cheie ''deriving''. Adăugat la sfârșitul unei definiții de tip, acesta poate fi urmat de una sau mai multe clase (într-un tuplu) în care tipul de date este înrolat cu o implementare simplistă: <code> Prelude> data Color = Red | Green | Blue deriving (Eq, Show) Prelude> Red == Red True Prelude> Red == Green False Prelude> Red Red </code> <note> Mai multe informații despre ce clase pot fi derivate automat și cum se realizează această derivare găsiți aici: [[https://www.haskell.org/onlinereport/decls.html#derived-decls|Haskell Report - 4.3.3 Derived Instances]]\\ [[https://www.haskell.org/onlinereport/derived.html|Haksell Report - 10 Specification of Derived Instances]] </note> ===== Constrângeri de tip ===== Amintiți-vă, din laboratorele trecute, de funcția ''elem'': aceasta primea un element și o listă și întorcea o valoare booleană care ne spunea dacă elementul se găsește în listă; deducem tipul ei ca fiind ''a -%%>%% [a] -%%>%% Bool''. O definiție posibilă a funcției ''elem'' este: <code haskell> elem _ [] = False elem e (x:xs) = e == x || elem e xs </code> Observăm că, pe cel de-al doilea caz, aplicăm funcția ''=='' între elementul căutat ''e'' și capul listei ''x''. Dar funcția ''=='' este parte a clasei ''Eq'', deci valorile primite ca argumente trebuie să aparțină unui tip înrolat în ''Eq''; ceea ce înseamnă că tipul ''a'' din semnătura funcției ''elem'', trebuie să fie înrolat în ''Eq''. Aceasta este o **constrângere de tip** (**type-constraint**) și este exprimată în Haskell prin următoarea construcție sintactică: <code> Prelude> :t elem elem :: (Eq a) => a -> [a] -> Bool </code> ''(Eq a) =%%>%%'' precizează că, în semnătura de tip care urmează, variabila ''a'' nu se poate referi chiar la orice tip, ci doar la cele înrolate în ''Eq''. <note> Toate constrângerile de tip sunt trecute într-un tuplu, urmat de ''=%%>%%'', apoi de tipul funcției: <code> *Main> :t (\x y -> x == 0 && y < 0) (\x y -> x == 0 && y == 0) :: (Eq a, Ord b, Num a, Num b) => a -> b -> Bool </code> </note> Odată înrolat un tip într-o clasă, putem folosi toate funcțiile care au acea clasă printre constrângeri: <code> *Main> :t elem elem :: (Eq a) => a -> [a] -> Bool *Main> elem Red [Blue, Green, Green, Red, Blue] True *Main> :m +Data.List *Main Data.List> :t delete delete :: (Eq a) => a -> [a] -> [a] *Main Data.List> delete Green [Blue, Green, Green, Red, Blue] [Blue, Red, Blue] </code> ===== Înrolarea tipurilor polimorfice ===== Să ne reamintin tipul de listă polimorfică din laboratorul trecut: <code haskell> data List a = Empty | Cons a (List a) </code> Deși am putea folosi mecanismul de ''deriving'', în scop pedagogic vom înrola manual lista noastră în clasa ''Eq''. Țineți minte că ''List'' nu este un tip, ci un //constructor de tip//. Astfel, va trebui să înrolăm ''List a'' în clasa ''Eq''. Când sunt două liste egale? * două liste goale sunt egale între ele * două liste cu cel puțin un element sunt egale dacă atât capetele lor, cât și cozile sunt egale <code haskell> eqLists Empty Empty = True eqLists (Cons a as) (Cons b bs) = (a == b) && (eqLists as bs) </code> În cel de-al doilea caz, utilizăm funcția ''=='' pentru a compara capetele listelor, deci tipul ''eqLists'' trebuie să conțină o constrângere de tip: ''eqLists :: (Eq a) -%%>%% List a -%%>%% List a -%%>%% Bool''. Scopul nostru final este de a înrola ''List a'' în ''Eq'' și a folosi chiar ''=='' în loc de ''eqLists''; pentru a face asta, avem nevoie de //o constrângere de tip la nivel de clasă//: <code haskell> instance (Eq a) => Eq (List a) where Empty == Empty = True (Cons a as) == (Cons b bs) = (a == b) && (as == bs) _ == _ = False </code> Primul rând exprimă ideea că putem egala două liste //de același tip// (''List a''), doar dacă //elementele lor sunt egalabile// (''(Eq a) =%%>%%''). Observați că, în expresia celui de-al doilea caz, funcția ''=='' apare de două ori: o dată pentru a compara elemente, o dată pentru a compara liste. Acesta este un exemplu de **polimorfism ad hoc**. Cele două apeluri de ''=='' pot avea implementări diferite (al doilea este mereu un apel recursiv; primul poate fi, de exemplu, comparație între doi întregi, în cazul ''List Int''). <note> Cuvântul cheie ''deriving'' se descurcă și cu înrolarea de tipuri polimorfice, fiind capabil să genereze această definiție cu tot cu constrângerea ca și elementele să fie parte din ''Eq''. </note> ===== Informații despre clase în ghci ===== Din cadrul ghci, puteți obține informații despre o clasă anume folosind comanda '':info <typeclass>'' ('':i <typeclass>''): <code> Prelude> :info Ord class Eq a => Ord a where compare :: a -> a -> Ordering (<) :: a -> a -> Bool (<=) :: a -> a -> Bool (>) :: a -> a -> Bool (>=) :: a -> a -> Bool max :: a -> a -> a min :: a -> a -> a {-# MINIMAL compare | (<=) #-} -- Defined in ‘GHC.Classes’ instance Ord a => Ord [a] -- Defined in ‘GHC.Classes’ instance Ord Word -- Defined in ‘GHC.Classes’ instance Ord Ordering -- Defined in ‘GHC.Classes’ instance Ord Int -- Defined in ‘GHC.Classes’ ... </code> Putem observa mai multe informații utile: * constrângerea de tip ''Eq a =%%>%%'' arată că un tip de date trebuie să fie membru al clasei ''Eq'' pentru a putea fi membru al clasei ''Ord''. * toate funcțiile oferite de clasa ''Ord'': ''compare'', ''<'', ''%%<%%='' etc. * linia ''{-# MINIMAL compare | (%%<%%= ) #-}'' ne informează că e suficient să implementăm fie funcția ''compare'', fie operatorul ''%%<%%='', pentru a putea utiliza toate funcțiile puse la dispoziție de clasa ''Ord'' (amintiți-vă de clasa ''Eq'' și cum ''/='' rămânea definit în funcție de ''==''). * linia ''-- Defined in ‘GHC.Classes’'' indică locul în care această clasă e definită. O căutare a numelui ne duce [[https://github.com/ghc/ghc/blob/master/libraries/ghc-prim/GHC/Classes.hs#L337|aici]], unde putem observa implementarea clasei, exact așa cum este folosită în ghc. * următoarele linii reprezintă o înșirare a tuturor tipurilor de date despre care ghci știe că sunt înrolate în clasa ''Ord'', precum și unde se găsește această înrolare. E.g. prima linie arată că listele ce conțin elemente ordonabile sunt și ele ordonabile, comportament definit în ''GHC.Classes'': https://github.com/ghc/ghc/blob/master/libraries/ghc-prim/GHC/Classes.hs#L394 ===== Legi pentru clase ===== Pe lângă nume, constrângeri, tipurile și implementările default ale funcțiilor, mai există o caracteristică a claselor de tip: legile respectate de funcții. Având cunoștiințe de matematică, s-ar putea să vă așteptați ca funcția ''=='' să aibă anumite proprietăți (e.g. reflexivitate: pentru orice valoare ''x'', ''x == x'' să fie ''True''). Într-adevăr, documentația claselor vine de obicei și cu un set de proprietăți care ar trebui respectate; [[https://hackage.haskell.org/package/base-4.12.0.0/docs/Data-Eq.html|aici]] puteți observa cele //cinci// legi ale clasei ''Eq''. Din păcate, aceste legi nu pot fi specificate la nivel de limbaj. Amintiți-vă de înrolarea tipului ''Color'' în clasa ''Eq''. Având control total asupra implementării, am putea scrie: <code haskell> instance Eq Color where _ == _ = True _ /= _ = True </code> Astfel, specificăm că oricare două culori sunt și egale între ele și diferite. Deși o definiție contraintuitivă și inutilă, din perspectiva Haskell acest lucru este perfect ok atât din punct de vedere sintactic cât si semantic; **nu există, la nivel de limbaj, un mecanism de a specifica ce reguli trebuiesc respectate și de a asigura respectarea acestora**. Este îndatorirea programtorului de a cunoaște aceste reguli și de a se asigura că sunt respectate de tipul înrolat; similar, când definiți propriile clase, ar trebui să vă gândiți ce legi ar trebui respectate și să le documentați. Scopul legilor este de a asigura comportamente intuitive, ferite de surprize. <note> Respectarea strictă a tuturor legilor unei clase are excepții. De exemplu, pentru operațiile în virgulă mobilă, o valoare specială [[https://en.wikipedia.org/wiki/NaN|NaN (Not a Number)]] modelează rezultatul anumitor expresii cu rezultat nedefinit. Două valori ''NaN'' nu sunt considerate egale; prin urmare, pentru tipuri ca ''Float'' și ''Double'', operația ''=='' nu respectă proprietatea de reflexivitate pe întreg domeniul: <code> Prelude> a = 0/0 :: Float Prelude> a == a False </code> </note> ===== Exerciții ===== 1. Înrolați tipul ''List a'' în clasa ''Show'', astfel încât șirul rezultat să fie identic cu cel pentru listele Haskell: <code> *Main> Cons 1 (Cons 2 (Cons 3 Empty)) [1,2,3] </code> 2. În [[pp:l04|laboratorul anterior]], ați definit operații (''foldrT'', ''mapT'' etc.) pentru lucrul cu arbori binari polimorfici. Am vrea să înrolăm acest tip la clasele corespunzătoare. a. înrolați ''BTree a'' în ''Eq'' b. înrolați ''BTree'' în ''Foldable'' și definiți ''foldr'' cu aceeași definiție ca ''foldrT'' din laboratorul trecut; veți avea apoi acces la funcții ca ''foldl'', ''sum'', ''minimum'' ('':info Foldable'' în ''ghci'' pentru a le vedea pe toate) c. înrolați ''BTree'' în ''Functor'' și definiți ''fmap'' cu aceeași definiție ca ''mapT'' din laboratorul trecut (mai multe informații [[https://wiki.haskell.org/Functor|aici]]) <note important> Atenție la tipurile ''foldr'' și ''fmap'': <code> Prelude> :t foldr foldr :: Foldable t => (a -> b -> b) -> b -> t a -> b Prelude> :t fmap fmap :: Functor f => (a -> b) -> f a -> f b </code> Observați că constrângerile sunt puse pe //constructorul de tip//, deci va trebui înrolat ''BTree'', nu ''BTree a''. </note> 3. Tot în [[pp:l04|laboratorul anterior]], ați definit un tip pentru a modela numerele naturale extinse cu un punct la infinit, precum și niște operații pe acestea. Dorim să facem implementarea elegantă, pentru a putea folosi operatori deja existenți (e.g. ''=='' pentru comparare) și pentru a putea folosi alte funcții existente care impun constrângeri de tip (e.g. ''Data.List.sort :: Ord a =%%>%% [a] -%%>%% [a]''). Înrolați tipul ''Extended'' în clasele: a. ''Show'' (a.î. să afișăm numerele fără vreun nume de constructor, iar infinitul ca "Inf") b. ''Eq'' c. ''Ord'' d. ''Num'' 4. Definiți un tip de date polimorfic care să modeleze următoarea gramatică (în care o valoare poate avea orice tip): <code> <expr> ::= <value> | <variable> | <expr> + <expr> | <expr> * <expr> </code> 5. Amintiți-vă de tabelele asociative din laboratorul trecut. Vom defini un "dicționar" ca fiind o tabelă asociativă cu șiruri de caractere drept chei: <code haskell> type Dictionary a = [(String, a)] </code> Ne mai interesează și funcția care primește o cheie și o tabelă asociativă și întoarce valoarea asociată cheii, împachetată într-un ''Maybe'': <code haskell> getValue :: String -> Dictionary a -> Maybe a </code> Fie următoarea clasă: <code haskell> class Eval t a where eval :: Dictionary a -> t a -> Maybe a </code> Spre deosebire de clasele prezentate în exemplele anterioare, care desemnează o //proprietate// a unui tip sau constructor de tip, ''Eval'' stabilește o **relație** între un constructor de tip ''t'' și un tip ''a''. Relația spune că orice container de tip ''t a'' poate fi evaluat in prezența unui dicționar cu valori de tip ''a'', la o valoare de tip ''Maybe a''. <note> Acest tip de clasă reprezintă o extensie a limbajului, **Multi-parameter type-class**. Cel mai probabil este nevoie de următoarele directive //la începutul fișierului// pentru a o defini și pentru a înrola tipuri la ea (directivele se găsesc deja în scheletul de laborator): <code haskell> {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-} class Eval t a where eval :: Dictionary a -> t a -> Maybe a </code> Mai multe despre acestea, precum și despre **Functional Dependencies**, un alt feature care este strâns legat de clasele de tipuri cu mai mulți parametrii puteți găsi [[https://en.wikibooks.org/wiki/Haskell/Advanced_type_classes|aici]]. </note> a. Înrolați ''Expr'' și ''Integer'' în clasa ''Eval''. Care este semnificația evaluării?\\ b. înrolați ''Expr'' și ''Extended'' în clasa ''Eval''.\\ c. Ce observați? Cum ați putea simplifica? {{:pp:lab5_-_schelet.zip|Lab 5 - Schelet}}\\ ===== Recommended Reading ===== * [[http://learnyouahaskell.com/types-and-typeclasses#typeclasses-101|Learn you a Haskell for Great Good - Chapter 2: Types and Typeclasses#Typeclasess 101]]\\ * [[http://learnyouahaskell.com/making-our-own-types-and-typeclasses#typeclasses-102|Learn you a Haskell for Great Good - Chapter 8: Making Our Own Types and Typeclasses#Typeclasess 102]]\\ * [[http://book.realworldhaskell.org/read/using-typeclasses.html|Real World Haskell - Chapter 6: Using Typeclasses]]\\ * [[https://www.haskell.org/tutorial/stdclasses.html|A Gentle Introduction to Haskell - Chapter 8: Standard Haskell Classes]]