Article technique

LAMBDA et LET en Delphi : fermetures HotXLS

HotXLS évalue LAMBDA d'Excel comme une véritable valeur de fonction de première classe. Un nom défini dont le texte RefersTo est une LAMBDA peut être appelé par son nom comme =MyFunc(5), une fermeture liée à l'intérieur d'un LET peut être appelée comme =LET(f, LAMBDA(x, x*2), f(21)), et l'environnement lexical capturé au moment de la définition voyage avec la fermeture. Le texte de la formule fait l'aller-retour dans le classeur mot pour mot

C'est la fonctionnalité qui sépare un moteur de formules d'un simple analyseur de formules. Tout ce qui précédait LAMBDA pouvait être évalué en parcourant un arbre de valeurs. LAMBDA exige une pile de portées, et une fois que vous disposez d'une pile de portées, toute une classe de logique de tableur écrite par les utilisateurs se met à fonctionner dans votre application Delphi au lieu de fonctionner uniquement dans Excel

Pourquoi la plupart des moteurs non-Excel s'arrêtent-ils au mot-clé LAMBDA ?

Parce qu'un évaluateur de tableur classique n'a exactement qu'un seul type de valeur : un nombre, une chaîne, un booléen, une erreur, ou une référence à des cellules contenant ceux-ci. Il n'y a nulle part où placer une fonction. Quand Excel 365 a introduit LAMBDA, il a ajouté un type de valeur qui porte des noms de paramètres, une expression de corps, et les liaisons visibles à l'endroit où elle a été écrite. Un moteur dépourvu de ce type peut analyser LAMBDA(x, x*2) et stocker le texte, mais au moment où une cellule tente de l'appeler, il n'y a rien à appeler

HotXLS implémente la pièce manquante sous la forme d'une valeur de fermeture plus une pile de portées à l'exécution. Appeler une fermeture empile son environnement capturé, puis empile les valeurs d'argument sous les noms de paramètres, évalue le corps, et tronque la pile jusqu'à la marque. Cet ordre compte, et la section suivante explique pourquoi

Les trois façons dont une LAMBDA est appelée

HotXLS résout un appel à un nom de fonction inconnu via trois chemins, essayés dans l'ordre, et savoir lequel se déclenche explique la plupart des surprises. D'abord, un nom lié dans la portée LET ou LAMBDA courante : si f est une liaison locale contenant une fermeture, f(21) l'applique. Ensuite, un nom défini au niveau du classeur dont le texte de formule commence par LAMBDA : MyFunc(5) compile le corps de ce nom et l'applique. Enfin, le gestionnaire de fonction utilisateur classique, inchangé, pour tout ce que les deux premiers chemins ne revendiquent pas

Une liaison locale qui contient autre chose qu'une fermeture n'est pas appelable. Liez f au nombre 3 puis écrivez f(21) et vous obtenez une erreur de valeur, pas une tentative de multiplication. C'est plus strict que ne le serait un langage dynamique, et délibérément ainsi : une faute de frappe qui transforme un appel de fonction en référence accidentelle est une mauvaise réponse silencieuse, le pire résultat qu'un moteur de tableur puisse produire

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

    // Une fonction nommée réutilisable, portée classeur
    Book.DefinedNames.Add('NetOf', 'LAMBDA(amount, rate, amount*(1-rate))');

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

    // Une fermeture liée et appliquée à l'intérieur d'une formule
    Sheet.Cells[3, 2].Formula := 'LET(double, LAMBDA(x, x*2), double(21))';

    // LET imbriqué : chaque liaison est visible pour celles qui suivent
    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;

Comment le masquage se résout-il quand des noms entrent en collision ?

Les paramètres l'emportent. Quand HotXLS applique une fermeture, il empile d'abord l'environnement lexical capturé puis les liaisons d'arguments ensuite, si bien qu'un paramètre nommé rate masque une liaison externe nommée rate et masque aussi une référence de colonne de même orthographe dans la formule environnante. Cet ordre est ce qui rend une fonction nommée sûre à réutiliser : l'appelant ne peut pas changer accidentellement le sens du corps en ayant une liaison de nom similaire dans la portée

