Na kraju prethodnog posta o lambda izrazima, spomenuo sam da postoje dva načina kako se mogu koristiti lambda izrazi. Nisam bio baš precizan. Tačnije bi bilo reći da postoje dva načina reprezentacije lambda izraza koje možete da koristite u vašem programu. U prošlom blog postu je pokazan jedan način, a to je "delegate način". Kao što je rečeno, lambda izraz je evolucija delegata i anonimnih delegata iz ranijih verzija .NET framework-a. Prva reprezentacija je dakle delegat.
Druga reprezentacija lambda izraza može da bude tzv. "Esxpression tree". Expression trees su implementacija koncepta poznatog kao "Abstract Syntax Tree (AST)"; oni su reprezentacija određenih izraza u obliku drveća. Jednom rečenicom, ovaj koncept predstavlja reprezentaciju sors koda u obliku
Odakle uopšte ova ideja i zašto se našla u C# verziji 3.0? Krenimo u blagu istorijsku priču...
Svi mi težimo LISP-u...manje ili više
Neretko ćete naći diljem interneta geslo da svi moderni jezici pokušavaju, na ovaj ili onaj način, da stignu neku funkcionalnost koju je LISP nudio pre 30 godina. Ponekad se desi da to bude preterivanje (pogotovo ako imate posla sa "smug lisp weenie"), ali u ovom konkretno slučaju se zadesilo da situacija bude baš takva.
LISP, odnosno Commin LISP i Scheme, dva najpoznatija dijalekta ovog jezika, su sa sobom donela koncept "quote-inga". Princip je relativno jednostavan, bilo da se radi o istoimenoj funkciji ili operatoru: kada odabere deo koda i uradio quote nad njim, interpreter ga ne izvršava, već ga tretira na sličan način kao da ste deklarisali promenljivu. Ukoliko želite da iskoristite taj deo koda, proledite ga kao parametar funkciji eval, koja dinamički re-interpretira taj deo koda i izvršava ga u tom trenutku.
U ovom slučaju, možemo da kažemo da smo tretirali naš kod kao podatke, ne kao kod, što nam je omogućilo da imamo veliku dinamičnost u trenutku runtime-a.
Malo manje ezoterije - JavaScript
Sličan mehanizam postoji u mnogim modernim dinamičkim jezicima. Perl, recimo, koristi eval na različite načina, i u dva osnovna oblika, kao što je već uobičajeno u Perlu[1], kao funkcija ili kao blok. Osnovna upotreba je identična kao gore opisana. Neki deo validnog perl koda se prosledi kao parametar i Perl interpreterga u tom trenutku obradi i izvrši. (Više o ovome, ako vas zanima, na adekvatnim perldoc stranama.)
Mnogo ponzatiji primer je JavaScript. eval() funkcija JavaScript-a je postala osnova novog formata, JSON-a, koji predstavlja okosnicu AJAX rešenja. Kada radite JSON-om, vi prosto prosledite string, koji je u ovom slučaju, serjalizovana predstava JavaScript objekata, eval funkciji, i dobijete nazad graf objekata. Naravno, većina popularnih bilbioteka kao što su jQuery ili AJAX client library. ima dodatne mehanizme da bi povećali sigurnost, ali je suština ista i ta...dinamičnost JavaScript jezika je omogućila da nastane format kao što je JSON.
Ovakva dinamičnost je inspirisala tim koji je pravio C# 3.0 da doda mogućnost pravljenje expression trees direktno u jezik, ali je to urađeno sa jednom specifičnom namerom.
Čemu sve ovo?
Naravno, posle sve ove teorijske diskusija, moguće da vas, verne čitaoce, boli glava i da se formira jedno pitanje, "a čemu sve to"? Odgovor (osim čuvenih "geek poena") je, naravno, LINQ, odnosno, da budem precizniji, LINQ provajderi ka različitim izvorima podataka.
LINQ, kao što sigurno znate, predstavlja API koji je došao sa .NET Framework-om 3.5 i koji pruža jedinstveni skup metoda za upite i projekcije nad raznorodnim izvorima podataka. OOB dolaze tri osnovna provajdera, ka objektima, odnosno kolekcijama, ka SQL serveru i ka XML-u. Iako se izvori podataka razlikuju, sam API je isti, odnosno, radi se sa extension metodima koji kao parametre primaju lambda izraze.
Kao što smo videli u prošlom izdanju ovog bloga, lambda izrazi se mogu predstaviti na dva načina, kao skraćena sintaksa delegata ili kao expression trees. Prvi način je veoma doar kada se radi sa kolekcijama podataka u memoriji, jer se onda delegat prosto Invoke()-uje nad podacima. Međutim, kada govorimo o drugim izvorima podataka, delegati nisu najbolje rešenje, jer podaci možda ne stoje u memoriji, već se nalaze na udaljenim lokacijama.
LINQ to SQL je glavni primer i može da ilustruje ovo o čemu pričam. Podaci se nalze u SQL Serveru, ne u memoriji, Iako za male setove podataka bi možda bilo optimalno da se "dovuku" u memoriju, to nije uvek slučaj, a generički API kao što je LINQ mora da uključi podršku za sve slučajeve, što znači i za slučajeve setova podataka koji su veoma glomazni. U tom slučaju, preslikavanje podataka u memoriju bi predstavljalo veliki overhead, te LINQ to SQL ne radi tako. On rabi drugu formu u kojoj mogu da se predstsave lambda izrazi da bi kasnije mogao da ih parsira, sačini SQL upit i prosledi ga SQL Serveru. Taj "trenutak istine" je, kao što već znate, trenutak iteracije nad rezultatom.
Kako izgledaju i kako se koriste Expression trees?
Donji dijagram predstavlja grafićki prikaz jednog drveta.
.png)
Generalno, čak i da ne želite da pišete sopstvene LINQ provajdere, postoje trenuci kada vam expression trees mogu biti od koristi. Činjenica da bilo koji lambda izraz (dakle, ultimativno, bilo koji delegat), možemo da "ostavimo" po strani da izvršimo kasnije, sigurno je veoma moćna stvar.
Osim putem LINQ to SQL provajdera, možete i "ručno" praviti expression trees. Recimo, ukoliko imamo sledeći lambda izraz:
mi njega možemo da predstavimo kao expression tree koristeći klase iz System.Linq.Expressions namespace-a:
1: using System.Linq.Exressions;
2:
3: Expression<Func<int, int>> f = x => x + 1;
4: Console.WriteLine(f);
5: var fc = f.Compile();
6: Console.WriteLine(fc(2));
Prvi output će biti ToString() reprezentacija lambda izraza, odnosno, njegovo telo (po default-u). Drugi izlaz će biti rezultat pozivanja funkcije. Primetite .Compile() metod na promenljivoj f. To je metod koji parsira expression tree i pretvara ga u IL, odnosno u nešto što može da se izvrši.
Dakle, expression tree možemo da kreiramo prosto tako što ćemo lambda izraz dodeliti tipu Expression, dok ćemo ga generički odrediti tipom delegata. Isto tako, možemo praviti expression trees kroz kod, deo po deo, dok ne dobijemo konačan izraz. Gornji primer se može onda prepisati i kao:
1: Expression leftParam = Expression.Parameter(typeof(int), "x");
2: Expression rightParam = Expression.Constant(1);
3: BinaryExpression AddIt = Expression.Add(leftParam, rightParam);
4: var lambda = Expression.Lambda<Func<int,int>>(AddIt, new ParameterExpression[] { (ParameterExpression)leftParam });
5: var fec = lambda.Compile();
6: Console.WriteLine(fec(4));
Ovo je svakako trivijalan primer, ali nije potrebno da opeterećujem sa kompleksnijim, jer sam siguran da će pažljivi čitaoci videti veliku vrednost i iz ovako jednog trivijalnog primera. Expression trees dotiču pomalo metaprogramiranje, ali na jedan suptilan i polagan način, tako da možete da radite dinamičku generaciju koda bez da se petljate sa Reflection-om ili CodeDOM-om, koji su daleko komplikovaniji.
Out.
B.D.
[1] TIMTOWTDI, "timtoadi" kako se izgovara, je Perl akronim koji je srž čitave njegove filozofije, i glasi "There is more than one way to do it"