Articol tehnic

LAMBDA și LET în Delphi: closures de formulă HotXLS

HotXLS evaluează LAMBDA din Excel ca o valoare funcție autentică, de prim rang. Un nume definit al cărui text RefersTo este un LAMBDA poate fi apelat după nume ca =MyFunc(5), o closure legată în interiorul unui LET poate fi apelată ca =LET(f, LAMBDA(x, x*2), f(21)), iar mediul lexical capturat la momentul definirii călătorește odată cu closure-ul. Textul formulei se întoarce nemodificat în registrul de lucru

Aceasta este funcționalitatea care separă un motor de formule de un parser de formule. Tot ce era înainte de LAMBDA putea fi evaluat parcurgând un arbore de valori. LAMBDA cere o stivă de scop, iar odată ce ai o stivă de scop, o întreagă clasă de logică de foaie de calcul scrisă de utilizator începe să funcționeze în aplicația ta Delphi, nu doar în Excel

De ce se opresc majoritatea motoarelor non-Excel la cuvântul cheie LAMBDA?

Pentru că un evaluator clasic de foaie de calcul are exact un singur tip de valoare: un număr, un string, un boolean, o eroare, sau o referință la celule care conțin acestea. Nu există unde să pui o funcție. Când Excel 365 a introdus LAMBDA, a adăugat un tip de valoare care poartă nume de parametri, o expresie corp, și legăturile vizibile acolo unde a fost scrisă. Un motor fără acel tip poate analiza LAMBDA(x, x*2) și stoca textul, dar în momentul în care o celulă încearcă să îl apeleze, nu este nimic de apelat

HotXLS implementează piesa lipsă ca o valoare closure plus o stivă de scop la runtime. Apelarea unei closure împinge mediul ei capturat, apoi împinge valorile argumentelor sub numele parametrilor, evaluează corpul, și trunchiază stiva înapoi la marcaj. Această ordine contează, iar secțiunea următoare explică de ce

Cele trei moduri în care un LAMBDA este apelat

HotXLS rezolvă un apel către un nume de funcție necunoscut prin trei trasee, încercate în ordine, iar a ști care se declanșează explică majoritatea surprizelor. Primul, un nume legat în scopul LET sau LAMBDA curent: dacă f este o legătură locală care conține o closure, f(21) o aplică. Al doilea, un nume definit la nivel de registru de lucru al cărui text de formulă începe cu LAMBDA: MyFunc(5) compilează corpul acelui nume și îl aplică. Al treilea, handler-ul clasic de funcție de utilizator, neschimbat, pentru tot ce primele două trasee nu revendică

O legătură locală care conține altceva decât o closure nu este apelabilă. Leagă f la numărul 3 și apoi scrie f(21) și obții o eroare de valoare, nu o încercare de înmulțire. Aceasta este mai strict decât ar fi un limbaj dinamic, și deliberat așa: o greșeală de tastare care transformă un apel de funcție într-o referință accidentală este un răspuns greșit silențios, ceea ce este cel mai rău rezultat pe care îl poate produce un motor de foaie de calcul

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

    // O funcție numită reutilizabilă, scop de registru de lucru
    Book.DefinedNames.Add('NetOf', 'LAMBDA(amount, rate, amount*(1-rate))');

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

    // O closure legată și aplicată în interiorul unei formule
    Sheet.Cells[3, 2].Formula := 'LET(double, LAMBDA(x, x*2), double(21))';

    // LET imbricat: fiecare legătură este vizibilă celor de după ea
    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;

Cum se rezolvă shadowing-ul când numele se ciocnesc?

Parametrii câștigă. Când HotXLS aplică o closure, împinge mai întâi mediul lexical capturat și legăturile de argumente al doilea, așa că un parametru numit rate umbrește o legătură exterioară numită rate și de asemenea umbrește o referință de coloană cu aceeași grafie din formula din jur. Această ordine este ce face o funcție numită sigură de reutilizat: apelantul nu poate schimba accidental ce înseamnă corpul având o legătură cu nume similar în scop

