Callback funkcije i lambda izrazi
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).
Ideja koju smo naveli, na prvi pogled možda ne deluje posebno zanimljivo i 'ubedljivo' (budući da znamo da se u telu jedne funkcije po potrebi može 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 (naravno, sve se odvija uz uvažavanje posebnih pravila na koja ćemo se kasnije osvrnuti).
Što se tiče lambda izraza, * za početak možemo 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 (a u nastavku ćemo svakako prodiskutovati o detaljima implementacije).
Za početno upoznavanje sa funkcijama povratnog poziva i sa lambda izrazima, koristićemo JavaScript, jezik u kome su lambda izrazi (kao i mnogo šta drugo), implementirani na "idiosinkratičan" ** ali istovremeno vrlo pregledan i razumljiv način, a pred kraj članka osvrnućemo se i na primere upotrebe lambda izraza u Python-u, C#-u i Javi.
Funkcije povratnog poziva uopšteno
Na samom početku, pretpostavićemo da je čitaocima poznato da se preko funkcije setInterval
(u jeziku JavaScript), mogu periodično pozivati druge funkcije:
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 vremenasetInterval
- periodično pozivanje funkcijeispis
Međutim, na ovom mestu, funkcija setInterval
za nas je zanimljivija, jer može se primetiti da se kao jedan od argumenata pojavljuje - naziv druge funkcije.
Kao što smo naveli na početku, kada se određenoj funkciji identifikator druge funkcije predaje kao argument, * predata funkcija ima ulogu funkcije povratnog poziva.
U gornjem primeru, preko funkcije setInterval
(čija je svrha periodično pokretanja 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. ***
Da bismo bolje razumeli pravi smisao funkcija povratnog poziva, pogledajmo primer - šematski prikaz funkcije za iscrtavanje grafika matematičkih funkcija (zapisan preko pseudokoda), pri čemu glavna funkcija Iscrtavanje
, za početak ne koristi povratne pozive (ali, na kraju ćemo je naravno unaprediti):
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.
Ako se u novoj verziji funkcije kao parametar doda callback funkcija (i 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 ....
.... posle čega poziv funkcije za iscrtavanje podrazumeva navođenje konkretnih funkcija za generisanje koordinata - u svojstvu argumenata:
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), grafika logaritamske funkcije - dovoljno je definisati funkciju koja će generisati koordinate za grafik logaritamske funkcije - i predati takvu funkciju kao argument:
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 uvodni primer koji odmah možete sami isprobati, pre nego što pređemo na nove teme (a kasnije će biti i zanimljivijih konkretnih primera):
Navedeni kod daje sledeći ispis:
Callback funkcije i lambda izrazi u JavaScript-u
Kroz primer sa funkcijom za iscrtavanje grafika matematičkih funkcija, mogli ste 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).
U takvim slučajevima, uputno je koristiti lambda izraze - male, neimenovane funkcije koje (u JavaScriptu), mogu biti implementirane na više načina:
- preko neimenovanih funkcija
- preko 'streličastih' funkcija
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 ....
.... moguće je kreirati novi niz u koji se kopira kvadrat svakog elementa ulaznog niza:
.... a takođe postoji mogućnost * da u novi niz budu kopirani samo elementi koji su veći od 10 ("svako x, tako da važi da je x > 10"):
Prikazani zapis, sa jedne strane deluje vrlo intuitivno, ali, istovremeno (sa druge strane), iskustvo je pokazalo 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.
Sama funkcija povratnog poziva biće implementirana na tri različita načina:
- preko imenovane funkcije
- preko neimenovane funkcije
- preko lambda notacije
Implementacija funkcija povratnog poziva preko imenovanih funkcija
Za početak, kreiraćemo samu funkciju za mapiranje niza, po ugledu na ugrađenu funkciju map
:
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:
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
"Prvi stepen optimizacije" tipično podrazumeva - definisanje i korišćenje neimenovane (tj. "bezimene") funkcije.
Funkcija kvadriranje
koju smo ranije koristili ....
.... sada se može zapisati na drugi način ....
.... ali - pod uslovom da se 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):
Dakle, "bezimene" funkcije mogu se 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'. *
Kreiranje arrow funkcija (lambda izraza) 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) ....
.... 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
), a može se primetiti i to da pojava rezervisane reči function
, praktično nema uticaja na rezultat.
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"):
U slučaju da funkcija sadrži samo jedan parametar, mogu se izostaviti zagrade oko paramet(a)ra ....
Ako funkcija ima samo jednu naredbu, mogu se izostaviti i zagrade oko bloka naredbi (vitičaste zagrade) ....
.... a ako funkcija ima samo jednu naredbu, takođe se može izostaviti i rezervisana reč return
:
Takođe, sada je sasvim primereno da se ceo izraz zapiše u jednom redu:
Na kraju (posle detaljnog prikaza "metamorfoze" neimenovane funkcije u lambda zapis), možemo pozvati i funkciju za mapiranje, kojoj se predaje lambda izraz:
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:
.... posle čega se konstanta može koristiti za pozivanje pripisanog lambda izraza.
Na primer, pozivanjem sledećeg koda ....
.... u konzoli će biti ispisano 15
.
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 preko specijalizovanog lambda izraza, na sledeći način:
.... ali, ako je potrebno malo više 'fleksibilnosti' (recimo, želimo da budemo u mogućnosti da lambda izraz za odabir časkom prepravimo, tako da umesto zbira i razlike u obzir budu uzeti 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 (dok ćemo kasnije implementirati i druge "unutrašnje" funkcije):
Implementiraćemo zatim i funkciju koja vraća veću od dve unete vrednosti ....
.... koja, ako je pozovemo na sledeći način:
.... 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 ....
....prostim predavanjem drugačijih lambda izraza (u svojstvu argumenata).
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:
.... ali, ponovo ćemo 'proći kroz sve korake' samostalne implementacije (za vežbu).
Definisaćemo prvo sopstvenu funkciju za mapiranje ....
.... koja se nadalje može pozivati na dva načina:
- Preko unapred definisane imenovane funkcije:
- Preko lambda izraza:
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
:
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 ....
.... a isto važi i za lambda izraze bez paramet(a)ra (baš kao i u JavaScript-u):
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 ....
Callback funkcije i lambda izrazi u C#-u
Kao i u prethodnim primerima (u drugim jezicima), i u slučaju C#-a takođe ćemo prvo napraviti kratak osvrt na ugrađenu funkciju za mapiranje nizova:
.... a zatim preći na implementaciju iz "domaće radinosti" (po ugledu na implementacije koje smo već prikazali).
Za implementaciju callback poziva, u C#-u se koriste tzv. delegati: *
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 - takođe i kao lambda izrazi.
Pri pozivu funkcije koja koristi delegat, potrebno je (kao i do sada (samo, ovoga puta 'zvaničnije')): ili navesti identifikator konkretne funkcije (koja odgovara ulaznim i izlaznim parametrima delegata), ili implementirati odgovarajući lambda izraz.
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, vidimo pre svega da inicijalizacija liste nije više toliko 'komotna' kao do sada, i vidimo da se mapiranje niza takođe obavlja na nešto komplikovaniji način:
Što se tiče lambda izraza, napomenućemo da sam lambda operator ima nešto drugačiji oblik: ->
, 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 odeljku), interfejsi u Javi definišu najopštije okvire (tj. parametre i povratne vrednosti) - 'funkcija koje će biti implementirane tek kasnije'.
Na primer interfejs CallbackMapiranje
(baš kao i delegat istog naziva iz C#-a) ....
.... određuje - na mestu na kom se poziva - pojavu funkcije, za koju (još uvek) ne znamo kako će tačno biti implementirana, ali - znamo da kao argument prima jednu celobrojnu vrednost i vraća podatak celobrojnog tipa.
Pozivanje metode preko interfejsa, izvodi se na malo drugačiji način (kao što ćemo videti), ali, postupak je i dalje sasvim jasan i intuitivan.
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, ali - mehanizmi za prosleđivanje povratnih poziva su kompleksniji i "zvaničniji" (što je i inače odlika ova dva jezika, u odnosu na JavaScript i Python).
Za kraj ....
Na ovom mestu ćemo se 'odjaviti', uz nekoliko opštih opaski ....
Posle nekog vremena provedenog u izučavanju programskih kodova različitog nivoa kompleksnosti, primetićete - sa jedne strane - da postoje jezičke konstrukcije većeg obima (klase, funkcije, moduli, biblioteke), koje su u svakodnevnom radu (krajnje) neophodne.
Sa druge strane, postoje i konstrukcije čiji obim, nivo kompleksnosti i objektivni značaj, nisu preveliki, to jest (prosto rečeno) - u pitanju su opcije "bez kojih bi se moglo".
Međutim - neke od takvih opcija "bez kojih bi se (objektivno) moglo" - baš (!) dobro dođu, i čine proces kodiranja lakšim, lepšim i - prirodnijim. :)
Shodno prethodno navedenoj kategorizaciji, lambda izrazi (ipak) spadaju u drugu kategoriju programskih konstrukcija ("one bez kojih se može"), ali, recimo da nam je drago što su nam lambda izrazi na raspolaganju i, u svemu (uz malo 'pesničke slobode'), lambda izrazi i druge slične pojave, na neki način podsećaju na ljude koje ne poznajemo lično, već samo iz priče (po dobrim delima), ali, povremeno se sa njima sretnemo u prolazu, razmenimo dobronameran prećutni pozdrav, i ostanemo neko vreme pod dobrim utiskom.