HotXLSは、Excelの LAMBDA を、正真正銘の第一級関数値として評価します。RefersToのテキストがLAMBDAである定義済み名前は、=MyFunc(5)のように名前で呼び出せます。LETの内部で束縛されたクロージャは=LET(f, LAMBDA(x, x*2), f(21))のように呼び出せ、定義時に捕捉されたレキシカル環境はそのクロージャとともに運ばれます。数式テキストはワークブックへそのままの形でラウンドトリップされます
これは、数式エンジンを単なる数式パーサーから分ける機能です。LAMBDA以前のものはすべて、値のツリーをたどるだけで評価できました。LAMBDAはスコープスタックを必要とし、いったんスコープスタックを持てば、ユーザーが作成したスプレッドシートロジックの丸ごと1カテゴリーが、Excelの中だけでなく自分のDelphiアプリケーションの中でも動き始めます
Excel以外のエンジンの多くがLAMBDAキーワードの手前で止まってしまうのはなぜか
なぜなら、クラシックなスプレッドシート評価エンジンが扱う値の種類はただ1つ、数値、文字列、ブール値、エラー、あるいはそれらを保持するセルへの参照だけだからです。関数を置く場所がどこにもありません。Excel 365がLAMBDAを導入したとき、パラメータ名、本体の式、そしてそれが書かれた場所から見える束縛を運ぶ、新しい値の型が追加されました。その型を持たないエンジンはLAMBDA(x, x*2)をパースしてテキストとして保存することはできますが、あるセルがそれを呼び出そうとした瞬間、呼び出す対象が何もありません
HotXLSは、この欠けていた部分を、クロージャ値とランタイムのスコープスタックとして実装しています。クロージャを呼び出すと、まず捕捉された環境がプッシュされ、次に引数の値がパラメータ名の下にプッシュされ、本体が評価され、最後にスタックはマークまで切り詰められます。この順序が重要であり、その理由は次のセクションで説明します
LAMBDAが呼び出される3通りの経路
HotXLSは、未知の関数名への呼び出しを、順番に試される3つの経路によって解決し、どれが発動したかを知ることで、ほとんどの意外な動作を説明できます。1つ目は、現在のLETまたはLAMBDAスコープに束縛された名前です。fがクロージャを保持するローカル束縛であれば、f(21)はそれを適用します。2つ目は、数式テキストがLAMBDAで始まるワークブックの定義済み名前です。MyFunc(5)はその名前の本体をコンパイルして適用します。3つ目は、最初の2つの経路が引き受けなかったすべてに対する、変更のないクラシックなユーザー関数ハンドラーです
クロージャ以外の何かを保持しているローカル束縛は呼び出しできません。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)';
// 1つの数式内でクロージャを束縛して適用
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は、ユーザーが1つの数式の中で同じ部分式を5回計算し直すのを避けるための仕組みであり、実際のワークブックは、まさに不完全な実装が間違えるような深くネストした形でこれを使います。以前、評価前にLETの束縛を展開することでこの不備を回避していたなら、その回避策はもう不要です
カンマかセミコロンか:今はどちらも使える
HotXLSの数式テキストは今、従来のセミコロンに加えて、カンマも引数区切りとして受け付けます。これはロケール設定ではなく、パーサーの受け入れルールです。これが重要なのは、数式が自分でコントロールできない場所から届くからです。サポートチケットからの貼り付け、ドキュメントからのコピー、Excelの正規構文を出力するスクリプトによる生成、数式文字列のCSVからのインポートなどです
実際の効果として、SUM(A1,A2)とSUM(A1;A2)のどちらもコンパイルできます。ラウンドトリップでは元のソースが使っていたものがそのまま保持されるため、読み込んだワークブックは、ユーザーの知らないところで正規化されるのではなく、元の区切り文字のまま書き戻されます
何がラウンドトリップされ、何を確認すべきか
数式テキストはそのままの形で保存されるため、定義済み名前の中のLAMBDAは、読み込みと保存のサイクルを経ても損なわれず、Excelでも同じ関数として開けます。値ではなくクロージャに評価される数式、つまりセル結果として保存されている素のLAMBDAは、既存の「値なしでスキップする」という挙動をそのまま保ちます。テキストは保持され、そのためにキャッシュされる数値結果が捏造されることはありません。キャッシュすべきスカラー値がそもそも存在しないのですから、これが誠実な帰結です
2つの習慣を身につけておく価値があります。1つは、特に理由がない限り、名前付きラムダにはワークブックスコープを与えることです。シートスコープの関数は、そのシートがコピーされると消えてしまい、原因から遠く離れた場所で名前エラーを引き起こすからです。スコープに関する規則は定義済み名前とシートをまたぐ数式で扱っています。もう1つは、名前付きラムダで満たされたワークブックが、安定していなければならないレポートの元になる場合、下流の消費者が対応していないかもしれない関数ではなく数値を見るように、ConvertFormulasToValuesで結果を固定することを検討することです
負荷の大きい再計算については、LAMBDAの本体は依存グラフの中の普通の式であり、他のあらゆる数式と同様にスケジューリングされます。これは増分再計算と依存グラフで説明しています。自分のモデルが1つの名前付き関数を数千行にわたって呼び出しているなら、コストの正体は呼び出しの仕組みではなく本体そのものであり、繰り返される数式全般に対するのと同じ最適化のアドバイスが当てはまります
HotXLSは、ExcelもOffice自動化も使わずにXLS、XLSX、ODSを読み書きする、DelphiとC++Builder向けのネイティブスプレッドシートコンポーネントです。数式エンジン、定義済み名前、再計算のAPIはHotXLS Delphiスプレッドシートコンポーネントページで説明されています