Aritatea este verificată înainte ca orice să fie evaluat. Un apel al cărui număr de argumente nu se potrivește cu numărul de parametri al closure-ului returnează imediat o eroare de valoare, în loc să evalueze niște argumente și apoi să eșueze, ceea ce menține evaluarea fără efecte secundare cu adevărat liberă de muncă parțială. Stiva de scop este trunchiată înapoi la marcajul ei de intrare într-un bloc finally, astfel încât o eroare în interiorul unui corp nu poate lăsa legături învechite vizibile pentru următoarea formulă

var
  Book: TXLSXWorkbook;
  Name: TXLSXDefinedName;
begin
  Book := TXLSXWorkbook.Create;
  try
    if Book.Open('customer-model.xlsx') = 1 then
    begin
      // Inspectează ce a autorizat utilizatorul înainte de a avea încredere într-o recalculare
      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 nu mai este parțial

Versiunile anterioare HotXLS implementau LET doar suficient pentru a gestiona cazul comun cu o singură legătură. Implementarea curentă este completă: fiecare legătură este vizibilă tuturor legăturilor ulterioare și expresiei corp, iar LET imbricat se compune normal, așa că LET(a, 1, b, a+1, LET(c, b*2, c)) se evaluează la fel cum îl evaluează Excel

Această completitudine contează mai mult decât pare. LET este modul în care utilizatorii evită recalcularea aceleiași subexpresii de cinci ori într-o formulă, așa că registrele de lucru reale îl folosesc exact în acele forme profund imbricate pe care o implementare parțială le greșește. Dacă anterior ocoleai golurile prin expandarea legăturilor LET înainte de evaluare, acea soluție temporară poate dispărea

Virgulă sau punct și virgulă: acum, amândouă

Textul formulei în HotXLS acceptă acum virgula ca separator de argument alături de clasicul punct și virgulă. Nu este o setare de locale; este o regulă de acceptare în parser. Contează pentru că formulele sosesc din locuri pe care nu le controlezi: lipite dintr-un tichet de suport, copiate din documentație, generate de un script care a emis sintaxa canonică a Excel, importate dintr-un CSV de string-uri de formulă

Efectul practic este că SUM(A1,A2) și SUM(A1;A2) se compilează amândouă. Round-trip-ul păstrează orice a folosit sursa, așa că un registru de lucru pe care l-ai încărcat este scris înapoi cu separatorii lui originali, nu normalizat pe la spatele utilizatorului

Ce se întoarce nemodificat, și ce să verifici

Textul formulei este stocat nemodificat, așa că un LAMBDA într-un nume definit supraviețuiește unui ciclu de încărcare și salvare intact și se deschide în Excel ca aceeași funcție. Un LAMBDA gol stocat ca rezultat de celulă, adică o formulă care se evaluează la o closure, nu la o valoare, păstrează comportamentul existent de sărire fără valoare: textul este păstrat, niciun rezultat numeric în cache nu este inventat pentru el. Acesta este rezultatul onest, întrucât nu există niciun scalar de pus în cache

Merită adoptate două obiceiuri. Dă lambda-urilor numite scop de registru de lucru, exceptând cazul în care există un motiv contrar, pentru că o funcție cu scop de foaie care dispare când o foaie este copiată produce o eroare de nume într-un loc departe de cauză; regulile de scop sunt acoperite în nume definite și formule între foi. Și când un registru de lucru plin de lambda-uri numite este destinat unui raport care trebuie să fie stabil, ia în calcul înghețarea rezultatelor cu ConvertFormulasToValues, astfel încât consumatorii din aval să vadă numere, nu funcții pe care poate nu le suportă

Pentru recalculare grea, corpurile LAMBDA sunt expresii obișnuite în graful de dependențe și sunt programate ca orice altă formulă, ceea ce este descris în recalcularea incrementală și graful de dependențe. Dacă modelul tău apelează o funcție numită pe mii de rânduri, costul este corpul, nu mecanismul de apel, iar același sfat de optimizare se aplică ca pentru orice formulă repetată

HotXLS este o componentă de foaie de calcul nativă pentru Delphi și C++Builder care citește și scrie XLS, XLSX și ODS fără Excel sau vreo automatizare Office. Motorul de formule, numele definite și API-ul de recalculare sunt documentate pe pagina componentei HotXLS Delphi spreadsheet