nav_dugme codeBlog codeBlog
  • početna Početna stranica
  • Sačuvani članci Sačuvani članci
  • Članci
     (spisak)
  • Kontakt
Povratak na vrh stranice

Info & povezani članci Info o članku - dugme

Info

trejler_sat Datum objave: 10.06.2021.

trejler_dokument Jezici: JavaScript,
Python,
C#,
Java

trejler_teg_narandzasti Težina: 7/10

JavaScript
Python
C#
Java
es6
nizovi
mapiranje
lambda funkcije
arrow funkcije
teorija

Povezani članci

Šablonske niske u programskim jezicimaOperacije sa nizovima u programskom jeziku JavaScriptAsinhrono programiranje u JavaScriptuJavaScript ES6 sintaksaUvod u Node.jsUvod u AJAX (Asynchronous JavaScript And XML)Uvod u Fetch APIUvod u PythonJSON - tekstualni format za razmenu podatakaASCII, Unicode i UTF - Predstavljanje znakova na računarimaIzbor prvog programskog jezikaGNU/Linux - 1. deo - Uvod
Svi članci
Simplicity is prerequisite for reliability.
Edsger Dijkstra

Callback funkcije i lambda izrazi

Facebook LinkedIn Twitter Viber WhatsApp E-mail
zoom_plus zoom_minus bookmark
početna > Članci > Teorija

Uvod

U programiranju, termin 'funkcija povratnog poziva' (eng. callback function), označava funkciju koja se drugoj funkciji predaje u svojstvu argumenta, nakon čega se predata funkcija može pokretati u okviru pozivajuće funkcije - u situacijama kada (tj. ako) se javi potreba za pokretanjem predate funkcije.

Navedena ideja možda ne deluje posebno 'zanimljivo i ubedljivo' na prvi pogled (budući da znamo da se u telu jedne funkcije može po potrebi pozvati neka druga funkcija), međutim, povratni pozivi podrazumevaju sasvim drugačiji način pokretanja spoljnih funkcija * i predstavljaju pristup koji otvara veoma zanimljive mogućnosti (i sve se odvija uz uvažavanje posebnih pravila na koja ćemo se kasnije osvrnuti).

Što se tiče lambda izraza, ** za sam početak se može reći da je u pitanju način da se funkcije manjeg obima zapišu "na licu mesta" (često u samo jednom redu), na mestima gde se inače pozivaju imenovane funkcije, pri čemu se ovakav vid zapisa najčešće vezuje upravo za povratne pozive (naravno, kasnije u članku, o svemu ćemo prodiskutovati detaljnije).