L'arité est vérifiée avant que quoi que ce soit ne soit évalué. Un appel dont le nombre d'arguments ne correspond pas au nombre de paramètres de la fermeture renvoie immédiatement une erreur de valeur, plutôt que d'évaluer certains arguments puis d'échouer, ce qui garde l'évaluation sans effet de bord véritablement exempte de travail partiel. La pile de portées est tronquée jusqu'à sa marque d'entrée dans un bloc finally, si bien qu'une erreur à l'intérieur d'un corps ne peut pas laisser de liaisons obsolètes visibles pour la formule suivante

var
  Book: TXLSXWorkbook;
  Name: TXLSXDefinedName;
begin
  Book := TXLSXWorkbook.Create;
  try
    if Book.Open('customer-model.xlsx') = 1 then
    begin
      // Inspecter ce que l'utilisateur a écrit avant de faire confiance à un recalcul
      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 n'est plus partiel

Les versions antérieures de HotXLS n'implémentaient LET que suffisamment pour gérer le cas courant à liaison unique. L'implémentation actuelle est complète : chaque liaison est visible pour toutes les liaisons ultérieures et pour l'expression de corps, et les LET imbriqués se composent normalement, si bien que LET(a, 1, b, a+1, LET(c, b*2, c)) s'évalue de la même façon qu'Excel l'évalue

Cette complétude compte plus qu'il n'y paraît. LET est le moyen par lequel les utilisateurs évitent de recalculer cinq fois la même sous-expression dans une formule, si bien que les classeurs réels l'emploient précisément dans les formes profondément imbriquées qu'une implémentation partielle traite mal. Si vous contourniez auparavant les lacunes en développant les liaisons LET avant l'évaluation, ce contournement peut disparaître

Virgule ou point-virgule : les deux, désormais

Le texte de formule dans HotXLS accepte désormais la virgule comme séparateur d'argument aux côtés du point-virgule classique. Ce n'est pas un paramètre régional ; c'est une règle d'acceptation dans le parseur. Cela compte car les formules arrivent de lieux que vous ne contrôlez pas : collées depuis un ticket de support, copiées depuis de la documentation, générées par un script qui a émis la syntaxe canonique d'Excel, importées depuis un CSV de chaînes de formule

L'effet pratique est que SUM(A1,A2) et SUM(A1;A2) compilent tous les deux. L'aller-retour préserve ce que la source utilisait, si bien qu'un classeur que vous avez chargé est réécrit avec ses séparateurs d'origine plutôt que normalisé à l'insu de l'utilisateur

Ce qui fait l'aller-retour, et ce qu'il faut vérifier

Le texte de formule est stocké mot pour mot, si bien qu'une LAMBDA dans un nom défini survit intacte à un cycle de chargement et d'enregistrement et s'ouvre dans Excel comme la même fonction. Une LAMBDA nue stockée comme résultat de cellule, c'est-à-dire une formule qui s'évalue en une fermeture plutôt qu'en une valeur, conserve le comportement existant d'omission sans valeur : le texte est préservé, aucun résultat numérique mis en cache n'est inventé pour elle. C'est le résultat honnête, puisqu'il n'y a aucun scalaire à mettre en cache

Deux habitudes méritent d'être adoptées. Donnez aux lambdas nommées une portée classeur sauf raison contraire, car une fonction à portée feuille qui disparaît quand une feuille est copiée produit une erreur de nom à un endroit éloigné de la cause ; les règles de portée sont couvertes dans les noms définis et les formules inter-feuilles. Et quand un classeur plein de lambdas nommées est destiné à un rapport qui doit rester stable, envisagez de figer les résultats avec ConvertFormulasToValues afin que les consommateurs en aval voient des nombres plutôt que des fonctions qu'ils ne prennent peut-être pas en charge

Pour un recalcul lourd, les corps de LAMBDA sont des expressions ordinaires dans le graphe de dépendances et sont planifiés comme n'importe quelle autre formule, ce qui est décrit dans le recalcul incrémental et le graphe de dépendances. Si votre modèle appelle une fonction nommée à travers des milliers de lignes, le coût est le corps, pas la mécanique d'appel, et les mêmes conseils d'optimisation s'appliquent que pour toute formule répétée

HotXLS est un composant tableur natif pour Delphi et C++Builder qui lit et écrit XLS, XLSX et ODS sans Excel ni aucune automatisation Office. Le moteur de formules, les noms définis et l'API de recalcul sont documentés sur la page HotXLS Delphi spreadsheet component