Artigo Técnico

LAMBDA e LET no Delphi: closures de fórmulas no HotXLS

O HotXLS avalia o LAMBDA do Excel como um valor de função verdadeiramente de primeira classe. Um nome definido cujo texto RefersTo é um LAMBDA pode ser chamado pelo nome como =MyFunc(5), uma closure vinculada dentro de um LET pode ser chamada como =LET(f, LAMBDA(x, x*2), f(21)), e o ambiente léxico capturado no momento da definição viaja junto com a closure. O texto da fórmula faz o round-trip para a pasta de trabalho literalmente

Esse é o recurso que separa um motor de fórmulas de um parser de fórmulas. Tudo antes do LAMBDA podia ser avaliado percorrendo uma árvore de valores. O LAMBDA exige uma pilha de escopos, e uma vez que você tem uma pilha de escopos, toda uma classe de lógica de planilha escrita pelo usuário passa a funcionar na sua aplicação Delphi, e não só no Excel

Por que a maioria dos motores fora do Excel para na palavra-chave LAMBDA?

Porque um avaliador de planilha clássico tem exatamente um tipo de valor: um número, uma string, um booleano, um erro, ou uma referência a células que contêm esses valores. Não há onde colocar uma função. Quando o Excel 365 introduziu o LAMBDA, ele adicionou um tipo de valor que carrega nomes de parâmetro, uma expressão de corpo, e as vinculações visíveis no lugar onde foi escrito. Um motor sem esse tipo consegue analisar LAMBDA(x, x*2) e armazenar o texto, mas no momento em que uma célula tenta chamá-lo, não há nada para chamar

O HotXLS implementa a peça que faltava como um valor de closure mais uma pilha de escopos em tempo de execução. Chamar uma closure empilha seu ambiente capturado, depois empilha os valores dos argumentos sob os nomes dos parâmetros, avalia o corpo, e trunca a pilha de volta até a marca. Essa ordem importa, e a próxima seção explica por quê

As três formas de chamar um LAMBDA

O HotXLS resolve uma chamada a um nome de função desconhecido por meio de três caminhos, tentados em ordem, e saber qual deles dispara explica a maioria das surpresas. Primeiro, um nome vinculado no escopo LET ou LAMBDA atual: se f é uma vinculação local contendo uma closure, f(21) a aplica. Segundo, um nome definido de pasta de trabalho cujo texto de fórmula começa com LAMBDA: MyFunc(5) compila o corpo daquele nome e o aplica. Terceiro, o handler clássico de função de usuário, inalterado, para tudo o que os dois primeiros caminhos não reivindicam

Uma vinculação local que contém algo que não é uma closure não é chamável. Vincule f ao número 3 e depois escreva f(21), e você recebe um erro de valor, não uma tentativa de multiplicação. Isso é mais rígido do que uma linguagem dinâmica seria, e deliberadamente: um erro de digitação que transforma uma chamada de função em uma referência acidental é uma resposta errada silenciosa, que é o pior resultado que um motor de planilha pode produzir

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

    // Uma função nomeada reutilizável, com escopo de pasta de trabalho
    Book.DefinedNames.Add('NetOf', 'LAMBDA(amount, rate, amount*(1-rate))');

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

    // Uma closure vinculada e aplicada dentro de uma única fórmula
    Sheet.Cells[3, 2].Formula := 'LET(double, LAMBDA(x, x*2), double(21))';

    // LET aninhado: toda vinculação é visível para as que vêm depois dela
    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;

Como o shadowing se resolve quando nomes colidem?

Os parâmetros vencem. Quando o HotXLS aplica uma closure, ele empilha primeiro o ambiente léxico capturado e depois as vinculações de argumento, de modo que um parâmetro chamado rate faz shadowing de uma vinculação externa chamada rate e também faz shadowing de uma referência de coluna com a mesma grafia na fórmula ao redor. Essa ordenação é o que torna uma função nomeada segura para reutilizar: quem chama não consegue acidentalmente mudar o que o corpo significa por ter uma vinculação com nome parecido em escopo