Za početno upoznavanje sa callback funkcijama i lambda izrazima koristićemo pre svega JavaScript, jezik u kome su lambda izrazi (kao i mnogo šta drugo) implementirani na "idiosinkratičan" *** ali istovremeno vrlo pregledan i razumljiv način (s tim da ćemo pred kraj članka takođe prikazati primere upotrebe lambda izraza i u Python-u, C#-u i Javi).

* Funkcije koje su vezane za određene događaje u toku izvršavanja programa (kao što smo prikazali u uvodnom članku o JavaScript-u), verovatno predstavljaju najočigledniji primer navedenog pristupa: kada korisnik klikne na dugme (ili, malo drugačije - "ako", korisnik klikne na dugme), poziva se odgovarajuća funkcija koja 'reaguje na klik'.

(Naravno, postoje i drugi načini da se povratni pozivi iskoriste.)

** Tipično, ako se određena pojava u nauci ili tehnici označi određenim slovom grčkog alfabeta (ovoga puta "lambda"), cela pojava poprima odlike misterioznosti i deluje komplikovano (kao nekakav tajni projekat od koga je zavisio ishod Drugog svetskog rata, ili nešto iz naučne fantastike), ali, ne bojte se, lambda izrazi nisu nešto komplikovano, već (naprotiv), uglavnom pružaju mogućnost da se programski kod pojednostavi.

*** Lambda izrazi, odnosno lambda funkcije, u JavaScript-u ne nose zvanično naziv "lambda funkcije", ali, u praksi stvari funkcionišu kao i u drugim jezicima (u kojima se naziv "lambda funkcije" koristi zvanično), a sama sintaksa je vrlo pregledna i elegantna.

Funkcije povratnog poziva uopšteno

Na samom početku, razmotrićemo jednostavan uvodni primer, pri čemu ćemo pretpostaviti da je čitaocima poznato da se preko funkcije setInterval (u jeziku JavaScript), mogu periodično pozivati druge funkcije:

		
function ispis() {
	let panel       = document.getElementById("info_panel");
	let d           = new Date();
	let sat         = d.getHours();
	let min         = d.getMinutes();
	let sek         = d.getSeconds();
	panel.innerHTML = `${sat}:${min}:${sek}`;
}

setInterval(ispis, 1000);
		
	
Slika 1. - Primer korišćenja funkcije setInterval.

Primer sa gornje slike (praktično) omogućava prikaz trenutnog vremena, preko HTML elementa sa id-om "info_panel", * pri čemu se rezultat dobija u sprezi dve funkcije:

  • ispis - očitavanje i ispis trenutnog vremena
  • setInterval - periodično pozivanje funkcije ispis

* Najprostije rečeno, u pitanju je "digitalni časovnik sa sekundama".

Međutim, na ovom mestu, funkcija setInterval za nas je zanimljivija, jer može se primetiti da se naziv druge funkcije pojavljuje kao jedan od argumenata.

Kao što smo naveli na početku: kada se određenoj funkciji, identifikator druge funkcije preda kao argument, * predata funkcija ima ulogu funkcije povratnog poziva.

U gornjem primeru, preko funkcije setInterval (čija je svrha periodično pokretanje drugih funkcija), pokretali smo funkciju ispis, međutim, bitno je primetiti da bi funkcija setInterval bila u stanju da pokrene bilo koju funkciju ** čiji bi identifikator (na istom mestu) bio predat kao argument. ***

* Obratite pažnju pri pisanju programskog koda: kao argument se predaje samo identifikator funkcije (bez zagrada).

** U svojstvu argumenta, predali smo funkciju ispis, ali (najprostije rečeno) - "mogli smo i neku drugu".

U svakom slučaju, u pitanju je pristup koji nije isti kao da smo samo pozvali funkciju ispis unutar funkcije setInterval (što će postati još jasnije u nastavku), međutim ....

*** Konkretna funkcija koju smo koristili, setInterval, projektovana je tako da može primati druge funkcije u svojstvu argumenta.

Ukoliko (neka druga) funkcija nije projektovana tako da može primati nazive funkcija kao argumente, povratni pozivi - ne funkcionišu.

Da bismo bolje razumeli pravu upotrebnu vrednost funkcija povratnog poziva (tj. da bismo što bolje razumeli razliku između povratnih poziva i 'običnog pozivanja funkcije unutar funkcije'), razmotrićemo jednostavan primer funkcije za iscrtavanje grafikona matematičkih funkcija.

U pitanju je svojevrstan "šematski kod" (regularna JS sintaksa, ali, bez prave implementacije funkcija), pri čemu će glavna funkcija Iscrtavanje, koja za početak ne koristi povratne pozive, na kraju (naravno) biti unapređena.

		
// Osnovna ideja podrazumeva da se
// funkcije koje generišu
// parove koordinata .....

function generisanjeKoordinataSin() {
	// naredbe
}

function generisanjeKoordinataCos() {
	// naredbe
}

function generisanjeKoordinataTan() {
	// naredbe
}

function generisanjeKoordinataCtg() {
	// naredbe
}

// .... koriste unutar glavne
// funkcije za iscrtavanje:

function Iscrtavanje(vrsta) {
	
	koordinate = [];
	
	switch (vrsta) {
		case "sin": koordinate.push(generisanjeKoordinataSin()); break;
		case "cos": koordinate.push(generisanjeKoordinataCos()); break;
		case "tan": koordinate.push(generisanjeKoordinataTan()); break;
		case "ctg": koordinate.push(generisanjeKoordinataCtg()); break;
		default: break;
	}

	CrtanjeGrafikona(koordinate);
}

// Poziv:

Iscrtavanje("sin");
		
	
Slika 2. - Šematski prikaz funkcije koja generiše grafikon jedne od četiri osnovne trigonometrijske funkcije (u sledećem koraku, uz korišćenje povratnog poziva, znatno ćemo uprostiti prikazani kod).

Pretpostavljamo da prikazani (pseudo)kod zapravo ne deluje 'posebno problematično' (pogotovo iz perspektive mlađih programera), ali, izvesno je da bi mogao biti i bolji.

Može se reći da programski kod - za sada - nije posebno problematičan, ali, ako bismo dodali sve poznatije funkcije koje se izučavaju od starijih razreda osnovne škole do starijih razreda srednje škole (recimo da takvih funkcija okvirno ima dvadesetak), nije teško zaključiti da bi programski kod - u novim okolnostima - postao prilično glomazan i nepraktičan.

Naravno, budući da ne mora biti tako, osvrnućemo se na elegantnije i praktičnije rešenje (koje je jedna od dve glavne teme članka).

Ako se, u novoj verziji funkcije, kao parametar doda callback funkcija (i ako se izbaci parametar "vrsta" koji više nije od koristi), celo (ni iz daleka neobimno) switch grananje iz prethodne implementacije - praktično se svodi na samo jednu naredbu ....

		
function Iscrtavanje(callback) {
	koordinate = [];
	koordinate.push(callback());
	CrtanjeGrafikona(koordinate);
}

// switch nije 'sveden', niti prepravljen,
// već (najprostije) - izbačen, ali, postigli
// smo da funkcionalnost bude očuvana
// (štaviše, funkcionalnost je praktično
// poboljšana - uz pojednostavljivanje koda).
		
	
Slika 3. - Funkcija za prikaz grafikona matematičkih funkcija, realizovana preko povratnog poziva.

.... posle čega, poziv funkcije za iscrtavanje, podrazumeva navođenje konkretnih funkcija za generisanje koordinata - u svojstvu argumenata:

		
// Iscrtavanje grafikona funkcije sin(x):
Iscrtavanje(generisanjeKoordinataSin);

// Iscrtavanje grafikona funkcije cos(x):
Iscrtavanje(generisanjeKoordinataCos);
		
	
Slika 4. - Poziv funkcije 'iscrtavanje', uz predavanje naziva odgovarajućih funkcija za generisanje koordinata za grafikone.

Boljom organizacijom programskog koda postiže se sledeće:

  • glavna funkcija za iscrtavanje je znatno pojednostavljena
  • ceo program je postao znatno jednostavniji za proširivanje (bilo koja funkcija koja ima odgovarajuću kombinaciju ulaznih i izlaznih vrednosti, može se sada proslediti kao funkcija povratnog poziva)

Druga stavka zaslužuje posebnu pažnju ....

Ako se javi potreba za proširivanjem mogućnosti funkcije Iscrtavanje, tako da bude dodata opcija za iscrtavanje (na primer) grafikona logaritamske funkcije - dovoljno je definisati funkciju koja će generisati koordinate za grafikon logaritamske funkcije - i predati takvu funkciju kao argument:

		
function generisanjeKoordinataLog() {
	// naredbe
}

// Iscrtavanje grafikona logaritamske funkcije:
Iscrtavanje(generisanjeKoordinataLog);
		
	
Slika 5. - Proširivanje mogućnosti funkcije iscrtavanje, prostim dodavanjem i pozivanjem nove funkcije.

Svakako je u pitanju bolji pristup, u odnosu na switch grananje, ali, pošto funkcije koje smo prikazali nisu u stanju da daju pravi rezultat (budući da sadrže samo pseudokod), pogledajmo i jednostavan primer koji odmah možete sami isprobati, pre nego što pređemo na nove teme (nakon čega će biti i zanimljivijih konkretnih primera):

		
function f1() {
	console.log("Poziv funkcije f1()");
}

function f2() {
	console.log("Poziv funkcije f2()");
}

function f3(callback1, callback2) {
	console.log("Funkcija f3() obavlja sledeće pozive:");
	callback1();
	callback2();
}

// Poziv:
f3(f1, f2);
f3(f2, f1);
		
	
Slika 6. - Primer jednostavne funkcije kojoj se kao argumenti predaju druge funkcije (primer možete isprobati samostalno).

Pokretanjem skripte sa slike #6 dobija se sledeći ispis:

		
Funkcija f3() obavlja sledeće pozive:
Poziv funkcije f1()
Poziv funkcije f2()

Funkcija f3() obavlja sledeće pozive:
Poziv funkcije f2()
Poziv funkcije f1()
		
	
Slika 7. - Konzolni ispis koji se dobija pozivanjem funkcije sa prethodne slike.

Callback funkcije i lambda izrazi u JavaScript-u

Kroz primer sa JS funkcijom za iscrtavanje grafikona matematičkih funkcija dalo se naslutiti da bi konkretne funkcije za generisanje koordinata - koje bi se koristile kao funkcije povratnog poziva - najverovatnije bili blokovi koda većeg obima i značaja, međutim (nasuprot navedenom), u praksi se vrlo često javlja i potreba za callback funkcijama sa veoma malim brojem naredbi (neretko sa samo jednom naredbom), i tada je uputno koristiti lambda izraze - male, neimenovane funkcije koje se u JavaScriptu mogu implementirati na više načina:

  • preko neimenovanih funkcija
  • preko 'streličastih' funkcija

.... ali, kada se pomenu lambda izrazi u JavaScript-u, obično se misli 'baš' na streličaste funkcije (više o "nomenklaturi", u nastavku).

Mapiranje i filtriranje nizova, tipični su primeri (reklo bi se zapravo - 'najtipičniji'), za metode u kojima je posebno zgodno koristiti lambda notaciju za definisanje funkcija povratnog poziva.

Ako kao ulaznu vrednost koristimo sledeći niz ....

		
let niz1 = [ 1, 2, 3, 4, 5, 6, 7 ];
		
	
Slika 8. - Primer niza koji ćemo koristiti za mapiranje.

.... moguće je kreirati novi niz u koji se kopira kvadrat svakog elementa ulaznog niza:

		
let niz2 = niz1.map(x => x * x);

// niz2 = [ 1, 4, 9, 16, 25, 36, 49 ];
		
	
Slika 9. - Kreiranje novog niza preko ugrađene funkcije map (kojoj se predaje lambda izraz).

.... a takođe postoji mogućnost * da se u novi niz kopiraju samo elementi koji su veći od 10 ("svako x, tako da važi da je x > 10"):

		
let niz3 = niz1.filter(x => x > 10);

// niz3 = [ 16, 25, 36, 49 ];
		
	
Slika 10. - Kreiranje novog niza preko ugrađene funkcije filter (kojoj se predaje lambda izraz).

* Naravno, postoje i mnoge druge mogućnosti.

Sa jedne strane, prikazani zapis deluje vrlo intuitivno, ali, istovremeno (sa druge strane), iskustvo pokazuje da mnogi programeri koji se prvi put sreću sa lambda zapisom koji se koristi u ugrađenim funkcijama kao što su map i filter, najčešće imaju utisak da je u pitanju sintaksa koja je svojstvena samo navedenim funkcijama (to jest, deluje da nije u pitanju notacija koja se može koristiti i u drugim okolnostima).

Pokazaćemo da to nije slučaj, već, da je samo u pitanju veoma elegantan zapis callback funkcija koje bi se inače mogle realizovati i preko 'običnih' neimenovanih funkcija (a svakako i preko imenovanih funkcija). *

Zarad upoznavanja sa različitim implementacijama funkcija povratnog poziva u JavaScript-u ** - samostalno ćemo implementirati funkciju za mapiranje niza.

Funkcija povratnog poziva biće implementirana na tri različita načina:

  • preko imenovane funkcije
  • preko neimenovane funkcije
  • preko lambda notacije

* Kad smo već kod 'početničkih grešaka', 🙂 recimo da ima i mlađih programera koji - za razliku od kolega koji na početku smatraju da je lambda notacija nešto što postoji samo unutar ugrađenih funkcija kao što su map i filter - smatraju da se lambda notacija može koristiti "svuda" (to jest, da se lambda izraz može predati bilo kojoj funkciji (u svojstvu argumenta)).

Međutim, kao što smo ranije pomenuli (kada smo koristili funkciju setInterval), neophodno je da funkcije - kojima se druge funkcije predaju u svojstvu argumenata - budu projektovane tako da na pravilan način prihvate predati identifikator spoljne funkcije.

Lambda notacija nije način da se funkcije koje inače nisu projektovane tako da koriste funkcije povratnog poziva, "privole da koriste povratne pozive" (već 'samo' sintaksa koja omogućava da se callback funkcije zapišu na koncizniji način).

** Kao što smo na početku naveli, smatramo da je JavaScript najprikladniji jezik za početno upoznavanje sa povratnim pozivima i lambda funkcijama, ali, naravno, prikazani principi važe i u mnogim drugim jezicima (na implementaciju u Python-u, C#-u i Javi, takođe ćemo se osvrnuti nešto kasnije u članku).

Implementacija funkcija povratnog poziva preko imenovanih funkcija

Za početak, kreiraćemo samu funkciju za mapiranje niza, po ugledu na ugrađenu funkciju map:

		
function mapiranje(niz, callback) {
	let novi_niz = [];

	for (let i = 0; i < niz.length; i++) {
		novi_niz.push(callback(niz[i]));
	}

	return novi_niz;
}
		
	
Slika 11. - Samostalna implementacija funkcije za mapiranje niza, koja u svojstvu argumenata prima niz i callback funkciju (preko koje se određuje po kom obrascu se elementi kopiraju).

Vidimo da se svaki element preslikava preko funkcije povratnog poziva, koja može biti implementirana u vidu (bilo kakve) konkretne funkcije - koja ispunjava sledeća dva uslova:

  • prima jedan celobrojni argument
  • vraća jednu celobrojnu vrednost

Sada možemo definisati i funkciju kvadriranje, koju ćemo proslediti funkciji mapiranje - u svojstvu callback funkcije:

		
function kvadriranje(x) {
	return x * x;
}

let niz      = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ];
let novi_niz = mapiranje(niz, kvadriranje);
		
	
Slika 12. - Pozivanje funkcije 'mapiranje', uz predavanje imenovane funkcije ('kvadriranje') u svojstvu callback funkcije.

Prikazane instrukcije (naravno) proizvode očekivani rezultat, međutim, budući da je funkcija kvadriranje mali i jednostavan blok programskog koda (praktično - jedna naredba), postoje i jednostavniji načini da se funkcija mapiranje poveže sa funkcijom povratnog poziva.

Implementacija funkcija povratnog poziva preko neimenovanih funkcija

Definisanje i korišćenje neimenovane (tj. "bezimene") funkcije, može se shvatiti kao "prvi stepen optimizacije".

U školskim primerima, može se shvatiti i kao 'međukorak' prema implementaciji lambda funkcije.

Funkcija kvadriranje koju smo ranije koristili ....

		
function kvadriranje(x) {
	return x * x;
}		
		
	
Slika 13. - Funkcija za računanje kvadrata unete vrednosti.

.... može se zapisati i na drugi način (kao neimenovana funkcija) ....

		
function (x) {
	return x * x;
}
		
	
Slika 14. - Funkcija za računanje kvadrata, implementirana kao neimenovana funkcija.

.... ali - pod uslovom da se takav kod zapiše, tj. definiše, na istom mestu na kom se poziva (ne možemo funkciju pisati u novom obliku "bilo gde", van povratnih poziva, a da to ima ikakvog smisla):

		
let niz      = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ];
let novi_niz = mapiranje(niz, function (x) {
	return x * x;
});
		
	
Slika 15. - Poziv funkcije 'mapiranje', uz korišćenje neimenovane funkcije (koja vraća kvadrat unete vrednosti) - u svojstvu callback funkcije.

