HotXLS вычисляет Excel LAMBDA как настоящее значение функции первого класса. Именованный диапазон, чей текст RefersTo является LAMBDA, можно вызвать по имени как =MyFunc(5), замыкание, связанное внутри LET, можно вызвать как =LET(f, LAMBDA(x, x*2), f(21)), а лексическое окружение, захваченное в момент определения, путешествует вместе с замыканием. Текст формулы возвращается в книгу дословно
Именно эта функция отличает движок формул от парсера формул. Всё, что было до LAMBDA, можно было вычислить, обходя дерево значений. LAMBDA требует стека областей видимости, а как только у вас есть стек областей видимости, целый класс пользовательской логики электронных таблиц начинает работать в вашем Delphi-приложении, а не только в Excel
Почему большинство движков за пределами Excel останавливаются на ключевом слове LAMBDA?
Потому что классический вычислитель электронных таблиц знает ровно один вид значения: число, строку, логическое значение, ошибку или ссылку на ячейки, содержащие всё перечисленное. Функции там просто негде разместиться. Когда Excel 365 представил LAMBDA, он добавил тип значения, несущий имена параметров, тело-выражение и привязки, видимые там, где оно было написано. Движок без такого типа может разобрать LAMBDA(x, x*2) и сохранить текст, но в момент, когда ячейка пытается его вызвать, вызывать попросту нечего
HotXLS реализует недостающую часть как значение-замыкание плюс стек областей видимости во время выполнения. Вызов замыкания заталкивает в стек его захваченное окружение, затем заталкивает значения аргументов под именами параметров, вычисляет тело и обрезает стек обратно до отметки. Этот порядок важен, и следующий раздел объясняет почему
Три способа вызова LAMBDA
HotXLS разрешает вызов неизвестного имени функции через три пути, опробуемых по порядку, и знание того, какой из них сработал, объясняет большинство сюрпризов. Во-первых, имя, связанное в текущей области видимости LET или LAMBDA: если f — локальная привязка, содержащая замыкание, то f(21) его применяет. Во-вторых, определённое имя книги, чей текст формулы начинается с LAMBDA: MyFunc(5) компилирует тело этого имени и применяет его. В-третьих, классический обработчик пользовательских функций, без изменений, для всего, что не подошло под первые два пути
Локальная привязка, содержащая нечто отличное от замыкания, не вызываема. Свяжите f с числом 3, а затем напишите f(21) — и вы получите ошибку значения, а не попытку умножения. Это строже, чем повёл бы себя динамический язык, и намеренно строже: опечатка, превращающая вызов функции в случайную ссылку, — это молчаливо неверный ответ, а это худший результат, который может выдать движок электронных таблиц
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
begin
Book := TXLSXWorkbook.Create;
try
Sheet := Book.Sheets.Add('Model');
// Переиспользуемая именованная функция, область видимости книги
Book.DefinedNames.Add('NetOf', 'LAMBDA(amount, rate, amount*(1-rate))');
Sheet.Cells[2, 2].Formula := 'NetOf(1250, 0.19)';
// Замыкание, связанное и применённое внутри одной формулы
Sheet.Cells[3, 2].Formula := 'LET(double, LAMBDA(x, x*2), double(21))';
// Вложенный LET: каждая привязка видна последующим
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;
Как разрешается затенение при совпадении имён?
Побеждают параметры. Когда HotXLS применяет замыкание, он сначала заталкивает захваченное лексическое окружение, а затем — привязки аргументов, поэтому параметр с именем rate затеняет внешнюю привязку с именем rate, а также затеняет одноимённую ссылку на столбец в окружающей формуле. Именно этот порядок делает именованную функцию безопасной для повторного использования: вызывающая сторона не может случайно изменить смысл тела, имея в области видимости похоже названную привязку
Арность проверяется до того, как что-либо вычисляется. Вызов, чьё число аргументов не совпадает с числом параметров замыкания, немедленно возвращает ошибку значения, вместо того чтобы вычислить часть аргументов, а затем упасть, — это сохраняет вычисление без побочных эффектов действительно свободным от частично проделанной работы. Стек областей видимости обрезается обратно до отметки входа в блоке finally, поэтому ошибка внутри тела не может оставить устаревшие привязки видимыми для следующей формулы
var
Book: TXLSXWorkbook;
Name: TXLSXDefinedName;
begin
Book := TXLSXWorkbook.Create;
try
if Book.Open('customer-model.xlsx') = 1 then
begin
// Проверить, что написал пользователь, прежде чем доверять пересчёту
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 больше не реализован частично
В более ранних выпусках HotXLS LET был реализован лишь настолько, чтобы покрывать распространённый случай с одной привязкой. Текущая реализация полна: каждая привязка видна всем последующим привязкам и телу-выражению, а вложенный LET компонуется нормально, так что LET(a, 1, b, a+1, LET(c, b*2, c)) вычисляется точно так же, как это делает Excel
Эта полнота важнее, чем звучит на первый взгляд. LET — это способ, которым пользователи избегают пятикратного повторного вычисления одного и того же подвыражения в одной формуле, поэтому реальные книги используют его именно в тех глубоко вложенных формах, на которых частичная реализация ошибается. Если вы раньше обходили пробелы, разворачивая привязки LET перед вычислением, этот обходной путь можно убрать
Запятая или точка с запятой: теперь оба варианта
Текст формулы в HotXLS теперь принимает запятую как разделитель аргументов наряду с классической точкой с запятой. Это не настройка локали, а правило приёма в парсере. Это важно, потому что формулы приходят из мест, которые вы не контролируете: вставлены из тикета поддержки, скопированы из документации, сгенерированы скриптом, выдавшим канонический синтаксис Excel, импортированы из CSV со строками формул
Практический эффект в том, что SUM(A1,A2) и SUM(A1;A2) компилируются оба. Обратное преобразование сохраняет то, что использовал источник, поэтому загруженная вами книга записывается обратно с исходными разделителями, а не нормализуется за спиной пользователя
Что переживает обратное преобразование, и что проверять
Текст формулы хранится дословно, поэтому LAMBDA в определённом имени переживает цикл загрузки и сохранения нетронутой и открывается в Excel как та же самая функция. «Голая» LAMBDA, сохранённая как результат ячейки — то есть формула, вычисляющаяся в замыкание, а не в значение, — сохраняет прежнее поведение «пропуск без значения»: текст сохраняется, а закэшированный числовой результат для неё не выдумывается. Это честный исход, поскольку кэшировать здесь просто нечего — нет скаляра
Стоит завести две привычки. Давайте именованным лямбдам область видимости книги, если нет причины поступить иначе, потому что функция с областью видимости листа, исчезающая при копировании листа, порождает ошибку имени в месте, далёком от причины; правила области видимости рассмотрены в статье об определённых именах и межлистовых формулах. А когда книга, полная именованных лямбд, предназначена для отчёта, который должен быть стабильным, рассмотрите заморозку результатов через ConvertFormulasToValues, чтобы потребители ниже по цепочке видели числа, а не функции, которые они, возможно, не поддерживают
При тяжёлом пересчёте тела LAMBDA — это обычные выражения в графе зависимостей, планируемые так же, как любая другая формула, что описано в статье об инкрементальном пересчёте и графе зависимостей. Если ваша модель вызывает одну именованную функцию на тысячах строк, стоимость заключена в теле, а не в механизме вызова, и применяются те же рекомендации по оптимизации, что и для любой повторяющейся формулы
HotXLS — это нативный компонент электронных таблиц для Delphi и C++Builder, читающий и записывающий XLS, XLSX и ODS без Excel и без какой-либо автоматизации Office. Движок формул, определённые имена и API пересчёта задокументированы на странице HotXLS Delphi spreadsheet component