Vítejte na blog.vyvojar.cz Přihlásit | Registrovat | Pomoc

Vlko napísal ...

.. mostly harmless ...
ExpressionEval Two - the lambda case

Zdrojové kódy: http://vlko.zilina.net/dwn/blog/ExpressionEval.zip

Intermezzo

Zaregistrovaním ExpressionEval do súťaže „UKAŽ SE A VYHRAJ (Vyvíjej open-source s Microsoftem)“ som si vyrobil rest, ktorý hádam týmto blog príspevkom napravím.

Takže čo stojí v popise projektu ExpressionEval?

Popis projektu: Cieľom projektu je serializacia a deserializacia expression stromu do/z textovej podoby. Súčasťou projektu bude aj vzorovy linq provide, ktorú umožní vzdialené vykonávanie linq výrazov (zatiaľ v zjednodušenej forme bez podpory anonymných objektov) cez http protokol. Initial verzia je popísana na http://blog.vyvojar.cz/vlko/archive/2008/12/24/expressioneval-coding-story.aspx

 Je preto čas pokročiť a ponúknuť niečo viac ako popis a initial verziu a trošku sa rozpísať, pretože odteraz sa pod ExpressionEval zastrešujú 3 podprojekty:

  1. ExpressionEval - pomocou ktorého je možno z textového zápisu expression výrazu dostať funkčný expresion výraz, jednoducho ide o spôsob ako deserializovať expression výraz
  2. Evalizer - je zasa naopak spôsob ako z expression výrazu získať jeho textovú podobu, ktorá môže byť následne pomocou ExpressionEval-u prevedená späť na expression výraz
  3. RemoteLinq - toto je hlavný dôvod, prečo vznikli oba dva predchadzájuce projekty a to preniesť zo serveru linq podporu na klienta, opäť zjednodušene povedané, na klientovy používať komfort linq-u, ktorý sa ale reálne bude vykonávať na vzdialenom servere

ExpressionEval verzia Two

Oproti prvotnej verzii sa nám podpora parsera rozšírila a tak k pôvodnýmvlastnostiam:

  • statické funkcie, properties a fieldy
  • funkcie, properties a fieldy parametra
  • string reťazce bez obmedzenia, teda viacriadkové a s úvodzovkami definovanými po C#ovsky: \"
  • numerické konštanty okrem toho že číslo s bodkou je desattinný float a bez bodky Int32 je umožnené definovať typy pomocou končiaceho textu, teda (posledné tri som si vymyslel, možno v budúcnosti pridam automatickú konverziu typov):
    • 12.1 - float
    • 12.1f - float
    • 12.1d - double
    • 12 - Int32
    • 12u - UInt32
    • 12l - Int64
    • 12ul - UInt64
    • 12s - Int16
    • 12us - UInt16
    • 12b - Byte
  • indexery
  • operátory (*,/,%,+,-,>,<.<=,>=,==,!=,|,^,&,||,&&, ...)
  • vytváranie inštancii pomocou new keywordu

pribudli

  • bugfixy chýb predchadzajúcich vlastností
  • podpora záporných čísel (zaujímavé, že si na ne človek spomenie až, keď sa s nimi reálne stretne)
  • negácia pomocou ! operátora
  • konštanty null, true, false
  • lambda výrazy
  • extensions metódy (s tým súvisi nutnosť explicitne zadeklarovať typ pre aký sa majú extensions metódy deklarovať cez ExpressionEval.AddExtension funkciu)

Lambda výrazy a extension metódy

Myslím si, že tieto dve vlastnosti stoja za osobitnú odbočku, pretože implementovať ich bol ten najtvrdší oriešok, pretože v rámci zachovania, čo najvyššej rýchlosti vytvárania Expression stromu nezisťujem cez reflexiu metódu dopredu. Pri lambda výrazoch je toto ale nutné, pretože musíme dopredu vedieť typ parametru, z ktorého následne môžeme ďalej budovať expression tree, takže treba rátať s tým, že lambda výrazy môžu negatívne ovplyvniť rýchlosť.

K tomu pridajme problém s extension metód a nutnosť preskakovať prvý parameter funkcie. Ono extension metódy sú celkom problematická vec pre reflexiu. Vo visual studiu to máme jednoduché, pridáme using namespace-u a prekompiler sa o všetko sám postará. V skutočnosti sú ale namespace len doplnková informácia, ktorú pomocou expression nie je možné jednoducho queryovať. Jedinou možnosťou je proste prebehnúť všetky načítané assembly a v nich všetky typy a až potom je možné nájsť potrebnú extension metódu.

Najlepšie je uviesť príklad, teda pre vyhľadanie všetkých IQueryable<T> rozšírení z System.Linq namespacu

