Technisch artikel

LAMBDA en LET in Delphi: HotXLS-formulesluitingen

HotXLS evalueert Excel LAMBDA als een echte eersteklas functiewaarde. Een gedefinieerde naam waarvan de RefersTo-tekst een LAMBDA is, kan bij naam aangeroepen worden als =MyFunc(5), een sluiting gebonden binnen LET kan aangeroepen worden als =LET(f, LAMBDA(x, x*2), f(21)), en de lexicale omgeving die bij definitie is vastgelegd, reist mee met de sluiting. De formuletekst gaat ongewijzigd door een laad- en opslagronde

Dit is de functie die een formule-engine onderscheidt van een formuleparser. Alles vóór LAMBDA kon geëvalueerd worden door een boom van waarden te doorlopen. LAMBDA vereist een scope-stack, en zodra u een scope-stack heeft, begint een hele klasse door de gebruiker geschreven spreadsheetlogica in uw Delphi-applicatie te werken in plaats van alleen in Excel

Waarom blijven de meeste niet-Excel-engines steken bij het LAMBDA-sleutelwoord?

Omdat een klassieke spreadsheetevaluator precies één soort waarde kent: een getal, een string, een boolean, een fout, of een verwijzing naar cellen die die waarden bevatten. Er is nergens plaats voor een functie. Toen Excel 365 LAMBDA introduceerde, voegde het een waardetype toe dat parameternamen, een body-expressie en de bindingen draagt die zichtbaar waren op de plek waar het geschreven werd. Een engine zonder dat type kan LAMBDA(x, x*2) parseren en de tekst opslaan, maar zodra een cel probeert het aan te roepen, is er niets om aan te roepen

HotXLS implementeert het ontbrekende stuk als een sluitingswaarde plus een scope-stack tijdens runtime. Een sluiting aanroepen duwt eerst haar vastgelegde omgeving op de stack, duwt dan de argumentwaarden onder de parameternamen, evalueert de body, en snoeit de stack terug tot de markering. Die volgorde is van belang, en de volgende sectie legt uit waarom

De drie manieren waarop een LAMBDA aangeroepen wordt

HotXLS lost een aanroep van een onbekende functienaam op via drie paden, in volgorde geprobeerd, en weten welke afvuurt, verklaart de meeste verrassingen. Ten eerste, een naam gebonden in de huidige LET- of LAMBDA-scope: als f een lokale binding is die een sluiting bevat, past f(21) die toe. Ten tweede, een gedefinieerde naam in het werkboek waarvan de formuletekst begint met LAMBDA: MyFunc(5) compileert de body van die naam en past hem toe. Ten derde, de klassieke user-function-handler, ongewijzigd, voor alles wat de eerste twee paden niet opeisen

Een lokale binding die iets anders bevat dan een sluiting, is niet aanroepbaar. Bind f aan het getal 3 en schrijf dan f(21) en u krijgt een waardefout, geen poging tot vermenigvuldiging. Dit is strenger dan een dynamische taal zou zijn, en met opzet: een spelfout die een functieaanroep in een toevallige verwijzing verandert, is een stille verkeerde uitkomst, en dat is de slechtste uitkomst die een spreadsheetengine kan produceren

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
begin
  Book := TXLSXWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Model');

    // Een herbruikbare benoemde functie, werkboekscope
    Book.DefinedNames.Add('NetOf', 'LAMBDA(amount, rate, amount*(1-rate))');

    Sheet.Cells[2, 2].Formula := 'NetOf(1250, 0.19)';

    // Een sluiting gebonden en toegepast binnen één formule
    Sheet.Cells[3, 2].Formula := 'LET(double, LAMBDA(x, x*2), double(21))';

    // Geneste LET: elke binding is zichtbaar voor de bindingen erna
    Sheet.Cells[4, 2].Formula :=
      'LET(base, 100, bump, LAMBDA(v, v+base), LET(step, bump(5), step*2))';

    Book.Recalculate;
    Book.SaveAs('lambda-model.xlsx');
  finally
    Book.Free;
  end;
end;

Hoe wordt shadowing opgelost wanneer namen botsen?

Parameters winnen. Wanneer HotXLS een sluiting toepast, duwt het eerst de vastgelegde lexicale omgeving en dan de argumentbindingen, zodat een parameter genaamd rate een buitenste binding genaamd rate overschaduwt, en ook een gelijknamige kolomverwijzing in de omringende formule overschaduwt. Die volgorde is wat een benoemde functie veilig maakt om te hergebruiken: de aanroeper kan niet per ongeluk veranderen wat de body betekent door een gelijknamige binding in scope te hebben