Dakle, "bezimene" funkcije se mogu pisati na mestima na kojima se inače pozivaju unapred definisane imenovane funkcije, pri čemu se (neimenovana) funkcija praktično definiše na istom mestu na kom se i poziva.

Neimenovana funkcija koju smo videli, predstavlja lambda izraz (funkciju manjeg obima koja je implementirana "na licu mesta"), ali - u najpraktičnijem smislu - pojam lambda izraza u JavaScript-u najčešće se vezuje za tzv. arrow funkcije.

Streličaste ("arrow") funkcije / lambda notacija

Strogo formalno, u JavaScript-u ne postoji sintaksa koja se zvanično prepoznaje kao lambda notacija, međutim, tzv. "streličaste" funkcije (eng. "arrow functions"), koje, po svemu - osim po imenu - odgovaraju onome što se u drugim jezicima smatra i naziva lambda notacijom, predstavljaju de facto standard za implementaciju lambda izraza u JS-u. *

Sam naziv "streličaste funkcije" (to jest, popularno, "arrow funkcije"), u vezi je sa operatorom => (koji se koristi u streličastim funkcijama).

Za početak je najlakše da nastavimo tamo gde smo stali, i usmerimo se na funkciju koja vraća kvadrat unetog broja (koju ćemo zapisati preko 'lambda notacije'). *

