Funkcije
Pojam funkcije u Haskel jeziku ima izvesne sličnosti sa pojmom funkcije u imperativnim jezicima poput C-a ili Jave, ali izvesne razlike. Dok se u navedenim jezicima funkcija shvata kao niz instrukcija koje je potrebno izvršiti, u Haskelu funkcija predstavlja pravilnost po kojoj se od neke vrednost dobija druga vrednost. Drugim rečima u Haskelu funkcije predstavljaju transformacije vrednosti. Ovo shvatanje se podudara sa matematičkom definicijom funkcije.
Takođe, dok je u imperativnim jezicima pojam funkcije razdvojen od pojma podatka (te je teško ili nemoguće vršiti sa funkcijama ono što je moguće vršiti sa podacima), u Haskelu je granica između ova dva pojma mnogo tanja. U Haskelu, svaka funkcija je samo jedna vrednost koja je sadržana u odgovarajućem tipu. I kao što vrednosti tipa Int
mogu transformisati sa odgovarajućim funkcijama, ili vrednosti tipa Bool
sa logičkim operacijama i funkcijama, tako se i funkcije mogu transformisati sa odgovarajućim funkcijama...
Pojam funkcije i tip funkcije
U matematici, funkcija je pravilnost po kojoj se svakom elementu nekog skupa pridružuju elementi nekog drugog skupa. Činjenicu da neka funkcija \(f\) elementima skupa \(A\) pridružuje elemente skupa \(B\) označavamo sa \(f\colon A \to B\) i kažemo da \(f\) ima (poseduje) tip \(A \to B\). Skup \(A\) nazivamo domen a skup \(B\) kodomen funkcije \(f\)1. Ako funkcija \(f\) elementu \(x\in A\) pridružuje vrednost \(y\in B\) to označavamo sa \(f(x) = y\).
Za funkciju tipa \(A \to B\) često kažemo da preslikava (slika, transformiše) skup \(A\) u skup \(B\). Za izraz \(f(x)\) kažemo da predstavlja primenu funkcije \(f\) na vrednost \(x\).
Dve funkcije \(f_1\) i \(f_2\) smatramo za jednake ako imaju isti domen, isti kodomen, i ako je \(f_1(x) = f_2(x)\) za svako \(x\) iz domena funkcije.
Neka je \(\mathbb N = \{0, 1, 2, \dots\}\) skup prirodnih brojeva. Funkcija \(s\colon \mathbb N \to \mathbb N\) definisana sa izrazom \(s(x)=x + 1\) svakom prirodnom broju pridružuje naredni prirodni broj tj. važi \(s(0)=1\), \(s(1)=2\), \(s(2)=3\) itd... Domen i kodomen ove funkcije je skup prirodnih brojeva \(\mathbb N\).
Funkciju \(s\) možemo da shvatimo kao transformaciju: \(s\) transformiše broj \(0\) u broj \(1\), broj \(1\) u broj \(2\), itd...
Funkcija \(c \colon \mathbb N \to \mathbb N\) definisana sa \(c(x) = 1\) svakom prirodnom broju pridružuje isti broj - broj \(1\). Za funkciju \(c\) kažemo da je konstantna funkcija jer je njena vrednost konstantna bez obzira na vrednost \(x\).
Zapravo, za svaki prirodan broj \(n\) može se konstruisati funkcija tipa \(\mathbb N \to \mathbb N\) koja sve prirodne brojeve preslikava u \(n\).
Neka je \(P\) skup svih tačaka jedne ravni, a \(T\) skup svih duži te ravni. Definišimo funkciju \(s\) koja svakoj duži iz \(T\) pridružuje središte te duži. Domen funkcije \(s\) je \(T\), kodomen je \(P\), a tip je \(T \to P\).
Primetimo da ovu funkciju nismo zadali sa formulom, već sa opisom. Ali jasno je da se radi o pravilnosti kojom se svakoj duži pridružuje tačka.
Neka je \(X\) proizvoljan skup. Tada možemo definisati funkciju \(i \colon X\to X\) sa \(i(x) = x\). Funkcija \(i\) preslikava svaku vrednost u samu sebe, tj. svakom elementu \(x\) pridružuje njega samog. Funkciju \(i\) nazivamo identička funkcija na skupu \(X\).
U Haskelu, analogno matematici, funkcije predstavljaju pravilnosti po kojima se vrednostima nekog tipa (domena) pridružuju vrednosti drugog tipa (kodomena). Tip funkcije koja vrednostima tipa A
pridružuje vrednosti tipa B
označava se sa A -> B
. Tip funkcije, kao i svaki drugi tip, predstavlja kolekciju nekih vrednosti. Tip A -> B
predstavlja kolekciju svih funkcija koje tip A
preslikavaju tip B
.
Funkcija not
svakoj logičkoj vrednosti dodeljuje njenu negaciju koja je takođe logička vrednost. Prema tome, funkcija not
ima tip Bool -> Bool
.
Funkcija even
, predefinisana u GHCi-ju, svakom celom broju dodeljuje logičku vrednost u zavisnosti da li je taj broj paran. Prema tome, tip funkcije even
je2 Int -> Bool
. Isto važi i za funkciju odd
koja proverava da li je broj neparan.
Za razliku od imperativnih jezika, funkcije u Haskelu uvek moraju da poseduju kodomen. Drugim rečima, primena funkcije na vrednost mora i sama da predstavlja vrednost nekog tipa. U imperativnim jezicima neretko koristimo void
funkcije koje nemaju povratnu vrednost. Takve funkcije se ne koriste u Haskelu.
Zadatak 1. Neka je \(B\) skup koji sadrži dve vrednosti. Koliko postoji funkcija tipa \(B \to B\)? Koje su to funkcije?
Zadatak 2. Neka je \(U\) skup koji sadrži jednu vrednost. Koliko postoji funkcija tipa \(\mathbb N \to U\)? Koliko postoji funkcija tipa \(U \to \mathbb N\)?
Lambda izrazi
Haskel je zasnovan na matematičkom formalizmu zvanom lambda račun. Lambda račun je formalni sistem koji opisuje pojam izračunljivosti pomoću definicija i primene funkcija. Ovde neće biti prezentovan lambda račun, ali mnoge stvari koje slede važe i za sam lambda račun.
U Haskelu, funkcije su vrednosti koje se definišu pravilom apstrakcije3. U Haskelu sintaksa za apstrakciju je \x -> v
. Lambda apstrakcija se sastoji od znaka4 \
, zatim imena parametra (u ovom primeru to je x
ali može biti bilo koja validna labela), zatim strelice ->
nakon koje sledi povratna vrednost funkcije odnosno neki izraz koji potencijalno zavisi od parametra (tj. od x
). Izraz \x -> v
nazivamo lambda funkcija a izraz koji sledi nakon strelice (tj. v
), nazivamo telo lambda funkcije.
Funkcija koja vraća dvostruki argument se definiše pravilom apstrakcije kao:
\x -> (2 * x)
Matematičkom notacijom zapisano, navedeni izraz predstavlja funkciju \(g(x) = 2 x\). Ime parametra je u potpunosti proizvoljno uzeto. Ista funkcija se može definisati i kao \y -> (2 * y)
ili kao \broj -> (2 * broj)
, itd... Telo prve navedene lambda funkcije je (2 * x)
. Primetimo da navedenim kodom nismo definisali i ime funkcije5. Da bismo mogli ovu funkciju da primenjujemo na vrednosti, dodelićemo je nekom imenu:
Nakon učitavanja navedenog koda u GHCi, možemo koristiti funkciju g
i primenjivati6 je na neke vrednosti. Da bi funkciju primenili na vrednost potrebno je tu vrednost navesti nakon imena funkcije, razdvajajući ih razmakom:
Svaki izraz oblika m n
, gde su m
i n
neki izrazi, nazivamo aplikacija7. U Haskelu, svaka aplikacija predstavlja pokušaj primene izraza m
na izraz n
. Međutim, m n
nema smisla ako m
nije funkcija. Na primer, "primena" 'a' 'b'
daje grešku
U prethodnom primeru, kada smo u GHCi prompt uneli g 10
i pritisnuli Enter
, GHCi je zamenio ime g
sa odgovarajućim lambda izrazom. Tom zamenom se dobija izraz (\x -> (2 * x)) 10
. Štaviše, i sami smo mogli da unesemo izraz u prompt i dobili bismo isti rezultat:
Izraz oblika (\x -> a) b
, gde su a
i b
proizvoljni lambda izrazi, naziva se redeks. Redeksi zapravo predstavljaju smislene primene funkcije na vrednost.
Primer redeksa je upravo (\x -> (2 * x)) 10
ali i g 10
jer je g
po definiciji (\x -> (2 * x))
. Za razliku od izraza 'a' 'b'
, svaki redeks predstavlja smislenu aplikaciju funkcije na vrednost.
Zadatak 3. Napisati lambda izraz koji za dati broj \(r\) daje površinu kruga čiji je poluprečnik r
. Dodeliti ovaj izraz imenu površinaKruga
. Izračunati površinu krugova čiji su poluprečnici \(2\) i \(5\). Konstanta \pi
je dostupna pod imenom pi
.
Površina kruga sa poluprečnikom \(r\) je \(\pi r^2\), te stoga možemo definisati traženi lambda izraz sa \r -> (pi * r^2)
. Ovaj izraz možemo imenovati, odnosno dodeliti nekom imenu, i sačuvati u .hs
datoteci:
površinaKruga = (\r -> (2 * r^2))
Nakon učitavanja datoteke u ghci, možemo naći površine različitih krugova:
ghci> površinaKruga 2 12.566370614359172 ghci> površinaKruga 5 78.53981633974483
Zadatak 4. Temperatura od \(x\) stepeni Farenhajta je jednaka temperaturi od \(\frac{5}{9}(x - 32)\) stepeni Celzijusa. Napisati funkciju uCelzijuse
koja stepene Farenhajta pretvara u stepne Celzijusa. Izraziti i stepene Farenhajta preko stepena Celzijusa, i napisati funkciju uFarenhajte
koja vrši konverziju. Potvrditi sa obe funkcije da je \(30^\circ \mathrm C\) jednako \(86^\circ \mathrm F\)
Beta redukcija
Ako je data matematička funkcija \(f(x)=2*x\), tada se postupak nalaženja vrednosti \(f(10)\) sastoji u tome da se svako pojavljivanje promenljive \(x\) u izrazu \(2*x\) zameni sa argumentom \(10\). Time se dobija izraz \(2*10\) koji može direktno da se izračuna. Analognim postupkom redeksi se mogu svesti na neku vrednost koja u sebi ne sadrži redeks.
Da bi evaluirao redeks (\x -> (2 * x)) 10
, GHCi u telu lambda funkcije zamenjuje x
sa 10
, čime se dobija 2 * 10
. Izraz 2 * 10
se zatim sračunava na nivou hardvera.
Opisani postupak nazivamo beta redukcija. Tačnije beta redukcija je zamena redeksa (\x -> a) b
sa izrazom koji se dobije kada se izrazu a
sva pojavljivanja imena x
zamene sa izrazom b
. Pritom se početak lambda izraza, \x ->
, uklanja.
Neka je dat redeks (\x -> (((\y -> (y + 1)) x) * x)) 10
. Ovaj redeks je zanimljiv, zato što se u telu lambda funkcije nalazi druga lambda funkcija. Beta redukcijom vrednost 10
zamenjujemo umesto x
u telu "veće" lambda funkcije (((\y -> (y + 1)) 10) * 10)
U rezultatu beta redukcije imamo redeks (\y -> (y+1)) 10
koji takođe možemo da redukujemo, pri čemu ostatak izraza ostaje nepromenjen: ((((10 + 1))) * 10)
. Višestruke zagrade ne igraju ulogu, te smo došli do izraza ((10 + 1) * 10)
koji se direktno računa do vrednosti 110
.
Postavlja se pitanje, da li je poredak evaluacije redeksa važan? Da li bismo isti rezultat dobili i da smo prvo izvršili beta redukciju unutrašnjeg redeksa a zatim i spoljašnjeg?
Nastavak prethodnog primera. Beta redukcijom unutrašnjeg redeksa izraza u (\x -> (((\y -> (y + 1)) x) * x)) 10
dobijamo izraz (\x -> ((x + 1) * x)) 10
. Vršenjem još jedne beta redukcije dobijamo isti izraz kao malopre ((10 + 1) * 10)
. Kao što vidimo, poredak vršenja lambda redukcije nije uticao na krajnju vrednost!
Osobina da poredak vršenja beta redukcije ne utiče na krajnu vrednost je poznata kao konfluentnost. Neće za svaki izraz važiti konfluentnost, a i u slučajevima kada važi, različit poredak beta redukcije može zahtevat drastično različit broj koraka (pa samim tim i vremena potrebnog kompletnu beta redukciju). Više o tome ćemo reći kasnije...
Tip funkcije
Do sada smo smo objasnili kako se definišu funkcije, ali nismo naveli mnogo o tipu funkcije. Razlog tome je što je Haskel automatski zaključivao tip definisanih funkcija. Ipak, kao i sa drugim vrednostima, programer može (i poželjno je) da eksplicitno navede tip funkcije.
Tip funkcije g
iz prethodne sekcije možemo eksplicitno da navedemo u kodu:
Nešto složeniji primer bi bila funkcija koja uzima listu tipa [Int]
, i vraća prvi član te liste uvećan za jedan:
Zanimljivo je primetiti šta se dešava sa tipom funkcije kada se funkcija primeni na vrednost. Pre svega, Haskel će proveriti da li se tip te vrednosti i domen funkcije podudaraju. Ako to nije slučaj, tada će doći do tipske greške.
Ako primenimo funkciju h :: [Int] -> Int
na broj, dobićemo grešku:
ghci> h 2 <interactive>:3:3: error: • No instance for (Num [Int]) arising from the literal ‘2’ • In the first argument of ‘h’, namely ‘2’ In the expression: h 2 In an equation for ‘it’: it = h 2
Naravno, razlog ove greške je taj što se funkcija h
"očekuje" vrednost tipa [Int]
a ne Int
. Domen funkcije h
je [Int]
, i stoga se ona može primenjivati samo na vrednosti koje poseduju ovaj tip.
Ako nije došlo do tipske greške, tada primena funkcija na neku vrednost predstavlja dobro tipiziran izraz. Pritom, ako je funkcija f
tipa A -> B
, a vrednost v
tipa A
, tada je f v
predstavlja vrednost tipa B
. Beta redukcijom moguće je odrediti tačnu vrednost izraza f v
, ali i bez redukcije znamo da ta vrednost poseduje tip B
. Prosto rečeno, primenom funkcije tipa A -> B
na vrednosti tipa A
"brišemo" A ->
iz tipa8.
Zadatak 5. Deklarisati tipove funkcija iz prethodnih zadataka ove lekcije.
Funkcije više promenljivih
Do sada je prikazano kako se mogu definisati funkcije samo sa jednim parametrom, ali su u prethodnoj lekciji korišćene funkcije sa dva parametara (kao što je funkcija mod
koja određuje ostatak pri deljenju ili funkcija elem
koja ispituje da li se element nalazi u listi). Postavlja se pitanje kako se ovakve funkcije definišu?
Pažljivi čitalac koji se seća sadržaja prethodne lekcije mogao je sam da konstruiše funkcije sa dva parametara. Naime, u prethodnoj sekciji smo videli kako se uređeni parovi mogu konstruisati. Prema tome, ako želimo funkciji da prosledimo dve vrednosti, dovoljno je da napišemo funkciju koja uzima jedan uređen par9.
ghci> ljubav ("Ana", "Milovana") "Ana voli Milovana!" ghci> ljubav ("Marijana", "Marijana") "Marijana voli Marijana!"
Zadatak 6. Napisati funkciju zapreminaValjka :: (Double, Double) -> Double
koja na osnovu uređenog para visine i poluprečnika baze računa zapreminu valjka.
Pozivanje ovako definisane funkcije podseća na pozivanje funkcija u imperativnim jezicima. Ali primetimo da smo mi u prethodnoj lekciji funkcije poput mod
pozivali sa mod 7 3
a ne sa mod (7, 3)
. Takođe možemo da primetimo da iako funkciji prosleđujemo dva podatka, funkcija ljubav
suštinski uzima samo jednu vrednost tipa (String, String)
.
Lambda račun, koji je osmišljen pola veka pre Haskela, je zamišljen kao minimalan formalni sistem i kao takav takav ne poseduje pojam uređene n
-torke. Umesto toga, funkcije sa više parametara u lambda računu se realizuju uz pomoć 'trika'. Naime, da bi dobili funkciju od više parametara, definisaćemo funkciju f
od jednog parametra čija povratna vrednost je druga funkcija. Na taj način, izraz (f x)
predstavlja novu funkciju koju možemo da primenimo na neku drugu vrednost. Ako je rezultat i ove primene funkcija, tada taj rezultat možemo da primenimo na sledeću vrednost i tako dalje.
Funkcija koja uzima dve vrednosti tipa Double
i vraća njihovu aritmetičku sredinu može se definisati sa: aritm = (\x -> (\y -> ((x+y)/2)))
.
U GHCi okruženju možemo da se uverimo da funkcija ispravno računa aritmetičku sredinu:
ghci> (aritm 8) 10 9
Zašto je funkcija pozvana na ovaj način? Pogledajmo samu definicije funkcije aritm
. Izraz aritm 8
predstavlja jedan redeks. Beta redukcijom ovaj redeks se svodi na (\y -> ((8+y)/2))
. Ali ovaj rezultat je ponovo jedna funkcija, stoga je možemo primeniti na vrednost 10
. Aplikacija (\y -> ((8+y)/2)) 10
se beta-redukuje u izraz (8 + 10)/2
koji se zatim sračunava u vrednost 9
.
Ipak, aplikacija (aritm 8) 10
ne izgleda baš u potpunosti isto kao aplikacija mod 7 2
koju smo videli u prethodnoj lekciji. Razlog tome je što u Haskelu aplikacija (primena funkcije na argument) je levo asocijativna operacija što znači da se izraz (f x) y
može jednostavnije zapisati kao f x y
. I zaista, ako se vratimo na prethodni primer, možemo se uveriti u to:
ghci> aritm 8 10 9 ghci> aritm 20 100 60
Kog tipa je funkcija aritm
? Za trenutak pretpostavimo da su parametri x
i y
tipa Double
10. Funkcija aritm
uzima vrednost tipa Double
i vraća neku funkciju, nazovimo je g
. Funkcija g
uzima i vraća vrednost tipa Double
te je njen tip Double -> Double
. Sada možemo da zaključimo da funkcija aritm
poseduje tip Double -> (Double -> Double)
jer uzima vrednost tipa Double
i vraća vrednost tipa Double -> Double
.
Koristeći opisan postupak, možemo konstruisati funkcije koje poseduju tri ili više parametara11. Tom prilikom konstruišemo funkcije tipa T₁ -> (T₂ -> ( … -> (Tₙ -> B)))
gde tipovi T₁
, ..., Tₙ
predstavljaju tipove parametara funkcije, a B
povratnu vrednost takve funkcije. Primenom funkcije f
tipa T₁ -> (T₂ -> ( … -> (Tₙ -> B)))
na vrednost tipa T₁
dobija se funkcija tipa T₂ -> (T₃ -> … -> (Tₙ -> B))
. Novodobijena funkcija može se primeniti na vrednost tipa T₂
čime se dobija funkcija tipa T₃ -> … -> (Tₙ -> B)
. Postupak se može ponavljati sve dok se ne stigne do vrednosti tipa B
.
Funkcija koja "uzima" ili "vraća" neku drugu funkciju naziva se funkcija višeg reda. Mnogi programski jezici nemaju podršku za funkcije višeg reda, ali kao što vidimo, u Haskelu je ovaj pojam je sasvim prirodan.
Zadatak 7. Napisati funkciju zapremina :: Double -> (Double -> (Double -> Double))
koja na osnovu tri argumenta računa zapreminu kvadra čije su dužine stranica zadate tim argumentima.
zapremina :: Double -> (Double -> (Double -> Double)) zapremina = (\a -> (\b -> (\c -> (a * b * c))))
Zadatak 8. Napisati funkciju zapreminaValjka :: Double -> (Double -> Double)
koja na osnovu prosleđene visine i poluprečnika baze računa zapreminu valjka.
Prelude
Prelid12 je kratka muzička forma improvizacijskog karaktera, koja se koristi kao uvod u veća muzička dela. U Haskelu, Prelude predstavlja kôd koji je dostupan svakom programu. Definicije iz Prelida, mogu se shvatiti kao uvod u definicije koje je korisnik napisao.
Sve predefinisane funkcije koje smo do sada predstavili, poput not
, mod
, div
, length
, head
, fst
, i tako dalje, zapravo su definisane u Haskel Prelidu. Takođe, konstante poput pi
su definisane u Prelidu. U narednim lekcijama videćemo kako su tačno navedene funkcija definisane, i upoznaćemo još mnoge druge definicije iz Prelida.
Zadaci
Zadatak 9. Rešenja \(x_1\) i \(x_2\) kvadratne jednačine \(ax^2+bx+c=0\) su data sa kvadratnom formulom \[x_{1,2} = \frac{-b\pm\sqrt{b^2-4ac}}{2a}.\] Implementirati funkciju rešenja :: Double -> (Double -> (Double -> (Double, Double)))
koja za tri prosleđena argumenta \(a\), \(b\) i \(c\) vraća uređeni par rešenja koja su dobijena putem navedene formule. Pretpostaviti da su koeficijenti tako zadati da su sve računske operacije definisane (u realnom domenu).
Zadatak 10. Napisati funkciju primeni :: (Int -> Bool) -> ((Int, Int) -> (Bool, Bool))
koja uzima jednu funkciju f :: Int -> Bool
, jedan uređen par tipa (Int, Int)
, i vraća uređen par koji se dobija primenom f
na svaku koordinatu uređenog para.
Kada se primeni
primeni na neku funkciju f
, potrebno je da dobijemo lambda funkciju koja uzima par i vraća par dobijen primenom f
na svaku od koordinata. Ova lambda funkcija se može zapisati kao \par -> (f (fst par), f (snd par))
. Jedino što je potrebno je da uvedemo i f
kao jedan parametar, tj. da napravimo apstrakciju po f
. Time dobijamo:
primeni :: (Int -> Bool) -> ((Int, Int) -> (Bool, Bool)) primeni = (\f -> (\par -> (f (fst par), f (snd par))))
Da bi testirali našu funkciju, definisaćemo i funkciju paran
paran :: Int -> Bool
paran = (\n -> (0 == mod n 2))
Nakon učitavanja u GHCi, uveravamo se da funkcija zaista radi:
ghci> primeni paran (2, 7) (True,False)
Za naredne zadatke neophodno je podsetiti se funkcija za rad sa listama iz prethodne lekcije: length
, head
, last
, init
, tail
, reverse
, elem
.
Zadatak 11. Napisati funkciju inicijali :: [Char] -> ([Char] -> [Char])
koja od dve niske koje predstavljaju ime i prezime pravi inicijale. Na primer inicijali "Nikola" "Tesla"
daje "N.T."
. Pretpostaviti da su obe niske neprazne.
Korišćenjem head
funkcije, "uzećemo" prva slova imena i prezimena. Primena funkcije head
na nisku nam daje vrednost tipa Char
. Stoga dobijenu vrednost moramo postaviti u novu nisku, da bismo mogli koristiti operator ++
koji nadovezuje niske.
inicijali :: [Char] -> ([Char] -> [Char]) inicijali = (\x1 -> (\x2 -> ( let y1 = head x1; y2 = head x2 in [x1] ++ "." ++ [x2] ++ "." )))
Zadatak 12. Napisati funkciju ukloni3 :: [Char] -> [Char]
koja uklanja prva tri elementa iz niske. Pretpostaviti da će funkcija uvek biti primenjena na niske dužine veće od tri.
Funkcija tail
nam vraća rep niske (sve osim prvog elementa). Primenom ove funkcije tri puta na neku listu, efektivno uklanjamo prva tri elementa iz liste
ukloni3 :: [Char] -> [Char] ukloni3 = (\x -> (tail (tail (tail x))))
Zadatak 13. Za nisku kažemo da je palindrom ako se čita isto i sa levo i sa desno, npr. "anavolimilovana"
jeste palindrom. Napisati funkciju palindrom :: [Char] -> Bool
koja proverava da li je niska palindrom.
Niske, odnosno vrednosti tipa [Char]
, pripadaju klasi Eq
. Zbog toga je moguće koristiti operator ==
za poređenje dve niske. Niska s
je palindrom ako je jednaka nisci reverse s
. Ovo zapažanje nas direktno dovodi do rešenja:
palindrom :: [Char] -> Bool palindrom = (\s -> (s == reverse s))
Zadatak 14. Sa tipom (Double, Double)
moguće je predstaviti vektor dvodimenzionalne ravni. Napisati funkciju
zbir :: (Double, Double) -> ((Double, Double) -> (Double, Double))
koja sabira vektore.
zbir :: (Double, Double) -> ((Double, Double) -> (Double, Double)) zbir = (\u -> (\v -> (fst u + fst v, snd u + snd v)))