Ariteit wordt gecontroleerd voordat er iets geëvalueerd wordt. Een aanroep waarvan het aantal argumenten niet overeenkomt met het aantal parameters van de sluiting, geeft onmiddellijk een waardefout terug, in plaats van eerst enkele argumenten te evalueren en dan pas te falen, wat evaluatie zonder neveneffecten werkelijk vrij van gedeeltelijk werk houdt. De scope-stack wordt teruggesnoeid tot haar startmarkering in een finally-blok, zodat een fout binnen een body geen verouderde bindingen zichtbaar kan laten voor de volgende formule

var
  Book: TXLSXWorkbook;
  Name: TXLSXDefinedName;
begin
  Book := TXLSXWorkbook.Create;
  try
    if Book.Open('customer-model.xlsx') = 1 then
    begin
      // Inspecteer wat de gebruiker schreef voordat u een herberekening vertrouwt
      Name := Book.DefinedNames.FindByName('NetOf');
      if (Name <> nil) and
         (UpperCase(Copy(Name.Formula, 1, 6)) = 'LAMBDA') then
        Log('Named lambda found: ' + Name.Formula);

      Book.Recalculate;
      Log(VarToStr(Book.Sheets[1].Cells[2, 2].Value));
    end;
  finally
    Book.Free;
  end;
end;

LET is niet langer onvolledig

Eerdere HotXLS-releases implementeerden LET slechts ver genoeg om het gangbare geval van één binding af te handelen. De huidige implementatie is compleet: elke binding is zichtbaar voor alle latere bindingen en voor de body-expressie, en geneste LET stelt zich normaal samen, zodat LET(a, 1, b, a+1, LET(c, b*2, c)) evalueert zoals Excel het evalueert

Die volledigheid is belangrijker dan ze klinkt. LET is hoe gebruikers vermijden dezelfde subexpressie vijf keer in één formule te herberekenen, dus echte werkboeken gebruiken het precies in de diep geneste vormen waar een gedeeltelijke implementatie het mis heeft. Werkte u eerder rond hiaten heen door LET-bindingen vóór evaluatie uit te schrijven, dan kan die omweg vervallen

Komma of puntkomma: allebei, nu

Formuletekst in HotXLS accepteert nu de komma als argumentscheidingsteken naast de klassieke puntkomma. Dit is geen locale-instelling; het is een acceptatieregel in de parser. Het is van belang omdat formules vandaan komen van plekken die u niet beheert: geplakt uit een supportticket, gekopieerd uit documentatie, gegenereerd door een script dat Excels canonieke syntax uitspuwde, geïmporteerd uit een CSV met formulestrings

Het praktische effect is dat zowel SUM(A1,A2) als SUM(A1;A2) compileert. Round-tripping behoudt wat de bron ook maar gebruikte, dus een werkboek dat u laadde, wordt teruggeschreven met zijn originele scheidingstekens in plaats van achter de rug van de gebruiker om genormaliseerd

Wat behouden blijft, en wat u moet controleren

De formuletekst wordt letterlijk opgeslagen, dus een LAMBDA in een gedefinieerde naam overleeft een laad- en opslagronde intact en opent in Excel als dezelfde functie. Een kale LAMBDA opgeslagen als celresultaat, wat een formule betekent die evalueert tot een sluiting in plaats van tot een waarde, behoudt het bestaande sla-over-zonder-waarde-gedrag: de tekst blijft bewaard, er wordt geen gecacht numeriek resultaat voor verzonnen. Dat is de eerlijke uitkomst, want er is geen scalair om te cachen

Twee gewoonten zijn de moeite waard om aan te nemen. Geef benoemde lambda's werkboekscope tenzij er een reden is om dat niet te doen, want een sheet-scope-functie die verdwijnt wanneer een sheet gekopieerd wordt, produceert een naamfout op een plek ver van de oorzaak; de scoperegels staan in gedefinieerde namen en formules over meerdere sheets. En wanneer een werkboek vol benoemde lambda's bestemd is voor een rapport dat stabiel moet zijn, overweeg dan om de resultaten te bevriezen met ConvertFormulasToValues zodat downstream-consumenten getallen zien in plaats van functies die ze mogelijk niet ondersteunen

Voor zware herberekening zijn LAMBDA-bodies gewone expressies in de afhankelijkheidsgraaf en worden ze ingepland als elke andere formule, wat beschreven wordt in incrementele herberekening en de afhankelijkheidsgraaf. Roept uw model één benoemde functie aan over duizenden rijen, dan zitten de kosten in de body, niet in de aanroepmachinerie, en hetzelfde optimalisatieadvies geldt als voor elke herhaalde formule

HotXLS is een native Delphi- en C++Builder-spreadsheetcomponent die XLS, XLSX en ODS leest en schrijft zonder Excel of enige Office-automatisering. De formule-engine, gedefinieerde namen en herberekenings-API staan gedocumenteerd op de HotXLS Delphi spreadsheetcomponentpagina