Evaluator.AddExtension("System.Linq", "IQueryable`1");

je potrebné

                // search all assemblies for extension methods

                foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())

                {

                    // choose only methods extensions methods and method with defined type and assembly

                    var query = from type in assembly.GetTypes()

                                // only sealed not generics and not nested types contains extensions method

                                where type.IsSealed && !type.IsGenericType && !type.IsNested

                                // so take only static methods

                                from method in type.GetMethods(BindingFlags.Static

                                    | BindingFlags.Public | BindingFlags.NonPublic)

                                // and finally check ExtensionAttribute (in C# it is this keyword in first parameter)

                                where method.IsDefined(typeof(System.Runtime.CompilerServices.ExtensionAttribute), false)

                                // now compare if type name and namespace is as specified

                                where extensionsToLoad.Any(exInfo =>

                                {

                                    Type paramType = method.GetParameters()[0].ParameterType;

                                    return (paramType.Name == "IQueryable`1")

                                        && (method.DeclaringType.Namespace == "System.Linq");

                                })

                                select method;

                }

Brrr, ide o pekne výpočtovo náročnú operáciu a preto sú všetky resolvnuté typy ukladané do statickej cache, takže na to myslíte pri svojich performance testoch a pridajte viac ako jednu iteráciu:)

Evalizer

To, že úlohou evalizeru je vytvoriť z expression výrazu textový ekvivalent je vám už jasné a osobne som to na začiatku pokladal za tú najjednoduchšiu úlohu, proste sa zavolá Expression.ToString() a je vystarané. 

Akurát, že:

  • čísla sú formatované s v aktuálnej kódovej stránke
  • extension metódy neobsahujú definíciu typu a to ide o statické metódy, ich textová reprezentácia sa ale tvári, že ide o metódy normálne, čo sa celkom dobre číta, ale bohužiaľ zle serializuje
  • typ new a teda NewExpression obsahuje iba meno, nijakú informáciu o namespace, prípadne assembly
  • converzia typov (napr. byte->int) sa proste volá Convert, nikde nie je informácia o tom na aký typ by sa mala konvertnúť
  • a najlepšie nakoniec, ak použijete vonkajšiu premennú, tak tá sa automaticky neresolvne, ale je buď odkazovaná priamo, v prípade privátnej premennej je vytvorená pomocná trieda, ktorá zapuzdruje túto hodnotu

Všetky tieto problémy ale musel Evalizer vyriešiť. V prvých troch bodoch rozpísať serializačnú funkciu pre každý mysliteľný typ Expression a triedu som bol príliš lenivý, použil som fintu a potrebné informácie odpĺňam pomocou Regex-u. Convert zasa nahrádzam patričnou Convert.To<Typ>(hodnota) funkciou, čo na základné typy bohate stačí.

A nakoniec to najzložitejšie a to náhrada zapuzdrených hodnôt. Tá je nazaujímavejšia a funguje tak, že dokiaľ nejde o ExpressionParameter, tak sa snaží compilovať expression, dokiaľ nedostane návratový typ, ktorý je zo namespace System.

A čo je dôležité, všetko krásne funguje a tak

            int x = 3;

            int y = 3;

            double z = 2.2;

            bool boolValue = false;

            string stringValue = "Test";

            Func<int> () => 5 * x // sa evalizuje na (5 * 3)

 

            Func<int> () => 5 * x / y // na ((5 * 3) / 3)

 

            Func<double> () => 5 * x / y * z // na (System.Convert.ToDouble(((5 * 3) / 3)) * 2.2)

 

            Func<bool> () => (stringValue.IndexOf("T") == 0) == boolValue // na ((0 = 0) = False)

pre zaujímavejšie konštrukcie odporúčam unit testy v projekte.

RemoteLinq

Je vyvrcholenie celého projektu a jeho cieľ, aj keď sa zdá, že ide počtom riadkov o jeho nepodstatnú časť. Trošku ma zamrzí, že Microsoft mi pomocou ADO.NET Data Services (project Astoria), vlastne vytvorili niečo veľmi podobné a najmä sofistikovanejšie, ale majú proste k dispozícii viac ľudí, takže je to pre nich o to jednoduchšie, čo už len môže zmôcť jednoduchý slovenský programátor?:) Mňa aspoň môže potešiť, že umožnujem serializovať aj tie extension metódy, pomocou ktorých môžete definovať fetching funkčnosť, prípadne iné špecifické veci, bez ktorých by ste sa neradi zaobišli.

