C# 3.0: Lambdaudtryk

C# 3.0: Lambdaudtryk

  • Comments 4

Lambdaudtryk (λ) er formentlig den mest interessante nyhed i C# 3.0, eller i det hele taget på .NET 3.5. Lambdaudtryk er kendt fra funktionelle programmeringssprog som LISP, Scheme, Haskell, ML og F# som nu bliver en del af .NET. Det er super interessant at funktionelle og imperative programmeringsparadigmer møder hinanden. Formålet med at bringe lambdaudtryk ind i C# har i udgangspunktet været at løse "impedance mismatch" problemet. Impedance mismatch problemet er kort sagt problemet med, at vi i dag har mange forskellige typesystemer og representationer af data (f.eks. relationelle data, XML, objekter). Interaktionen mellem typesystemerne er besværligt og fejlbehæftiget (se denne video med Anders Hejlsberg).

Endnu mere interessant bliver det, på længere sigt, at integrationen af funktionelle principper vil blive en programmeringsmodel for multi-core/CPU programmering (se denne video med Anders Hejlsberg og Joe Duffy om PLINQ/PFX). Det betyder at lambdarfunktioner er noget vi kommer til at se meget mere til ;-)

Hvad er lambdaudtryk: lambdaudtryk er "bare" syntaktisk sukker over anonyme metoder. Anonyme metoder blev implementeret i C# 2.0. Hvis du forstår anonyme metoder og delegates, så er du så godt som i mål med lambda'er:-)

For de af jer der ikke har arbejdet med anonyme metoder, hvilket der er rigtig mange der ikke har endnu, så er her et lille eksempel på en anonym metode:

public
delegate int ADelegate(int a, int b);
class Lambdas
{
    public int Add(int a, int b)
    { return a + b; }

    public void Exec()
    {
        //den gode gamle delegate
        ADelegate del = Add;
        //kald af delegate
        int result = del(40, 2);
        //definer ny anonym metode - så kan den deklarerede Add metode undværes.
        del = delegate(int a, int b) { return a + b; };
        //kald den nye anonyme metode
        result = del(19, 2);
     }
}
Jeg bruger altså Add delegaten til at lægge to tal sammen. Efterfølgende laver jeg en anonym metode, hvor jeg genbruger del variablen som er af typen ADelegate.

Selve den anonyme metode kan omskrives til en lambda:

//en "gammel" C# 2.0 anonym metode
del = delegate(int a, int b) { return a + b; };
//lambda med eksplicitte parametre, samt et body med {} og return
del = (int a, int b) => { return a + b; };
//typerne er udledes gennem inferens (del er en ADelegate<int>),
//return og {} kan undlades da der kun er et statement.
del = (a, b) => a + b;


I ovenstående kan du se hvordan de forskellige literaler/parametre mapper mellem lambdaen og den anonyme metode. Input er på venstresiden af => og metodekroppen på højreside. Du kan, som i en anonym metode, skrive så mange linjer i en lambdafunktion som du har lyst til. Ved mere en et statement, skal du bruge start og slut { }, samt eksplicit returnere.

Med C# 2.0 kom også generics. Ovenstående delegate kan ved hjælp af generics omskrives til:

public delegate T ADelegate<T>(T a, T b);

Fordi delegaten er lavet generisk, kan vi også bruge den med andre typer:

ADelegate<string> Concat = (a, b) => a + " " + b;
string myString = Concat("Hello", "World");

I C# 3.0 er en række generiske delegates allerede lavet for dig, som tager op til fire input parametre (T1 til T4) og har en retur parameter (TResult):

public delegate TResult Func<T, TResult>(T arg);
public delegate TResult Func<T1, T2, TResult>(T1 arg1, T2 arg2);       
public delegate TResult Func<T1, T2, T3, TResult>(T1 arg1, T2 arg2, T3 arg3);
public delegate TResult Func<T1, T2, T3, T4, TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4);   

Det vil sige, at jeg kan omskrive Add og Concat metoder til at anvende de predefinerede delegates:

Func<string, string, string> Concat = (a, b) => a + " " + b;
Func<int, int, int> Add = (a, b) => a + b;

