Odborný článok

LAMBDA a LET v Delphi: uzávery vzorcov HotXLS

HotXLS vyhodnocuje Excel LAMBDA ako skutočnú funkčnú hodnotu prvej triedy. Definovaný názov, ktorého text RefersTo je LAMBDA, sa dá volať podľa mena ako =MyFunc(5), uzáver naviazaný vnútri LET sa dá volať ako =LET(f, LAMBDA(x, x*2), f(21)), a lexikálne prostredie zachytené v čase definície cestuje spolu s uzáverom. Text vzorca sa do zošita zapíše a znovu načíta bezo zmeny

Toto je funkcia, ktorá oddeľuje formulový engine od formulového parsera. Všetko pred LAMBDA sa dalo vyhodnotiť prechádzaním stromu hodnôt. LAMBDA vyžaduje zásobník rozsahov (scope stack), a keď už raz máte zásobník rozsahov, celá trieda logiky tabuliek napísanej používateľmi začne fungovať vo vašej Delphi aplikácii namiesto iba v Exceli

Prečo väčšina enginov mimo Excelu zastane pri kľúčovom slove LAMBDA?

Pretože klasický vyhodnocovač tabuliek má presne jeden druh hodnoty: číslo, reťazec, boolovskú hodnotu, chybu, alebo referenciu na bunky, ktoré tieto hodnoty nesú. Niet kam uložiť funkciu. Keď Excel 365 zaviedol LAMBDA, pridal typ hodnoty, ktorý nesie mená parametrov, výraz tela a väzby viditeľné tam, kde bola napísaná. Engine bez tohto typu dokáže naparsovať LAMBDA(x, x*2) a uložiť text, ale vo chvíli, keď sa ju bunka pokúsi zavolať, niet čo volať

HotXLS implementuje chýbajúci kúsok ako hodnotu uzáveru plus zásobník rozsahov za behu. Volanie uzáveru najprv vloží na zásobník jeho zachytené prostredie, potom vloží hodnoty argumentov pod menami parametrov, vyhodnotí telo a zásobník skráti späť na značku. Na tomto poradí záleží, a prečo, vysvetľuje nasledujúca časť

Tri spôsoby, akými sa LAMBDA volá

HotXLS rieši volanie neznámeho názvu funkcie cez tri cesty, vyskúšané v poradí, a vedieť, ktorá z nich zaberie, vysvetľuje väčšinu prekvapení. Po prvé, meno naviazané v aktuálnom rozsahu LET alebo LAMBDA: ak f je lokálna väzba nesúca uzáver, f(21) ho aplikuje. Po druhé, definovaný názov zošita, ktorého text vzorca začína LAMBDA: MyFunc(5) skompiluje telo tohto názvu a aplikuje ho. Po tretie, klasický handler používateľských funkcií, nezmenený, pre všetko, čo si prvé dve cesty nenárokujú

Lokálna väzba, ktorá nesie niečo iné než uzáver, sa nedá volať. Naviažte f na číslo 3 a potom napíšte f(21) a dostanete chybu hodnoty, nie pokus o násobenie. Toto je prísnejšie, než by bol dynamický jazyk, a zámerne: preklep, ktorý zmení volanie funkcie na náhodnú referenciu, je tichá nesprávna odpoveď — a to je najhorší možný výsledok, aký môže formulový engine vyprodukovať

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

    // Znovupoužiteľná pomenovaná funkcia, rozsah zošita
    Book.DefinedNames.Add('NetOf', 'LAMBDA(amount, rate, amount*(1-rate))');

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

    // Uzáver naviazaný a aplikovaný vnútri jedného vzorca
    Sheet.Cells[3, 2].Formula := 'LET(double, LAMBDA(x, x*2), double(21))';

    // Vnorené LET: každá väzba je viditeľná pre tie, čo nasledujú po nej
    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;

Ako sa rieši tienenie, keď sa mená zrazia?

Vyhrávajú parametre. Keď HotXLS aplikuje uzáver, vloží na zásobník najprv zachytené lexikálne prostredie a až potom väzby argumentov, takže parameter s menom rate zatieni vonkajšiu väzbu s menom rate a zatieni aj rovnako pomenovanú referenciu na stĺpec v okolitom vzorci. Práve toto poradie robí pomenovanú funkciu bezpečnou na opätovné použitie: volajúci nemôže náhodne zmeniť význam tela tým, že by mal v rozsahu podobne pomenovanú väzbu

