Artykuł techniczny

LAMBDA i LET w Delphi: domknięcia formuł HotXLS

HotXLS wylicza Excelowe LAMBDA jako prawdziwą wartość funkcyjną pierwszej klasy. Nazwa zdefiniowana, której tekst RefersTo jest LAMBDA, może być wywołana po nazwie jako =MyFunc(5), domknięcie związane wewnątrz LET może być wywołane jako =LET(f, LAMBDA(x, x*2), f(21)), a środowisko leksykalne przechwycone w momencie definicji podróżuje razem z domknięciem. Tekst formuły odtwarza się wiernie po zapisie i odczycie

To jest funkcja, która odróżnia silnik formuł od parsera formuł. Wszystko przed LAMBDA dało się wyliczyć, przechodząc po drzewie wartości. LAMBDA wymaga stosu zakresów, a gdy masz już stos zakresów, w twojej aplikacji Delphi zaczyna działać cała klasa logiki arkuszy autorstwa użytkowników, a nie tylko w Excelu

Dlaczego większość silników spoza Excela zatrzymuje się na słowie kluczowym LAMBDA?

Ponieważ klasyczny ewaluator arkusza kalkulacyjnego ma dokładnie jeden rodzaj wartości: liczbę, łańcuch znaków, wartość logiczną, błąd albo odwołanie do komórek trzymających te wartości. Nie ma gdzie umieścić funkcji. Gdy Excel 365 wprowadził LAMBDA, dodał typ wartości niosący nazwy parametrów, wyrażenie ciała oraz powiązania widoczne w miejscu, gdzie funkcja została napisana. Silnik bez tego typu potrafi sparsować LAMBDA(x, x*2) i zapisać tekst, ale w chwili, gdy komórka próbuje ją wywołać, nie ma czego wywołać

HotXLS implementuje brakujący element jako wartość domknięcia plus stos zakresów w czasie wykonania. Wywołanie domknięcia zdejmuje na stos jego przechwycone środowisko, potem zdejmuje na stos wartości argumentów pod nazwami parametrów, wylicza ciało, a następnie przycina stos z powrotem do znacznika. Ta kolejność ma znaczenie i wyjaśnia to następna sekcja

Trzy sposoby wywołania LAMBDA

HotXLS rozwiązuje wywołanie nieznanej nazwy funkcji przez trzy ścieżki, sprawdzane po kolei, a wiedza, która się uruchamia, wyjaśnia większość zaskoczeń. Po pierwsze, nazwa związana w bieżącym zakresie LET albo LAMBDA: jeśli f jest lokalnym wiązaniem trzymającym domknięcie, f(21) je stosuje. Po drugie, nazwa zdefiniowana w skoroszycie, której tekst formuły zaczyna się od LAMBDA: MyFunc(5) kompiluje ciało tej nazwy i je stosuje. Po trzecie, klasyczny obsługiwacz funkcji użytkownika, niezmieniony, dla wszystkiego, czego pierwsze dwie ścieżki nie roszczą sobie

Lokalne wiązanie trzymające coś innego niż domknięcie nie jest wywoływalne. Zwiąż f z liczbą 3, a potem napisz f(21), a dostaniesz błąd wartości, a nie próbę mnożenia. Jest to bardziej rygorystyczne, niż zachowałby się język dynamiczny, i celowo: literówka, która zamienia wywołanie funkcji w przypadkowe odwołanie, to cicha błędna odpowiedź, czyli najgorszy wynik, jaki może wyprodukować silnik arkusza kalkulacyjnego

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

    // Funkcja nazwana wielokrotnego użytku, zakres skoroszytu
    Book.DefinedNames.Add('NetOf', 'LAMBDA(amount, rate, amount*(1-rate))');

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

    // Domknięcie związane i zastosowane wewnątrz jednej formuły
    Sheet.Cells[3, 2].Formula := 'LET(double, LAMBDA(x, x*2), double(21))';

    // Zagnieżdżone LET: każde wiązanie jest widoczne dla tych po nim
    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;

Jak rozwiązuje się cieniowanie, gdy nazwy się zderzają?

Wygrywają parametry. Gdy HotXLS stosuje domknięcie, zdejmuje na stos najpierw przechwycone środowisko leksykalne, a wiązania argumentów na drugim miejscu, więc parametr o nazwie rate przesłania zewnętrzne wiązanie o nazwie rate, a także przesłania identycznie nazwane odwołanie do kolumny w otaczającej formule. Ta kolejność sprawia, że nazwaną funkcję można bezpiecznie ponownie wykorzystać: wywołujący nie może przypadkowo zmienić znaczenia ciała, mając w zakresie podobnie nazwane wiązanie