Lambdaer kan frit bruges inde fra andre metoder og lambdaer.

Func<int, bool> Even = a => (a % 2) == 0;
Func<int, int, bool> AddResultIsEven = (a, b) => Even(Add(a, b));
var r = AddResultIsEven(2, 2);

Den første lambda i ovenstående er en predikatfunktion som returnerer true eller false (sidste parameter i delegaten Func er en bool). Den sidste lambda anvender både Add og Even lambdafunktionerne.

Lambdafunktioner bruges i C# 3.0 bl.a. til at lave LINQ forespørgsler med. Jeg har tidligere gennemgået extension methods. Kort sagt så er LINQ (to Objects) lavet ved at implementere extension methods på IEnumerable<T>. Where er en extension method på IEnumerable<T>. Nedenstående kodelinje er signaturen til Where predikatet.

public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate);

Parameteren this IEnumerable<TSource> source deklarerer at det er IEnumerable<T> der skal extendes, og anden parameter tager en lambdafunktion af typen Func<TSource, bool>, altså et predikat. Det vil sige, at alle collections der implementerer IEnumerable<T> kan drage nytte af alle LINQ metoderne, hvis man inkluderer System.Linq namespace'et.

Afslutningsvis er her et eksempel på, hvordan man kan filtrere på en liste af stenge ved hjælp af lambdaudtryk:

List<string> l = new List<string> {
    "Hello", "World",
    "Henrik", "Hansen",
    "And", "Some", "More" };