* Bićemo praktični u nastavku: za pojavu lambda izraza koji su (u JavaScript-u) implementirani preko streličastih funkcija, koristićemo i odrednice "lambda funkcije", "lambda notacija" i "lambda zapis" (što je pristup koji je veoma uobičajen i na drugim mestima u literaturi, a pogotovo u svakodnevnom govoru).

Naravno, streličaste funkcije se takođe moraju zapisati, tj. definisati - na mestima na kojima se i pozivaju (s tim da postoji i drugi način upotrebe, o čemu ćemo pisati kasnije u članku).

Kreiranje arrow funkcija po ugledu na "obične" anonimne funkcije

Ako pažljivije razmotrimo strukturu obične, "nestreličaste" anonimne funkcije manjeg obima * (a isto važi i za imenovane funkcije slične strukture) ....

		
function (x) {    // ulazna vrednost
	return x * x; // obrada ulazne vrednost
}
		
	
Slika 16. - Prvi korak u kreiranju lambda izraza po ugledu na neimenovanu funkciju, podrazumeva - prepoznavanje parametara i naredbi od kojih zavisi izlazna vrednost (u primeru koji razmatramo, to su: parametar x i naredba return x * x).

.... može se zapaziti da rezultat izvršavanja funkcije zavisi od vrednosti parametra x i naredbe preko koje se računa povratna vrednost (x * x), i može se primetiti da pojava rezervisane reči function praktično nema uticaja na rezultat.

