HotXLS wertet Excel-LAMBDA als echten First-Class-Funktionswert aus. Ein definierter Name, dessen RefersTo-Text ein LAMBDA ist, kann als =MyFunc(5) namentlich aufgerufen werden, eine in LET gebundene Closure kann als =LET(f, LAMBDA(x, x*2), f(21)) aufgerufen werden, und die zum Definitionszeitpunkt erfasste lexikalische Umgebung reist mit der Closure. Der Formeltext geht beim Roundtrip wortgetreu durch
Das ist das Feature, das eine Formel-Engine von einem Formel-Parser unterscheidet. Alles vor LAMBDA ließ sich auswerten, indem man einen Baum aus Werten durchlief. LAMBDA erfordert einen Scope-Stack, und hat man einmal einen Scope-Stack, beginnt eine ganze Klasse von nutzerautorisierter Tabellenkalkulationslogik, in der eigenen Delphi-Anwendung zu funktionieren statt nur in Excel
Warum enden die meisten Nicht-Excel-Engines beim Schlüsselwort LAMBDA?
Weil ein klassischer Tabellenkalkulations-Evaluator genau eine Art von Wert kennt: eine Zahl, einen String, einen Boolean, einen Fehler oder einen Verweis auf Zellen, die diese Werte halten. Es gibt keinen Platz für eine Funktion. Als Excel 365 LAMBDA einführte, fügte es einen Werttyp hinzu, der Parameternamen, einen Rumpfausdruck und die an der Schreibstelle sichtbaren Bindungen trägt. Eine Engine ohne diesen Typ kann LAMBDA(x, x*2) parsen und den Text speichern, aber in dem Moment, in dem eine Zelle versucht, ihn aufzurufen, gibt es nichts aufzurufen
HotXLS implementiert das fehlende Stück als Closure-Wert plus einen Laufzeit-Scope-Stack. Ein Aufruf einer Closure schiebt deren erfasste Umgebung, schiebt dann die Argumentwerte unter den Parameternamen, wertet den Rumpf aus und kürzt den Stack zurück auf die Markierung. Diese Reihenfolge zählt, und der nächste Abschnitt erklärt, warum
Die drei Wege, wie ein LAMBDA aufgerufen wird
HotXLS löst einen Aufruf eines unbekannten Funktionsnamens über drei Pfade auf, der Reihe nach probiert, und zu wissen, welcher davon greift, erklärt die meisten Überraschungen. Erstens ein im aktuellen LET- oder LAMBDA-Scope gebundener Name: Ist f eine lokale Bindung, die eine Closure hält, wendet f(21) sie an. Zweitens ein definierter Name im Arbeitsbuch, dessen Formeltext mit LAMBDA beginnt: MyFunc(5) kompiliert den Rumpf dieses Namens und wendet ihn an. Drittens der klassische, unveränderte Handler für nutzerdefinierte Funktionen, für alles, was die ersten beiden Pfade nicht beanspruchen
Eine lokale Bindung, die etwas anderes als eine Closure hält, ist nicht aufrufbar. Bindet man f an die Zahl 3 und schreibt dann f(21), erhält man einen Wertfehler, keinen Multiplikationsversuch. Das ist strenger, als es eine dynamische Sprache wäre, und das mit Absicht: Ein Tippfehler, der einen Funktionsaufruf in einen versehentlichen Verweis verwandelt, ist eine stille falsche Antwort, das schlimmste Ergebnis, das eine Tabellenkalkulations-Engine liefern kann
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
begin
Book := TXLSXWorkbook.Create;
try
Sheet := Book.Sheets.Add('Model');
// Eine wiederverwendbare benannte Funktion, Arbeitsbuch-Scope
Book.DefinedNames.Add('NetOf', 'LAMBDA(amount, rate, amount*(1-rate))');
Sheet.Cells[2, 2].Formula := 'NetOf(1250, 0.19)';
// Eine Closure, in einer Formel gebunden und angewendet
Sheet.Cells[3, 2].Formula := 'LET(double, LAMBDA(x, x*2), double(21))';
// Verschachteltes LET: jede Bindung ist für die nachfolgenden sichtbar
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;
Wie wird Shadowing bei Namenskonflikten aufgelöst?
Parameter gewinnen. Wendet HotXLS eine Closure an, schiebt es zuerst die erfasste lexikalische Umgebung und danach die Argumentbindungen, sodass ein Parameter namens rate eine äußere Bindung namens rate überschattet und ebenso einen gleichnamigen Spaltenverweis in der umgebenden Formel überschattet. Diese Reihenfolge ist es, die eine benannte Funktion sicher wiederverwendbar macht: Der Aufrufer kann nicht versehentlich ändern, was der Rumpf bedeutet, nur weil eine ähnlich benannte Bindung im Scope liegt
Die Arität wird geprüft, bevor überhaupt etwas ausgewertet wird. Ein Aufruf, dessen Argumentzahl nicht zur Parameterzahl der Closure passt, liefert sofort einen Wertfehler, statt einige Argumente auszuwerten und dann zu scheitern, was die nebenwirkungsfreie Auswertung tatsächlich frei von Teilarbeit hält. Der Scope-Stack wird in einem finally-Block zurück auf seine Eintrittsmarkierung gekürzt, sodass ein Fehler im Rumpf keine veralteten Bindungen für die nächste Formel sichtbar lassen kann
var
Book: TXLSXWorkbook;
Name: TXLSXDefinedName;
begin
Book := TXLSXWorkbook.Create;
try
if Book.Open('customer-model.xlsx') = 1 then
begin
// Prüfen, was der Nutzer verfasst hat, bevor einer Neuberechnung vertraut wird
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 ist nicht mehr unvollständig
Frühere HotXLS-Versionen implementierten LET nur so weit, dass der häufige Fall mit einer einzelnen Bindung funktionierte. Die aktuelle Implementierung ist vollständig: Jede Bindung ist für alle späteren Bindungen und für den Rumpfausdruck sichtbar, und verschachteltes LET setzt sich normal zusammen, sodass LET(a, 1, b, a+1, LET(c, b*2, c)) so ausgewertet wird, wie Excel es auswertet
Diese Vollständigkeit zählt mehr, als es klingt. LET ist, wie Nutzer vermeiden, denselben Teilausdruck fünfmal in einer Formel neu zu berechnen, also nutzen echte Arbeitsbücher es genau in den tief verschachtelten Formen, bei denen eine unvollständige Implementierung versagt. Wer zuvor Lücken umging, indem er LET-Bindungen vor der Auswertung expandierte, kann diesen Workaround jetzt aufgeben
Komma oder Semikolon: jetzt beides
Formeltext in HotXLS akzeptiert jetzt das Komma als Argumenttrenner neben dem klassischen Semikolon. Das ist keine Locale-Einstellung; es ist eine Akzeptanzregel im Parser. Das zählt, weil Formeln von Orten kommen, die man nicht kontrolliert: aus einem Support-Ticket eingefügt, aus Dokumentation kopiert, von einem Skript erzeugt, das Excels kanonische Syntax ausgab, aus einer CSV mit Formeltexten importiert
Der praktische Effekt ist, dass sowohl SUM(A1,A2) als auch SUM(A1;A2) kompilieren. Roundtripping bewahrt, was auch immer die Quelle verwendet hat, sodass ein geladenes Arbeitsbuch mit seinen ursprünglichen Trennzeichen zurückgeschrieben wird, statt hinter dem Rücken des Nutzers normalisiert zu werden
Was beim Roundtrip erhalten bleibt, und was zu prüfen ist
Der Formeltext wird wortgetreu gespeichert, sodass ein LAMBDA in einem definierten Namen einen Lade- und Speicherzyklus intakt übersteht und in Excel als dieselbe Funktion öffnet. Ein nacktes LAMBDA, das als Zellergebnis gespeichert ist, also eine Formel, die zu einer Closure statt zu einem Wert auswertet, behält das bestehende Skip-without-value-Verhalten: Der Text bleibt erhalten, kein zwischengespeichertes numerisches Ergebnis wird dafür erfunden. Das ist das ehrliche Ergebnis, da es keinen Skalar zum Cachen gibt
Zwei Gewohnheiten lohnen sich. Geben Sie benannten Lambdas Arbeitsbuch-Scope, sofern kein Grund dagegenspricht, denn eine blattbezogene Funktion, die beim Kopieren eines Blatts verschwindet, erzeugt einen Namensfehler an einem Ort weit entfernt von der Ursache; die Scoping-Regeln werden in definierten Namen und blattübergreifenden Formeln behandelt. Und wenn ein Arbeitsbuch voller benannter Lambdas für einen Bericht bestimmt ist, der stabil sein muss, sollten Sie erwägen, die Ergebnisse mit ConvertFormulasToValues einzufrieren, damit nachgelagerte Konsumenten Zahlen sehen statt Funktionen, die sie vielleicht nicht unterstützen
Für aufwendige Neuberechnung sind LAMBDA-Rümpfe gewöhnliche Ausdrücke im Abhängigkeitsgraphen und werden wie jede andere Formel eingeplant, was in inkrementeller Neuberechnung und dem Abhängigkeitsgraphen beschrieben wird. Ruft Ihr Modell eine benannte Funktion über Tausende Zeilen hinweg auf, liegen die Kosten im Rumpf, nicht in der Aufrufmaschinerie, und dieselben Optimierungsempfehlungen gelten wie für jede wiederholte Formel
HotXLS ist eine native Delphi- und C++Builder-Spreadsheet-Komponente, die XLS, XLSX und ODS ohne Excel oder jede Office-Automatisierung liest und schreibt. Die Formel-Engine, die definierten Namen und die Recalculation-API sind auf der HotXLS Delphi Spreadsheet-Komponente-Seite dokumentiert