Technischer Artikel

LAMBDA & LET in Delphi: HotXLS-Formel-Closures

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