string search = "H";
Func<string,bool> pred = a => a.StartsWith(search);
var query = l.Where(a => pred(a));
foreach (var item in query)
    Console.WriteLine(item);

  • Hej Henrik,

    God introduktion til lambda expressions, dog savner jeg lidt opklaring omkring den her udmelding:

    "Formålet med at bringe lambdaudtryk ind i C# har i udgangspunktet været at løse "impedance mismatch" problemet"

    Hvordan løser lambda expression impedance mishmatch problemet og i hvilken kontekst? (er det imellem den objektorienterede model og den relationelle?)

  • Hej Jakob

    Måske skulle udmeldingen nærmere have været:

    "Formålet med at bringe LINQ ind i C# har i udgangspunktet været at løse "impedance mismatch" problemet".

    Men LINQ-forespørgsler omsættes til lambdaudtryk - så i bund og grund er det lambdaudtryk der er svaret på vores bønner. I og med at lambdaudtryk kan udtrykke som expressiontrees, kan et expressiontree "omsættes" til en vilkårlig forespørgsel (mod objekter, dataset, databaser, xmlfiler etc).

    Jeg er med på at jeg linkede til objekt vs relationeldata impedance mismatch på wikipedia, hvis det var det der forvirrede dig:-) Det var umiddelbart det bedste jeg kunne finde på impedance mismatch problemet. Men problemet gør sig gældende mellem forskellige typesystemer og repræsentationer.

    Tak for din kommentar (jeg tror godt nok du allerede kendte svaret ;-) )

    /Henrik

  • Hej Henrik,

    Det giver mere mening omend jeg er af den overbevisning at impedance mismatch problemet ikke kan løses, man kan højst minimere størrelsen af kløften.

    Bare fordi vi har sprogkonstruktioner til at udtrykke queries, der efterfølgende kan blive oversat til SQL har vi stadig en kløft i form af:

    1. Vedligeholdelse af 2 modeller

    Vi har stadig 2 modeller vores domæne og vores data, ja de vil ofte være 1:1 men ikke altid. Især ikke hvis vi snakker løsninger vor den samme datamodel bruges til flere forskellige domænemodeller der evt. kan basere sig på forskellige datamodeller.

    En anden ting er performance, til tider kan strukturering af store datamængder kræve at datamodellen ikke er direkte 1:1 med domænet, det kan være tekniske begrænsninger(f.eks. begrænsning på row size i DBMS) eller blot optimeringer der er skyld i det.

    2. Mapping

    Netop fordi vi har 2 forskellige modeller skal vi mappe imellem vores dem vha enten attribut- eller fil-baseret mapping. Vi kan hjælpes godt på vej af forskellig værktøjer men stadig kan de ikke tage højde for alt så der er stadig noget manuel mapping og især i forbindelse med refacoring kan der her snige sig fejl ind som ikke kan findes på compile-time.

    3. Oversættelse af funktionalitet

    Vi arbejder i et general purpose programmeringssprog med et rigt bibliotek af funktionalitet, hvis der ikke skulle være noget mismatch ville jeg forlange jeg kunne skrive:

    var query = from c in db.Customers where c.CompanyName.StartsWith("C", StringComparison.InvariantCultureIgnoreCase) select c;

    det kan jeg ikke, det vil give mig en fejl på runtime.

    4. 2 Typesystemer

    Typesystemerne i vores database og vores framework stemmer ikke overens, jeg skal være opmærksom på hvad jeg prøver at putte i database, f.eks. er DateTime.MinValue ikke smart at prøve at gemme i SQL Serveren.

    Så at tale om at impedance mismatch problemet er løst er at stramme den lidt vil jeg påstå, og reelt set er det heller ikke et problem jeg ønsker at løse så længe vi har med 2 så forskellige verdener. Jeg ønsker at være klar over hvornår min database bliver brugt og ikke mindst hvordan den bliver brugt, så hvis vi lukker kløften kommer udviklere til at skyde i blinde og miste muligheden for at optimere/skalere i databasen, det ville være en skam.

    Hvis vi virkerlig vil udover impedance mismatch snakker vi måske nærmere oo-databaser men også her er skal data persisteres til enten ram eller I/O som kan vise sig at være en flaskehals. Og i OO-databaser har vi også problemet med reporting etc som RDBMS er så gode til.

    Måske lyder jeg negativ overfor LINQ, det er ikke tilfældet det er bare min ydmyge holdning at det skal bruges rigtigt og det er vigtigt at vide hvad der sker bag alt det syntaktiske sukker. At sidde uvidende og trække rundt i LINQ to SQL designeren og skrive nogle smarte queries kan blive det værste der nogensinde er sket

  • Hej Jakob

    Rigtig god kommentar. Jeg skriver dog ikke at impedance mismatch problemet er løst. Jeg skriver at formålet har været at løse impedance mismatch problemet. Det kunne mindre firkantet have været formuleret som: formålet har været at reducere impedance mismatch problemet.

    LINQ to SQL eller LINQ to ”relationelle data” generelt er en lidt anden snak end LINQ to objects som jeg bruger som demo.

    Du henviser til 1:1 forhold mellem domæne og data: nogle gange er det tilfældet og andre gange er det ikke. LINQ to SQL er en 1:1 mapping. Der ligger altså ikke nogen abstraktion over skemaet. Med andre ord, det er en ORM (object relational mapper). Med Entity Frameworket er det muligt at lave en abstraktion over skemaet så du kan mappe en ”vilkårlig” RDBMS til en rigere domænemodel. Arbejdet her vil være meget af det arbejde man i dag laver i datalaget. Jeg er heller ikke et øjeblik i tvivl om at uanset hvilken LINQ to ”relationelle data” teknologier man anvender, så vil der være manuelt arbejde involveret – jeg tror ikke det  er en silver bullet. Uanset hvilke teknologi man anvender, så er man nødsaget til at vide hvordan skemaet ser ud, hvordan basen er distribueret fysisk, og hvordan LINQ og SQL opfører sig (der er sikkert mange flere vigtige parametre).

    Jeg må lave en blogpost om LINQ to relationel data. I eksemplerne ovenfor bruger jeg udelukkende LINQ to objects som ikke har impedance mismatch problemet;-) Det var for at kridte banen op. Lambdaer bruges som det ”universelle forespørgselssprog”, da en lambda kan omsættes til et expressiontree og et epressiontree kan omsættes til noget vilkårligt – f.eks.  sql optimeret mod MS SQL, Oracle, DB2, MySQL etc. Funktionelle programmeringssprog er jo bland andet gode til at lave transformationer.  

    Jeg tror problemet med drag & drop programmering altid vil være til stede. Det vil man altid skulle være påpasselig med, eller i hvert fald gøre sig bevidst hvad det er man få ud af drag & drop programmering.

    /Henrik

Page 1 of 1 (4 items)