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. Scrieți o funcție ce primește un număr $ x$ și o listă de numere de forma $ [a_1, a_2, \ldots a_n]$ ca parametrii și calculează $ x - a_1 - a_2 - \ldots - a_n$.

2. Inversați o listă folosind foldl.

3. Definiți o închidere funcțională care prefixează [1,2,3] la o listă primită ca parametru.

4. Scrieți o închidere funcțională care elimină caracterul 'a' dintr-un string primit ca parametru.

  • Scrieți o implementare ce utilizează o funcție de tip fold
  • Scrieți o alta care folosește funcția filter

5. Implementați o funcție ce primește ca parametru o lista de string-uri și returnează concatenarea lor folosind fold.

6. Se dau două cuvinte de aceeași lungime. Să se numere câte caractere sunt diferite în cele doua șiruri.

7. Definiți o funcție de ordin superior care primește o funcție și un număr, și aplică de două ori funcția pe numărul respectiv.

8. Definiți o funcție care primește un operator binar, și întoarce același operator în care ordinea parametrilor a fost inversată (e.g. 1/3 → 3/1)

9. Implementați și testați funcțiile:

  • foldl
  • foldr
  • map
  • filter
  • zipWith
  • compunerea de funcții (operatorul . din Haskell)
Dacă nu cunoașteți vreuna dintre funcții, o puteți căuta pe Hoogle pentru a vedea tipul și scopul ei.

10. Implementați, folosind foldl sau foldr:

  • map
  • filter

11. Implementați o funcție ce primește 2 parametri: un șir de cifre de forma “111222111333112” și un număr K. Această funcție trebuie să facă următorii pași de K ori:

  • se grupează cifrele identice în felul următor “111222111333112” → [“111”, “222”, “111”, “333”, “11”, “2”]
  • pentru fiecare element din lista rezultată se generează un element de forma (numărul de apariții al cifrei din șir, cifra din șir) ex: [“111”, “222”, “111”, “333”, “11”, “2”] → [(“3”, “1”), (“3”, “2”), (“3”, “1”), (“3”, “3”), (“2”, “1”), (“1”, “2”)]
  • se face flatten pe lista rezultată la pasul anterior ex: [(“3”, “1”), (“3”, “2”), (“3”, “1”), (“3”, “3”), (“2”, “1”), (“1”, “2”)] → “313231332112”
Pentru mai multe informații despre secvență: https://en.wikipedia.org/wiki/Look-and-say_sequence

Laborator 2 - Schelet