Bez rezervisane reči function, program sa prethodne slike ne bi (naravno) proradio, ali, sama po sebi, rezervisana reč function (kao što smo naveli), nema direktnog uticaja na rezultat izvršavanja funkcije.

* U većini slučajeva, lambda funkcije praktično sadrže (samo) jednu naredbu, i upravo zato smo za primer uzeli funkciju sa jednom naredbom.

Shodno navedenom, JavaScript omogućava da se zapis dodatno optimizuje.

Prvo se može izostaviti rezervisana reč function, a između parametara i tela funkcije (tj. 'vitičastih zagrada'), zapisuje se operator => ("lambda operator"):

		
(x) => {
	return x * x;
}
		
	
Slika 17. - Arrow funkcija u programskom jeziku JavaScript, preko koje se implementira lambda izraz za računanje kvadrata unete vrednosti.

U slučaju da funkcija sadrži samo jedan parametar, mogu se izostaviti zagrade oko paramet(a)ra ....

		
x => {
	return x * x;
}
		
	
Slika 18. - Pojednostavljivanje lambda izraza, izostavljanjem zagrada oko jedinog parametra (x).

Ako funkcija ima više od jednog parametra, ili manje od jednog parametra, zagrade se ne mogu izostaviti.

Ako funkcija sadrži samo jednu naredbu, mogu se izostaviti i zagrade oko bloka naredbi (vitičaste zagrade) ....

		
x =>
	return x * x;
		
	
Slika 19. - Pojednostavljivanje lambda izraza, izostavljanjem vitičastih zagrada oko jedine naredbe iz tela funkcije (return x * x;).

.... a ako funkcija sadrži samo jednu naredbu, takođe se može izostaviti i rezervisana reč return:

		
x =>
	x * x;
		
	
Slika 20. - Pojednostavljivanje lambda izraza, izostavljanjem rezervisane reči return - u slučaju kada telo funkcije sadrži samo jednu naredbu.

Da se podsetimo: ranije smo naveli da je u pitanju funkcija koja prima jedan argument i vraća vrednost, i stoga, ukoliko telo funkcije sadrži samo jednu naredbu, jasno je da se radi o naredbi preko koje funkcija vraća vrednost (odnosno, tako bi "moralo biti"). :)

Takođe, sada je sasvim primereno da se ceo izraz zapiše u jednom redu:

		
x => x * x;
		
	
Slika 21. - Estetsko "doterivanje" arrow funkcije (koja sada praktično predstavlja "uobičajeni" Lambda izraz).

Na kraju (posle detaljnog prikaza "metamorfoze" neimenovane funkcije u lambda zapis), možemo pozvati i funkciju za mapiranje, uz korišćenje lambda izraza:

		
let niz      = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ];
let novi_niz = mapiranje(niz, x => x * x);
		
	
Slika 22. - Poziv funkcije 'mapiranje' koju smo prethodno definisali, uz predavanje lambda izraza u svojstvu callback funkcije.

Programski kod koji smo videli, idejno je vrlo sličan pozivu ugrađene funkcije map (koju smo koristili na početku), a sam lambda izraz je identičan.

Ukratko o funkcijama višeg reda

Pojam funkcija višeg reda obuhvata:

  • funkcije koje mogu prihvatati (druge) funkcije u svojstvu argumenata (što smo već videli)
  • funkcije čija je povratna vrednost - takođe funkcija (čime ćemo se baviti drugom prilikom)