Takže späť k projektu, pretože ten si určite každý bude chcieť vyskúšať a overiť či to o čom je celý tento príspevok nie je len prázdne mlátenie hubou. Aby ste ho teda mohli spustiť a otestovať je potrebné poznať štruktúru projektov, z ktorých sa skladá:

  • RemoteLinqBase - základná knižnica, ktorá obsahuje popis modelu a základné triedy pre server časť a client časť (celý remote linq je len taký nadstrel ako by taky remote linq mal vyzerať)
  • RemoteLinqServer - server časť, ktora pomocou REST starter kitu zprístupňuje cez http protokol dáta (zmienil som sa už že príklad používa REST?)
  • RemoteLinqClient - client časť je jednoduchá konzolová aplikácia, ktorá ukazuje ako pomocou normálnych linq queries ukazuje, že to proste funguje, kto neverí, nech localhost nahradí za fiddler a sám sa presvedčí

Ako RemoteLinq v skutočnosti funguje už normálne nemam po toľkom texte ani chuť ani silu, okrem toho ide o takú oklieštenú verziu implementácie pre LLBLGen na strane servera (LLBLGen by asi priniesol do projektu projektu závislosť na komerčnej knižnici a to ja nerád, kto ale má záujem, vždy sa môžme dohodnúť, v predprodukčnom nasadení to už funguje;).

Nuž v skratke teda:

  • ako som už písal vyššie na komunikáciu na strane servera používam REST starter kit, ktorý zasa využíva WCF
  • používam DataContract serializáciu, s priamo definovanými podporovanými typmi pre serializáciu, takže si môžem odpustiť nutnosť definovať DataContract atribúty na triedov modelu
  • a pretože model na klientovi a servere je rovnaký, nerobí to problém, vy ale na to musíte myslieť
  • z linq funkčnosti mam otestované Where, Order, Count, Skip, Take, teoreticky nie sú problém aj iné, ale to už v nejakom ďalšom release, nápady sú a tieto by mali pre väčšinu prípadov postačovať
  • na strane klienta je použitý jednoduchý linq provider na základe inšpirácie z článku LINQ: Building an IQueryable Provider - Part
  • a na strane servera je IQueryable simulované jednoduchým List<T>.ToQueryable()

Nechcem vás ale obrať o radosť povŕtať sa v zdrojových kódoch a tak sa do toho puste sami, nie je to zatial žiadna sláva, zopár review kód ešte bude musieť prekonať, dôležité ale je, že to funguje, no nie?

Performance

Na tú som prihliadal pri návrhu najviac a tak na prvotny parsing je použitý regex, potom sú výsledky prehnané cez stavový automat, ktorý vytvorí prvotnú stromovú štruktúru na základe ktorej sa vytvorí samotný expression tree. Aj keď som to na začiatku nepredpokladal, nie som sám:

  • od Microsoft je tu Dynamic Query Library, ktorá je súčasťou samples vo visual studiu
  • MetaLinq
  • tie predchádzajúce sa dostali do performance testu, potom tu je napr Mono.Cecil, ale ten je už o troška niečom inom a hlavne asi vyžaduje vyššie právomoci, v prípade ak máte nejaké ďalšie zaujímavé knižnice na porovnanie, dajte vedieť do komentárov

Súčasťou projektu je aj ExpressionEvalPerformanceTest projekt, ktorý je jednoduchá konzolová aplikácia na základne porovnanie výkonnosti. V porovnaní je teda ExpressionEval, DQL a MetaLinq a pre zaujímavosť aj serializácia cez Evalizer a jeho spätný prevod cez ExpressionEval.