Liczba argumentów jest sprawdzana, zanim cokolwiek zostanie wyliczone. Wywołanie, którego liczba argumentów nie zgadza się z liczbą parametrów domknięcia, natychmiast zwraca błąd wartości, zamiast wyliczyć część argumentów i dopiero potem zawieść, co utrzymuje wyliczanie wolne od skutków ubocznych naprawdę wolne od częściowej pracy. Stos zakresów jest przycinany z powrotem do znacznika wejścia w bloku finally, więc błąd wewnątrz ciała nie może pozostawić nieaktualnych wiązań widocznych dla następnej formuły

var
  Book: TXLSXWorkbook;
  Name: TXLSXDefinedName;
begin
  Book := TXLSXWorkbook.Create;
  try
    if Book.Open('customer-model.xlsx') = 1 then
    begin
      // Sprawdź, co napisał użytkownik, zanim zaufasz ponownemu przeliczeniu
      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 nie jest już częściowe

Wcześniejsze wydania HotXLS implementowały LET tylko na tyle, by obsłużyć typowy przypadek pojedynczego wiązania. Obecna implementacja jest kompletna: każde wiązanie jest widoczne dla wszystkich późniejszych wiązań i dla wyrażenia ciała, a zagnieżdżone LET komponuje się normalnie, więc LET(a, 1, b, a+1, LET(c, b*2, c)) wylicza się dokładnie tak, jak wylicza to Excel

Ta kompletność ma większe znaczenie, niż mogłoby się wydawać. LET to sposób, w jaki użytkownicy unikają ponownego obliczania tego samego podwyrażenia pięć razy w jednej formule, więc prawdziwe skoroszyty używają go dokładnie w tych głęboko zagnieżdżonych kształtach, które częściowa implementacja psuje. Jeśli wcześniej obchodziłeś luki, rozwijając wiązania LET przed wyliczeniem, to obejście można teraz porzucić

Przecinek czy średnik: teraz oba

Tekst formuły w HotXLS akceptuje teraz przecinek jako separator argumentów obok klasycznego średnika. To nie jest ustawienie regionalne; to reguła akceptacji w parserze. Ma znaczenie, ponieważ formuły przychodzą z miejsc, których nie kontrolujesz: wklejone ze zgłoszenia wsparcia, skopiowane z dokumentacji, wygenerowane przez skrypt, który wyemitował kanoniczną składnię Excela, zaimportowane z pliku CSV zawierającego łańcuchy formuł

Praktyczny efekt jest taki, że zarówno SUM(A1,A2), jak i SUM(A1;A2) się kompilują. Odtwarzanie po zapisie zachowuje to, czego użyło źródło, więc skoroszyt, który wczytałeś, zapisuje się z powrotem z oryginalnymi separatorami, zamiast być po cichu znormalizowany za plecami użytkownika

Co się odtwarza po zapisie, a co warto sprawdzić

Tekst formuły jest przechowywany dosłownie, więc LAMBDA w nazwie zdefiniowanej przetrwa cykl wczytania i zapisu nienaruszona i otwiera się w Excelu jako ta sama funkcja. Goła LAMBDA zapisana jako wynik komórki, czyli formuła, która wylicza się do domknięcia zamiast do wartości, zachowuje dotychczasowe zachowanie pomijania bez wartości: tekst jest zachowany, żaden buforowany wynik liczbowy nie zostaje dla niej wymyślony. To uczciwy wynik, ponieważ nie ma tam żadnej wartości skalarnej do zbuforowania

Warto przyjąć dwa nawyki. Nadawaj nazwanym lambdom zakres skoroszytu, chyba że masz powód, by tego nie robić, ponieważ funkcja o zakresie arkusza, która znika, gdy arkusz zostaje skopiowany, tworzy błąd nazwy w miejscu odległym od przyczyny; reguły zakresu są omówione w nazwach zdefiniowanych i formułach między arkuszami. A gdy skoroszyt pełen nazwanych lambd jest przeznaczony do raportu, który musi być stabilny, rozważ zamrożenie wyników za pomocą ConvertFormulasToValues, aby konsumenci dalszej części potoku widzieli liczby, a nie funkcje, których mogą nie obsługiwać

Przy ciężkim przeliczaniu ciała LAMBDA są zwykłymi wyrażeniami w grafie zależności i są planowane tak samo jak każda inna formuła, co opisano w przyrostowym przeliczaniu i grafie zależności. Jeśli twój model wywołuje jedną nazwaną funkcję w tysiącach wierszy, kosztem jest ciało, nie mechanizm wywołania, i obowiązują te same porady optymalizacyjne co dla każdej powtarzanej formuły

HotXLS to natywny komponent arkusza kalkulacyjnego dla Delphi i C++Builder, który odczytuje i zapisuje XLS, XLSX i ODS bez Excela ani żadnej automatyzacji Office. Silnik formuł, nazwy zdefiniowane i API przeliczania są udokumentowane na stronie komponentu arkusza kalkulacyjnego HotXLS dla Delphi