Povratni pozivi nisu jedina prilika za korišćenje lambda izraza, međutim, ako je potrebno arrow funkcije koristiti izvan konteksta povratnih poziva (pozabavićemo se dodatno funkcijama koje primaju funkcije kao argumente), određena anonimna funkcija može se dodeliti imenovanoj konstanti:

		
let zbir = (x, y) => x + y;
		
	
Slika 23. - Definicija arrow funkcije koja se pripisuje imenovanoj konstanti.

.... posle čega se konstanta može koristiti za pozivanje pripisanog lambda izraza.

Na primer, pozivanjem sledećeg koda ....

		
console.log(zbir(5, 10));
		
	
Slika 24. - Pozivanje arrow funkcije preko imenovane konstante.

.... u konzoli će biti ispisano 15.

Zarad detaljnijeg upoznavanja, uzećemo za primer da je potrebno implementirati lambda izraz koji proverava da li dva uneta broja imaju veći zbir ili razliku, što bismo mogli učiniti i preko specijalizovanog lambda izraza, na sledeći način ....

		
let odabir = (x, y) => (x + y >= x - y)? x + y : x - y;
		
	
Slika 25. - Definisanje lambda funkcije koja proverava da li dva uneta argumenta imaju veći zbir, ili razliku.

.... ali, ako je potrebno nešto više 'fleksibilnosti' (tj. ako želimo da budemo u mogućnosti da lambda izraz za odabir časkom prepravimo, tako da se umesto zbira i razlike u obzir uzme zbir i razlika kubova), biće potrebno osmisliti 'uopšteniji' lambda izraz.

Za početak, zadržaćemo se na zbiru i razlici, i pridodaćemo funkciju koja računa razliku (a uskoro ćemo implementirati i druge "unutrašnje" funkcije):

		
let razlika = (x, y) => x - y;
		
	
Slika 26. - Definicija lambda funkcije koja računa razliku dva argumenta (i može se pozivati preko imenovane konstante).

Implementiraćemo zatim i funkciju koja vraća veću od dve unete vrednosti ....

		
let vecaOdDveVrednosti = (x, y) => (x >= y)? x : y;
		
	
Slika 27. - Definicija lambda funkcije koja vraća veći od dva argumenta.

.... koja, ako je pozovemo na sledeći način:

		
let odabir = (x, y, f1, f2) => vecaOdDveVrednosti(f1(x, y), f2(x, y));
console.log(odabir(-10, -15, zbir, razlika));

// Funkciju takođe možemo pozvati i na sledeći način:
// console.log(odabir(-10, -15, (x, y) => x + y, (x, y) => x - y));
		
	
Slika 28. - Definicija i poziv opšte funkcije koja proverava koja od dve funkcije - koje se (takođe) predaju kao argumenti - vraća veću vrednost.

.... praktično postaje ono što smo naumili: funkcija koja proverava da li dve unete vrednosti imaju veći zbir ili razliku, ali - istovremeno - i funkcija koja se može lako prepraviti ....

		
let f3 = (x, y) => x ** 3 + y ** 3; // zbir kubova
let f4 = (x, y) => x ** 3 - y ** 3; // razlika kubova

console.log(odabir(-10, -15, f3, f4));
		
	
Slika 29. - Poziv funkcije 'odabir', uz predavanje drugih (lambda) funkcija u svojstvu argumenata.

.... prostim predavanjem drugačijih lambda izraza (u svojstvu argumenata).

Verujemo da će mnogi čitaoci prepoznati lepotu i potencijal ovakvog pristupa, ali, pravi uvod u funkcionalno programiranje ipak ćemo ostaviti za neku drugu priliku.

U nastavku, osvrnućemo se na implementaciju lambda izraza u drugim jezicima (prvi sledeći jezik je Python) ....

Callback funkcije i lambda izrazi u Python-u

Da bismo se upoznali sa funkcijama povratnog poziva i Lambda izrazima u Python-u, ponovo ćemo mapirati niz.

Python takođe (reklo bi se, 'očekivano'), nudi 'lep' i elegantan ugrađeni mehanizam za mapiranje:

		
niz      = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]
novi_niz = list(map(lambda x: x * x, niz))
		
	
Slika 30. - Poziv funkcije za mapiranje niza u programskom jeziku Python, uz korišćenje lambda izraza.

.... ali ponovo ćemo proći kroz sve korake samostalne implementacije (zarad 'utvrđivanja gradiva').

Definisaćemo prvo sopstvenu funkciju za mapiranje ....

		
def mapiranje(niz, callback):
	pom = []
	for n in niz:
		pom.append(callback(n))

	return pom
		
	
Slika 31. - Samostalna implementacija funkcije za mapiranje niza u programskom jeziku Python (funkcija mapiranje, prima callback funkciju kao jedan od argumenata).

.... koja se nadalje može pozivati na dva načina:

- Preko unapred definisane imenovane funkcije:

		
def kvadriranje(x):
	return x * x

niz      = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]
novi_niz = mapiranje(niz, kvadriranje)
		
	
Slika 32. - Poziv funkcije 'mapiranje', uz predavanje prethodno definisane imenovane funkcije u svojstvu argumenta.

- Preko lambda izraza:

		
niz      = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]
novi_niz = mapiranje(niz, lambda x: x * x)
		
	
Slika 33. - Poziv funkcije za mapiranje, uz predavanje lambda izraza u svojstvu argumenta.