Ako si v porovnaní ExpessionEval stojí? Po prvom rune je beznádejne posledný, ale akonáhle je knižnica v pamäti poráža všetkých súperov o hlavu, veď sa presvedčte sami tú sú výsledky (nie su testované lamba, extensions a niektoré ďalšie črty, ktoré ostatným knižniciam proste chýbaju:():

Run number: 0
Normal lambda expression creation finished in 3 ms.
Expression eval creation finished in 150 ms.
Dynamic Library creation finished in 74 ms.
Deserialization finished in 133 ms.
Expression eval through evalizer creation finished in 56 ms.
Run number: 1
Normal lambda expression creation finished in 0 ms.
Expression eval creation finished in 1 ms.
Dynamic Library creation finished in 13 ms.
Deserialization finished in 11 ms.
Expression eval through evalizer creation finished in 14 ms.
Run number: 2
Normal lambda expression creation finished in 0 ms.
Expression eval creation finished in 1 ms.
Dynamic Library creation finished in 16 ms.
Deserialization finished in 12 ms.
Expression eval through evalizer creation finished in 12 ms.
Run number: 3
Normal lambda expression creation finished in 0 ms.
Expression eval creation finished in 1 ms.
Dynamic Library creation finished in 11 ms.
Deserialization finished in 11 ms.
Expression eval through evalizer creation finished in 11 ms.
Myslím si, že výsledky netreba nijak komentovať.

TODO

A úplne nakoniec, čo nás čaká v najbližšej budúcnosti

  • vybratie licencie (tej najmenej reštriktívnej, takže sa nemusíte báť už teraz použiť knižnicu niekde v svojich projektoch)
  • uverejniť projekt na codeplexe, aby som splnil podmienky súťaže, bohužiaľ sa mi do anglického popisu dvakrát nechce
  • podpora pre anonymous triedy
  • vytvoriť RemoteLinq pre NHibernate, tak aby mal čo najviac blížil plnohodnotnému linq provideru

Možno súťaž nevyhrám, najmä keď pozerám na konkurenciu a hlavne neviem či sa nájde niekto, kto okrem mňa tento projekt aj reálne použije, ale išlo o hádam tu najzábavnejšiu prácičku v mojom poslednom období (najmä keď na začiatku som vôbec nedúfal, že je takéto niečo možné, ešteže tu máme agile) a tak nemám srdce nechať projekt hniť na jednom mieste, keď stovky betatesterov sa nemôže dočkať až ma zaplavia kopou chýb;)

Posted: Tuesday, April 28, 2009 3:40 PM by vlko
Vedeno pod: ,

Komentář

steelspace napsal:

Ano, to je zajímavá knihovna. Sám jsem ji použil v reálném line-of-business projektu. Konkrétně se jednalo o definici podmínek, za kterých se má odeslat email. Podmníka se definovala jako c# výraz, který se pak pomocí této knihovny "spustil" (vyhodnotil).

Tak jsme dosáhly obrovské flexibility bez nutnosti implementovat na definování podmínek nějaké složité UI.

# April 29, 2009 1:14 PM

vlko napsal:

Podobne ExpessionEval pouzivam aj ja, teda okrem toho linq serializera.

Z textu nevidiet ktoru konretne kniznicu pouzivate, ale nepredpokladam, ze moju:)

# April 29, 2009 10:33 PM

Stu Cam napsal:

Do you have this blog post in English?

# May 6, 2009 1:36 AM

vlko napsal:

to Stu Cam: no, but you can use google translator to have a clue what is inside: http://translate.google.com/translate?prev=hp&hl=en&js=n&u=http%3A%2F%2Fblog.vyvojar.cz%2Fvlko%2Farchive%2F2009%2F04%2F28%2Fexpressioneval-two-the-lambda-case.aspx&sl=sk&tl=en

and btw: I plan to move project to codeplex, so be patient:)

# May 7, 2009 9:41 AM

matej napsal:

Na každýho dojde :-) Poprvé jsem něco podobného ExpressionEval napsal před 15 lety v semestrálce (C++), aby mi to kreslilo graf zadané funkce. Podruhé zhruba před 10 v PowerBuilderu - tam to testovalo správnost vzorce pro výpočet množství přísady s účinnou látkou  pro výrobu léku. No a naposled hodně zjednodušeně před necelými 5 lety v C#, pro zadávání rozhodovací podmínky ve workflow (asi si to vyfrknu za letos znova, abych zachoval kontinuitu :-).

Existuje vůbec programátor, který se takto nesnažil vyřešit neexistenci příkazu DWIM?

# May 13, 2009 10:22 PM

Lakeisha napsal:

AKAIK you've got the asnewr in one!

# May 29, 2011 6:54 AM

zixbnteesz napsal:

# May 29, 2011 1:59 PM

akzqomzy napsal:

1DYI9x , [url=http://urzmppurkspk.com/]urzmppurkspk[/url], [link=http://kyngtnxwsxyx.com/]kyngtnxwsxyx[/link], http://jugivyzgntvk.com/

# May 30, 2011 7:59 PM

xfmrbvixpfy napsal:

# May 31, 2011 9:32 AM

wttjjdl napsal:

tCmMgM , [url=http://mhdoelfqyvji.com/]mhdoelfqyvji[/url], [link=http://tmzocmibcicm.com/]tmzocmibcicm[/link], http://dolerenawdfp.com/

# June 1, 2011 6:53 PM
Vytvoření nového komentáře

(povinný) 

(povinný) 

(nepovinný)

(povinný) 

Opiš čísla, která vidíš na obrázku:

Upozornění na nové komentáře

Pokud chčeš dostávat upozornění emailem na změny u toho příspěvku,tak se zaregistruj zde.zde

Odebírat komentáře k tomuto příspěvku pomocí RSS