This is an old revision of the document!


Funcții

Scopul laboratorului:

  • Semnificația termenului aplicație
  • Definirea funcțiilor anonime în Haskell
  • Curry vs uncurry
  • Închideri funcționale

Funcțiile de ordin superior sunt funcții care lucrează cu alte funcții: le primesc ca parametrii sau le returnează.

Pentru a înțelege importanța lor, vom da următorul exemplu: ne propunem să scriem două funcții care primesc o listă și returnează, într-o nouă listă

  • toate elementele pare
  • toate elementele mai mari decât 10
evenElements [] = []
evenElements (x:xs) = if even x
                          then x : evenElements xs
                          else evenElements xs
 
greaterThan10 [] = []
greaterThan10 (x:xs) = if x > 10
                           then x : greaterThan10 xs
                           else greaterThan10 xs
În loc de if, puteți folosi următoarea sintaxă:
myFunction x
        | x < 10    = "One digit"
        | x < 100   = "Two digits"
        | x < 1000  = "Three digits"
        | otherwise = "More than four digits"

Testăm funcțiile scrise:

*Main> evenElements [1..20]
[2,4,6,8,10,12,14,16,18,20]
*Main> greaterThan10 [1..20]
[10,11,12,13,14,15,16,17,18,19,20]

Observăm că funcțiile definite mai sus sunt foarte asemănătoare. De fapt, doar condiția verificată în if diferă. Scriem, deci, o funcție generală care primește o funcție pentru testarea elementelor:

-- In primul pattern, nu folosim functia de testare, deci nu ne intereseaza ca aceasta
-- sa fie legata la un nume, lucru marcat prin "_"
myFilter _ [] = []
myFilter test (x:xs) = if test x
                           then x : myFilter test xs
                           else myFilter test xs

Acum putem rescrie funcțiile noastre, într-un mod mai elegant, utilizând funcția de filtrare:

evenElements = myFilter even
 
greaterThan10 = myFilter (> 10)

Currying (numit după tizul Haskell-ului) este procesul prin care, dintr-o funcție care ia mai multe argumente, se obține o secvență de funcții care iau un singur argument.

De exemplu, dintr-o funcție de două argumente f : X × Y → Z se obține o funcție
curry(f) : X → (Y → Z). Noua funcție curry(f) primește un argument de tipul X și întoarce o funcție de tipul Y → Z (adică o funcție care primește un argument de tipul Y și întoarce un rezultat de tip Z). Considerând operatorul asociativ la dreapta, putem omite parantezele, i.e. curry(f) : X → Y → Z.

Operația inversă se numește “uncurry”: f : X → Y → Z, uncurry(f): X × Y → Z.

Întorcându-ne la funcția de filtrare definită mai devreme, care este tipul ei?

*Main> :t myFilter
myFilter :: (a -> Bool) -> [a] -> [a]

Și în laboratorul trecut, am observat că nu există o separare între domeniu și codomeniu, de genul
(a -> Bool) x [a] -> [a].

Acest lucru se datorează faptului că, în Haskell, toate funcțiile iau un singur argument. Alternativ, putem spune despre ele că sunt curried.

Tipul funcției noastre trebuie interpretat astfel:
primește ca argument o funcție de tipul a -> Bool (ia un argument și întoarce o booleană) și întoarce o funcție de tipul [a] -> [a] (ia o listă și întoarce o listă de același tip).

De aceea, în exemplul de mai sus am putut definit evenElements (o funcție care ia o listă și returnează o listă) ca fiind myFilter even care are exact acest tip, i.e. [a] -> [a].

Haskell pune la dispoziție funcțiile curry și uncurry.
*Main> :t curry
curry :: ((a, b) -> c) -> a -> b -> c
*Main> :t uncurry
uncurry :: (a -> b -> c) -> (a, b) -> c

Funcțiile primite, respectiv returnate de curry și uncurry iau tot un singur argument, numai că acesta este un tuplu.

*Main> let evenElements = (uncurry myFilter) even

<interactive>:12:39:
    Couldn't match expected type '(a0 -> Bool, [a0])'
                with actual type 'a1 -> Bool'
    In the second argument of 'uncurry', namely 'even'
    In the expression: (uncurry myFilter) even
    In an equation for 'evenElements':
        evenElements = (uncurry myFilter) even
*Main> let evenElements l = (uncurry myFilter) (even, l)
*Main>

Funcțiile le putem apela și cu un număr mai mic de argumente. În acest caz, funcția apelată a fost parțial aplicată și va rezulta o nouă funcție cu un număr mai mic de parametrii.

f a b = a - b

Funcția f primește 2 parametrii și întoarce diferența dintre ei. Dacă apelăm f cu un singur argument se întoarce o altă funcție care se așteaptă să primească un singur argument.

g = f 3 -- se crează o altă funcție care primește un singur parametru (g b = 3 - b)
g 4 -- se apeleaza funcția g
-1 -- rezultatul 3 - 4

Ne propunem să scriem o funcție care primește o listă și întoarce toate elementele ei divizibile cu 5. Având deja o funcție de filtrare, putem scrie:

testDiv5 x = mod x 5 == 0
multiplesOf5 = myFilter testDiv5

Această abordare funcționează, însă am poluat spațiul de nume cu o funcție pe care nu o folosim decât o singură dată - testDiv5.

Funcțiile anonime (cunoscute și ca expresii lambda) sunt funcții fără nume, folosite des în lucrul cu funcții de ordin superior.

În Haskell, se folosește sintaxa: \x -> x + 5. Funcția definită ia un parametru și returnează suma dintre acesta și 5. Caracterul \ este folosit pentru că seamănă cu λ (lambda).

Rescriind funcția noastră, obținem:

multiplesOf5 = myFilter (\x -> mod x 5 == 0)

Următoarele expresii sunt echivalente:

f x y = x + y
f x = \y -> x + y
f = \x y -> x + y
Nu există vreo diferență între \x y -> x + y și \x -> \y -> x + y.

Exemple de alte limbaje unde se pot folosi funcții anonime:

  • Python
print((lambda x, y: x + y)(3, 4))
  • C++ - începând cu C++11
std::cout << [](int a, int b){return a + b;}(3, 4) << std::endl;
  • JavaScript
print(function(a,b){return a+b}(3, 4))

Închiderile funcționale (closures) se folosesc de variabile libere în definiția lor. Cu alte cuvinte, sunt funcții care, pe lângă definiție, conțin și un environment de care se folosesc; acest environment conține variabilele libere despre care vorbeam. Denumirea provine din faptul că environment-ul nu este menționat explicit, el este determinat implicit; spunem că funcția se închide peste variabilele (libere) a, b, etc.

flip f = \a -> \b -> f b a
      -- ^^^^^^^^^^^^+^^^^ 
      --     f este o variabilă liberă în contextul funcției anonime de după egal

Ce ne returnează funcția flip, atunci când îi dăm un singur argument? O închidere funcțională peste f. Prin urmare, funcția mod_flipped de mai jos este o închidere funcțională care atunci când primește 2 parametrii va folosi funcția mod (stocată - într-un mod neobservabil – în contextul său) și va returna restul împărțirii celui de-al doilea la primul.

mod_flipped = flip mod

Alte exemple de închideri funcționale:

mod3 x = mod x 3 -- închidere funcțională peste funcția mod din Prelude și constanta 3
plus5 x = x + 5  -- închidere funcțională peste funcția (+) și constanta 5
(+5)             -- aceeași ca mai sus;
                 --     (+) este o funcție care primește 2 argumente și se evaluează la suma lor
                 --     (+5) este închiderea funcțională rezultată în urma "hardcodării" unuia dintre argumente

Hoogle este un motor de căutare pentru funcții Haskell, o unealtă foarte utilă pentru dezvoltatori, mai ales la început. Vă recomandăm să citiți această pagină.

1. Definiti operatorul $.

2. Care este tipul operatorului de compunere al functiilor? Definiti-l printr-o functie myComp.

3. Consideram urmatoarele functii lambda care definesc f x y = x + y:

f x = \y -> x + y
f = \x -> \y -> x + y
f = \x y -> x + y
  • Care este tipul lui f?
  • Care este tipul lui f 5?

4. Putem defini un set ca fiind o functie de tipul s :: Integer → Bool, s x returneaza true daca x se afla in setul s.

  • Scrieti functia s
  • Definiti setul f = {1,2,3} (tot ca o functie)
  • Scrieti o functie n pentru multimea numerelor naturale
  • Scrieti, in 2 moduri, intersectia a 2 seturi.
  • (hard) Scrieti o functie care primeste o lista de seturi si calculeaza intersectia acestora

5. Definiti o functie myFilter care primeste o functie g :: Integer → Bool, o lista de numere intregi, si returneaza o lista cu numerele pentru care g returneaza True.

6. Folositi functia myFilter definita anterior, scrieti o functie care pastreaza numerele pozitive dintr-o lista de numere intregi

7. Implementati functia map. Folosind functia map, scrieti o functie care primeste o lista de Bool, si returneaza o lista cu 1 in loc de True si 0 in loc de False.

8. Folosind map si/sau filter, scrieti o functie care sa implementeze comportamentul:

f "321CB" [("321CB", ["Matei", "Andrei", "Mihai"]), ("322CB",["George, Matei"])] = ["Matei", "Mihai"]

9. Folosind map si/sau filter, scrieti o functie care sa implementeze comportamentul:

f [("Dan Matei Popovici",9),("Mihai",4),("Andrei Alex",6)] = [(["Dan", "Matei", "Popovici"],10),(["Andrei,Alex"],6)]

10. Folosind map, scrieti o functie care primeste o lista de liste si returneaza lista obtinuta prin concatenarea lor intr-o singura lista.

11. Rescrieti functia de la (10) folosind recursivitatea pe coada. Care sunt diferentele?

12. Implementati foldr si foldl.

13. Implementati concatenarea folosind fold.

14. Implementati functia reverse folosind fold.

15. (hard) Folosind foldr sau foldl, scrieti o functie care primeste o lista de seturi si calculeaza intersectia acestora