A aridade é verificada antes que qualquer coisa seja avaliada. Uma chamada cuja contagem de argumentos não corresponde à contagem de parâmetros da closure retorna um erro de valor imediatamente, em vez de avaliar alguns argumentos e depois falhar, o que mantém a avaliação livre de efeitos colaterais genuinamente livre de trabalho parcial. A pilha de escopos é truncada de volta até a sua marca de entrada em um bloco finally, de modo que um erro dentro de um corpo não pode deixar vinculações obsoletas visíveis para a próxima fórmula

var
  Book: TXLSXWorkbook;
  Name: TXLSXDefinedName;
begin
  Book := TXLSXWorkbook.Create;
  try
    if Book.Open('customer-model.xlsx') = 1 then
    begin
      // Inspecione o que o usuário escreveu antes de confiar em um recálculo
      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;

O LET não é mais parcial

Versões anteriores do HotXLS implementavam o LET só o suficiente para lidar com o caso comum de vinculação única. A implementação atual é completa: toda vinculação é visível para todas as vinculações posteriores e para a expressão de corpo, e LET aninhado se compõe normalmente, então LET(a, 1, b, a+1, LET(c, b*2, c)) é avaliado da mesma forma que o Excel avalia

Essa completude importa mais do que parece. O LET é como os usuários evitam recalcular a mesma subexpressão cinco vezes em uma fórmula, então pastas de trabalho reais o usam exatamente nas formas profundamente aninhadas que uma implementação parcial erra. Se você anteriormente contornava lacunas expandindo vinculações LET antes da avaliação, essa solução alternativa pode ser removida

Vírgula ou ponto e vírgula: os dois, agora

O texto de fórmula no HotXLS agora aceita a vírgula como separador de argumento ao lado do ponto e vírgula clássico. Isso não é uma configuração de locale; é uma regra de aceitação no parser. Isso importa porque as fórmulas chegam de lugares que você não controla: coladas de um chamado de suporte, copiadas de uma documentação, geradas por um script que emitiu a sintaxe canônica do Excel, importadas de um CSV de strings de fórmula

O efeito prático é que SUM(A1,A2) e SUM(A1;A2) compilam ambos. O round-trip preserva o que quer que a fonte tenha usado, então uma pasta de trabalho que você carregou é gravada de volta com os separadores originais, em vez de normalizada nas costas do usuário

O que faz round-trip, e o que verificar

O texto da fórmula é armazenado literalmente, então um LAMBDA em um nome definido sobrevive intacto a um ciclo de carregamento e gravação e abre no Excel como a mesma função. Um LAMBDA puro armazenado como resultado de célula, ou seja, uma fórmula que é avaliada como uma closure em vez de um valor, mantém o comportamento existente de pular sem valor: o texto é preservado, nenhum resultado numérico em cache é inventado para ele. Esse é o resultado honesto, já que não há escalar para armazenar em cache

Vale adotar dois hábitos. Dê às lambdas nomeadas escopo de pasta de trabalho, a menos que haja um motivo para não fazer isso, porque uma função com escopo de planilha que desaparece quando uma planilha é copiada produz um erro de nome em um lugar distante da causa; as regras de escopo são abordadas em nomes definidos e fórmulas entre planilhas. E quando uma pasta de trabalho cheia de lambdas nomeadas se destina a um relatório que precisa ser estável, considere congelar os resultados com ConvertFormulasToValues para que os consumidores a jusante vejam números, e não funções que talvez não suportem

Para recálculo pesado, os corpos de LAMBDA são expressões comuns no grafo de dependências e são agendados como qualquer outra fórmula, o que é descrito em recálculo incremental e o grafo de dependências. Se o seu modelo chama uma função nomeada em milhares de linhas, o custo é o corpo, não o mecanismo de chamada, e o mesmo conselho de otimização se aplica a qualquer fórmula repetida

O HotXLS é um componente de planilha nativo para Delphi e C++Builder que lê e grava XLS, XLSX e ODS sem Excel ou qualquer automação do Office. O motor de fórmulas, os nomes definidos e a API de recálculo estão documentados na página do componente HotXLS Delphi para planilhas