Pre svega, lambda notacija je (ovoga puta) jedini način za implementaciju anonimne callback funkcije (jer Python ne podržava drugi način).

Što se tiče same lambda notacije u Python-u, prvo treba primetiti da se lambda izrazi ne definišu preko operatora =>, kao u JavaScript-u (i mnogim drugim jezicima), već, navođenjem rezervisane reči lambda:

		
lambda x: x * x
		
	
Slika 34. - Primer lambda izraza sa jednim parametrom u programskom jeziku Python.

Osim navedenih pravila, u Python-u važi i pravilo da se zagrade mogu izostaviti u slučaju izraza sa jednim parametrom (što smo videli u gornjim primerima).

Ako izraz ima više od jednog parametra - zagrade se ne smeju izostavljati ....

		
lambda (x, y): 2 * x + y
		
	
Slika 35. - Primer lambda izraza sa dva parametra (ili više) u programskom jeziku Python.

.... a isto važi i za lambda izraze bez paramet(a)ra (baš kao i u JavaScript-u):

		
lambda ():  print("Dobar dan!")
		
	
Slika 36. - Lambda izraz bez parametara.

Kada se radi o jezicima kao što su C# i Java, može se reći da je implementacija lambda izraza idejno slična implementaciji u JS-u ili Python-u, * i stoga se nećemo previše udubljivati u primere (prosto rečeno - ne želimo da se ponavljamo).

Sa druge strane, postoje (naravno) detalji implementacije lambda izraza u C#-u i Javi, koji su i te kako vredni pažnje ....

* Ako zanemarimo sitne (i očigledne) tehničke razlike, implementacija lambda izraza u Python-u, u idejnom smislu je veoma slična implementaciji lambda izraza u JavaScript-u.

Callback funkcije i lambda izrazi u C#-u

Slično kao u prethodnim primerima (iz drugih jezika), u C#-u ćemo takođe prvo napraviti kratak osvrt na ugrađenu funkciju za mapiranje nizova:

		
Int32[] niz1 = { 1, 2, 3, 4, 5, 6, 7 };
let     niz2 = niz1.Select(x => x * x);
		
	
Slika 37. - Poziv funkcije za mapiranje niza u programskom jeziku C#.

.... a zatim ćemo preći na implementaciju iz "domaće radinosti" (po ugledu na implementacije koje smo već prikazali).

Za implementaciju povratnih poziva, u C#-u se koriste tzv. delegati: *

		
public delegate Int32 CallbackMapiranje(Int32 v);
		
	
Slika 38. - Delegat (buduće) funkcije za mapiranje niza u programskom jeziku C#.

* Za razliku od JS-a i Python-a, callback funkcije u C#-u (i Javi) ne mogu se pozivati "bez prethodne najave".

Praktično: delegat CallbackMapiranje definiše mogućnost pojave funkcije koja za jednu unetu celobrojnu vrednost (Int32 v), treba da vrati podatak čiji je tip takođe Int32.

Delegati (kako u gornjem primeru, tako i inače), mogu biti implementirani preko imenovanih funkcija, ali - što je u ovom slučaju mnogo zanimljivije - mogu biti implementirani i kao lambda izrazi.

Pri pozivu funkcije koja koristi delegat, situacija je slična kao i do sada (samo što je ovoga puta sve 'zvaničnije'): ili treba navesti identifikator konkretne funkcije (koja odgovara ulaznim i izlaznim parametrima delegata), ili treba implementirati odgovarajući lambda izraz.

		
public delegate Int32 CallbackMapiranje(Int32 vrednost);

List<Int32> mapiranje(List<Int32> niz, CallbackMapiranje callback) {	
    List<Int32> nova_lista = new List<Int32>();

    for (Int32 i = 0; i < niz.Count; i++) {
        nova_lista.Add(callback(niz[i]));
    }

    return nova_lista;
}

// poziv:

List<Int32> niz1 = new List<Int32> { 1, 2, 3, 4, 5, 6, 7 };
let         niz2 = mapiranje(niz1, x => x * x);
		
	
Slika 39. - Poziv funkcije za mapiranje niza koju smo samostalno implementirali.

Implementacija u Javi, u idejnom smislu je gotovo identična implementaciji u C#-u, ali - 'tehnikalije' su nešto kompleksnije (doduše, "programski kod koji povećava kompleksnost", nema mnogo veze sa implementacijom lambda funkcija).

Callback funkcije i lambda izrazi u Javi

Kada je u pitanju implementacija u Javi, prvo se može primetiti da inicijalizacija liste nije više toliko 'komotna' kao do sada, a pri tome je i sam proces mapiranja niza ponešto komplikovaniji.

		
List<Integer> niz1 = new ArrayList<Integer>() {{
	add(1);
	add(2);
	add(3);
	add(4);
	add(5);
	add(6);
	add(7);
}};

let niz2 = niz1.stream()
               .map(x -> x * x)
               .collect(Collectors.toList());
		
	
Slika 40. - Poziv ugrađene funkcije za mapiranje niza u programskom jeziku Java.

Kao što rekosmo, "komplikovaniji kod" nema toliko veze sa lambda izrazima, koliko sa drugim detaljima.

