Scopul laboratorului:
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.
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:
length :: [a] -> Int length [] = 0 length (_:xs) = 1 + length xs
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 genericitate.
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:
// 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); } }
$ javac Main.java && java Main double function! int function!
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:
function :: Int -> String function _ = "int function!" function :: Double -> String function _ = "double function!"
Prelude> :l Main.hs [1 of 1] Compiling Main ( Main.hs, interpreted ) test.hs:4:1: error: Duplicate type signatures for ‘function’ [...]
Polimorfismul ad hoc se realizează în Haskell cu ajutorul claselor de tipuri.
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.
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):
class Eq a where (==) :: a -> a -> Bool (/=) :: a -> a -> Bool x == y = not (x /= y) x /= y = not (x == y)
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
:
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
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
:
*Main> Red == Red True *Main> Red /= Red False *Main> Red == Blue False *Main> Red /= Blue True
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):
class Show a where show :: a -> String instance Show Color where show Red = "Red" show Green = "Green" show Blue = "Blue"
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ă:
Prelude> data Color = Red | Green | Blue deriving (Eq, Show) Prelude> Red == Red True Prelude> Red == Green False Prelude> Red Red
Haskell Report - 4.3.3 Derived Instances
Haksell Report - 10 Specification of Derived Instances
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:
elem _ [] = False elem e (x:xs) = e == x || elem e xs
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ă:
Prelude> :t elem elem :: (Eq a) => a -> [a] -> Bool
(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
.
=>
, apoi de tipul funcției:*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
Odată înrolat un tip într-o clasă, putem folosi toate funcțiile care au acea clasă printre constrângeri:
*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]
Să ne reamintin tipul de listă polimorfică din laboratorul trecut:
data List a = Empty | Cons a (List a)
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?
eqLists Empty Empty = True eqLists (Cons a as) (Cons b bs) = (a == b) && (eqLists as bs)
Î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ă:
instance (Eq a) => Eq (List a) where Empty == Empty = True (Cons a as) == (Cons b bs) = (a == b) && (as == bs) _ == _ = False
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
).
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
.
Din cadrul ghci, puteți obține informații despre o clasă anume folosind comanda :info <typeclass>
(:i <typeclass>
):
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’ ...
Putem observa mai multe informații utile:
Eq a =>
arată că un tip de date trebuie să fie membru al clasei Eq
pentru a putea fi membru al clasei Ord
.Ord
: compare
, <
, <=
etc.{-# 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 ==
).– Defined in ‘GHC.Classes’
indică locul în care această clasă e definită. O căutare a numelui ne duce aici, unde putem observa implementarea clasei, exact așa cum este folosită în ghc.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
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; 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:
instance Eq Color where _ == _ = True _ /= _ = True
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.
NaN
nu sunt considerate egale; prin urmare, pentru tipuri ca Float
și Double
, operația ==
nu respectă proprietatea de reflexivitate pe întreg domeniul:Prelude> a = 0/0 :: Float Prelude> a == a False
1. Înrolați tipul List a
în clasa Show
, astfel încât șirul rezultat să fie identic cu cel pentru listele Haskell:
*Main> Cons 1 (Cons 2 (Cons 3 Empty)) [1,2,3]
2. În 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 aici)
foldr
și fmap
: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
Observați că constrângerile sunt puse pe constructorul de tip, deci va trebui înrolat BTree
, nu BTree a
.
3. Tot în 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):
<expr> ::= <value> | <variable> | <expr> + <expr> | <expr> * <expr>
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:
type Dictionary a = [(String, a)]
Ne mai interesează și funcția care primește o cheie și o tabelă asociativă și întoarce valoarea asociată cheii, împachetată într-un Maybe
:
getValue :: String -> Dictionary a -> Maybe a
Fie următoarea clasă:
class Eval t a where eval :: Dictionary a -> t a -> Maybe a
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
.
{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-} class Eval t a where eval :: Dictionary a -> t a -> Maybe a
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 aici.
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?