Arita sa kontroluje skôr, než sa čokoľvek vyhodnotí. Volanie, ktorého počet argumentov nesúhlasí s počtom parametrov uzáveru, okamžite vráti chybu hodnoty namiesto toho, aby vyhodnotilo niektoré argumenty a potom zlyhalo — čo drží vyhodnocovanie bez vedľajších účinkov naozaj bez čiastočnej práce. Zásobník rozsahov sa skráti späť na svoju vstupnú značku v bloku finally, takže chyba vnútri tela nemôže nechať zastarané väzby viditeľné pre ďalší vzorec

var
  Book: TXLSXWorkbook;
  Name: TXLSXDefinedName;
begin
  Book := TXLSXWorkbook.Create;
  try
    if Book.Open('customer-model.xlsx') = 1 then
    begin
      // Preskúmajte, čo používateľ napísal, skôr než dôverujete prepočtu
      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 už nie je čiastočný

Skoršie vydania HotXLS implementovali LET len natoľko, aby zvládli bežný prípad s jednou väzbou. Súčasná implementácia je úplná: každá väzba je viditeľná pre všetky neskoršie väzby aj pre výraz tela, a vnorené LET sa skladá normálne, takže LET(a, 1, b, a+1, LET(c, b*2, c)) sa vyhodnotí presne tak, ako ho vyhodnotí Excel

Táto úplnosť je dôležitejšia, než znie. LET je spôsob, akým sa používatelia vyhýbajú prepočítavaniu toho istého podvýrazu päťkrát v jednom vzorci, takže reálne zošity ho používajú presne v tých hlboko vnorených tvaroch, na ktorých čiastočná implementácia zlyhá. Ak ste doteraz obchádzali medzery rozbaľovaním väzieb LET pred vyhodnotením, tento workaround môže padnúť

Čiarka alebo bodkočiarka: teraz oboje

Text vzorca v HotXLS teraz akceptuje čiarku ako oddeľovač argumentov popri klasickej bodkočiarke. Nejde o nastavenie jazykového prostredia; ide o pravidlo prijímania priamo v parseri. Je to dôležité, pretože vzorce prichádzajú z miest, ktoré nekontrolujete: prilepené z tiketu podpory, skopírované z dokumentácie, vygenerované skriptom, ktorý vypísal kanonickú syntax Excelu, importované z CSV s reťazcami vzorcov

Praktickým dôsledkom je, že sa skompiluje aj SUM(A1,A2), aj SUM(A1;A2). Spätný zápis zachováva to, čo použil zdroj, takže zošit, ktorý ste načítali, sa zapíše späť s pôvodnými oddeľovačmi namiesto toho, aby sa normalizoval za chrbtom používateľa

Čo prežije spätný zápis, a čo skontrolovať

Text vzorca sa ukladá doslovne, takže LAMBDA v definovanom názve prežije cyklus načítania a uloženia nedotknutá a v Exceli sa otvorí ako tá istá funkcia. Holá LAMBDA uložená ako výsledok bunky — teda vzorec, ktorý sa vyhodnotí na uzáver namiesto na hodnotu — si zachováva existujúce správanie preskočenia bez hodnoty: text sa zachová, žiadny cachovaný číselný výsledok sa preň nevymýšľa. To je čestný výsledok, keďže niet žiadny skalár na cachovanie

Oplatí sa osvojiť si dva zvyky. Dávajte pomenovaným lambdám rozsah zošita, pokiaľ na to nie je dôvod opačný, pretože funkcia s rozsahom hárku, ktorá zmizne pri skopírovaní hárku, vyprodukuje chybu názvu na mieste ďaleko od príčiny; pravidlá rozsahu sú opísané v článku definované názvy a vzorce naprieč hárkami. A keď je zošit plný pomenovaných lambd určený pre report, ktorý musí byť stabilný, zvážte zamrazenie výsledkov pomocou ConvertFormulasToValues, aby následní spotrebitelia videli čísla namiesto funkcií, ktoré možno nepodporujú

Pri náročnom prepočte sú telá LAMBDA obyčajné výrazy v grafe závislostí a plánujú sa rovnako ako každý iný vzorec, čo je opísané v článku inkrementálny prepočet a graf závislostí. Ak váš model volá jednu pomenovanú funkciu naprieč tisíckami riadkov, nákladom je telo, nie mechanizmus volania, a platia rovnaké odporúčania na optimalizáciu ako pri akomkoľvek opakovanom vzorci

HotXLS je natívny spreadsheetový komponent pre Delphi a C++Builder, ktorý číta a zapisuje XLS, XLSX a ODS bez Excelu alebo akejkoľvek automatizácie Office. API pre formulový engine, definované názvy a prepočet sú zdokumentované na stránke HotXLS Delphi spreadsheet component