Što se tiče lambda izraza, napomenimo da sam lambda operator ima nešto drugačiji oblik: -> (kao što možemo videti na gornjoj slici), a pre nego što implementiramo metodu za mapiranje, osvrnućemo se i na pojam interfejsa.

Slično delegatima u C#-u (sa kojima smo se upoznali u prethodnom poglavlju), interfejsi u Javi definišu najopštije okvire (tj. parametre i povratne vrednosti), funkcija koje će biti implementirane 'kasnije'.

Na primer interfejs CallbackMapiranje (baš kao i delegat istog naziva iz C#-a) ....

		
interface CallbackMapiranje {  
     int funkcija(int v);  
}
		
	
Slika 41. - Definicija interfejsa za mapiranje nizova u programskom jeziku Java.

.... određuje - na mestu na kom se poziva - pojavu funkcije, za koju (još uvek) nije poznato kako će tačno biti implementirana, ali, poznato je da kao argument prima jednu celobrojnu vrednost i vraća podatak celobrojnog tipa.

Pozivanje metode preko interfejsa izvodi se na ponešto drugačiji način (kao što ćemo videti), međutim, postupak je i dalje sasvim jasan i intuitivan.

		
package mapiranjeNizova;

import java.util.ArrayList;
import java.util.List;

public class MapiranjeNizova {
	
	interface CallbackMapiranje {  
	     int funkcija(int v);  
	}
	
	static List<Integer> mapiranje(List<Integer> lista,
								   CallbackMapiranje callback) {
		
		List<Integer> nova_lista = new ArrayList<Integer>();
		
		for (int i = 0; i < lista.size(); i++) {
			nova_lista.add(callback.funkcija(lista.get(i)));
		}
		
		return nova_lista;
	}

	public static void main(String[] args) {
		     
		List<Integer> niz1 = new ArrayList<Integer>() {{
			add(1);
			add(2);
			add(3);
			add(4);
			add(5);
			add(6);
			add(7);
		}};
		
		let niz2 = mapiranje(niz1, x -> x * x);
		
		System.out.println(niz2.toString());
				
	}
}
		
	
Slika 42. - Poziv funkcije za mapiranje niza, koju smo samostalno implementirali u programskom jeziku Java.

Može se zaključiti da C#-a i Java, kada su u pitanju callback funkcije i lambda izrazi, funkcionišu po istom opštem principu kao i interpretirani jezici JavaScript i Python - uz korišćenje kompleksnijih i "zvaničnijih" programskih konstrukcija (što je i inače odlika jezika kao što su Java i C#, u odnosu na JavaScript, Python i druge skriptne jezike).

Za kraj ....

Na ovom mestu ćemo se 'odjaviti', uz nekoliko opštih opaski ....

Posle izvesnog vremena provedenog u izučavanju programskih kodova različitog nivoa kompleksnosti, primetićete - sa jedne strane - pojavu jezičkih konstrukcija većeg obima (klase, funkcije, moduli, biblioteke i sl), koje su u svakodnevnom radu (krajnje) neophodne.

Neke od takvih konstrukcija su "lepe i jednostavne" (bez obzira na veliki obim i nezanemarljivu kompleksnost), druge nisu jednostavne, ali - u oba slučaja se radi o (praktično) neophodnim mehanizmima za obradu podataka.

Sa druge strane, postoje i konstrukcije čiji obim, nivo kompleksnosti i objektivni značaj nisu preveliki, to jest (jednostavno rečeno), u pitanju su opcije "bez kojih bi se moglo".

Shodno prethodnoj 'kategorizaciji', nije teško naslutiti da ciljamo na to da lambda izrazi spadaju u kategoriju opcija "bez kojih bi se moglo", ali, vrlo često (baš kao u slučaju lambda izraza), mnoge opcije bez kojih bi se moglo - veoma dobro dođu, i čine proces kodiranja lakšim, lepšim i prirodnijim. :)

Autor članka Nikola Vukićević Za web portal codeblog.rs
Napomena: Tekstovi, slike, web aplikacije i svi ostali sadržaji na sajtu codeblog.rs (osim u slučajevima gde je drugačije navedeno) predstavljaju intelektualnu svojinu autora sajta codeblog.rs i zabranjeno je njihovo korišćenje na drugim sajtovima i štampanim medijima, kao i bilo kakvo drugo korišćenje u komercijalne svrhe, bez eksplicitnog pismenog odobrenja autora.
© 2020-2026. Sva prava zadržana.
Facebook LinkedIn Twitter Viber WhatsApp E-mail
početna > Članci > Callback funkcije i lambda izrazi
codeBlog codeBlog
Sajt posvećen popularizaciji kulture i veštine programiranja.
Napomena: Tekstovi i slike na sajtu codeblog.rs (osim u slučajevima, gde je drugačije navedeno) predstavljaju intelektualnu svojinu autora sajta codeblog.rs i zabranjeno je njihovo korišćenje na drugim sajtovima i štampanim medijima, kao i bilo kakvo drugo korišćenje u komercijalne svrhe, bez eksplicitnog odobrenja autora.
© 2020-2026. Sva prava zadržana.
Facebook - logo
Instagram - logo
LinkedIn - logo
Twitter - logo
E-mail
Naslovna
   •
Uslovi korišćenja
   •
Obaveštenja
   •
FAQ
   •
Kontakt