Artikel Teknis

LAMBDA dan LET di Delphi: Closure Formula HotXLS

HotXLS mengevaluasi LAMBDA Excel sebagai sebuah nilai fungsi first-class yang sesungguhnya. Sebuah defined name yang teks RefersTo-nya berupa LAMBDA bisa dipanggil dengan nama sebagai =MyFunc(5), sebuah closure yang di-bind di dalam LET bisa dipanggil sebagai =LET(f, LAMBDA(x, x*2), f(21)), dan lexical environment yang ditangkap saat definisi ikut terbawa bersama closure-nya. Teks formula round-trip ke dalam workbook secara verbatim

Inilah fitur yang membedakan formula engine dari sekadar formula parser. Segala sesuatu sebelum LAMBDA bisa dievaluasi dengan menyusuri sebuah tree nilai. LAMBDA membutuhkan scope stack, dan begitu Anda memiliki scope stack, seluruh kelas logika spreadsheet buatan pengguna mulai berfungsi di aplikasi Delphi Anda, bukan hanya di Excel

Mengapa sebagian besar engine non-Excel berhenti di keyword LAMBDA?

Karena evaluator spreadsheet klasik hanya punya persis satu jenis nilai: angka, string, boolean, error, atau referensi ke cell yang menyimpan hal-hal itu. Tidak ada tempat untuk menaruh sebuah fungsi. Saat Excel 365 memperkenalkan LAMBDA, ia menambahkan sebuah tipe nilai yang membawa nama parameter, sebuah body expression, dan binding yang terlihat di tempat ia ditulis. Sebuah engine tanpa tipe itu bisa mem-parsing LAMBDA(x, x*2) dan menyimpan teksnya, tapi begitu sebuah cell mencoba memanggilnya, tidak ada apa pun untuk dipanggil

HotXLS mengimplementasikan bagian yang hilang itu sebagai sebuah nilai closure ditambah sebuah runtime scope stack. Memanggil sebuah closure melakukan push pada environment yang ditangkapnya, lalu push nilai argumen di bawah nama parameternya, mengevaluasi body-nya, lalu memotong stack itu kembali ke mark-nya. Urutan itu penting, dan bagian berikutnya menjelaskan alasannya

Tiga cara sebuah LAMBDA dipanggil

HotXLS meresolve pemanggilan ke nama fungsi yang tidak dikenal melalui tiga jalur, dicoba secara berurutan, dan mengetahui jalur mana yang terpicu menjelaskan sebagian besar kejutan. Pertama, sebuah nama yang di-bind di dalam scope LET atau LAMBDA saat ini: jika f adalah local binding yang menyimpan sebuah closure, f(21) menerapkannya. Kedua, sebuah defined name workbook yang teks formulanya diawali LAMBDA: MyFunc(5) mengompilasi body nama itu dan menerapkannya. Ketiga, handler user-function klasik, tidak berubah, untuk segala sesuatu yang tidak diklaim oleh dua jalur pertama

Sebuah local binding yang menyimpan sesuatu selain closure tidak bisa dipanggil. Bind f ke angka 3 lalu tulis f(21) dan Anda akan mendapat value error, bukan sebuah usaha untuk mengalikan. Ini lebih ketat daripada yang akan dilakukan sebuah bahasa dinamis, dan itu disengaja: kesalahan ketik yang mengubah sebuah pemanggilan fungsi menjadi referensi yang tidak disengaja adalah jawaban salah yang diam-diam, dan itulah hasil terburuk yang bisa dihasilkan sebuah spreadsheet engine

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

    // Sebuah fungsi bernama yang bisa dipakai ulang, workbook scope
    Book.DefinedNames.Add('NetOf', 'LAMBDA(amount, rate, amount*(1-rate))');

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

    // Sebuah closure yang di-bind dan diterapkan di dalam satu formula
    Sheet.Cells[3, 2].Formula := 'LET(double, LAMBDA(x, x*2), double(21))';

    // LET bersarang: setiap binding terlihat oleh binding-binding setelahnya
    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;

Bagaimana shadowing diselesaikan saat nama saling bertabrakan?

Parameter yang menang. Saat HotXLS menerapkan sebuah closure, ia push lexical environment yang ditangkap terlebih dahulu, lalu push argument binding-nya, sehingga parameter bernama rate menge-shadow binding luar bernama rate dan juga menge-shadow referensi kolom dengan ejaan yang sama di dalam formula sekitarnya. Urutan itulah yang membuat sebuah fungsi bernama aman untuk dipakai ulang: pemanggilnya tidak bisa secara tidak sengaja mengubah arti body-nya hanya karena ada binding bernama serupa di dalam scope

Arity diperiksa sebelum apa pun dievaluasi. Sebuah pemanggilan yang jumlah argumennya tidak cocok dengan jumlah parameter closure-nya langsung mengembalikan value error, alih-alih mengevaluasi sebagian argumen lalu gagal, yang menjaga evaluasi bebas efek samping itu benar-benar bebas dari pekerjaan parsial. Scope stack dipotong kembali ke mark awalnya di dalam blok finally, sehingga sebuah error di dalam body tidak bisa meninggalkan binding basi yang terlihat oleh formula berikutnya

var
  Book: TXLSXWorkbook;
  Name: TXLSXDefinedName;
begin
  Book := TXLSXWorkbook.Create;
  try
    if Book.Open('customer-model.xlsx') = 1 then
    begin
      // Periksa apa yang ditulis pengguna sebelum mempercayai sebuah recalculation
      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 tidak lagi parsial

Rilis HotXLS sebelumnya mengimplementasikan LET hanya sejauh cukup untuk menangani kasus single-binding yang umum. Implementasi saat ini sudah lengkap: setiap binding terlihat oleh semua binding setelahnya dan oleh body expression-nya, dan LET bersarang tersusun secara normal, sehingga LET(a, 1, b, a+1, LET(c, b*2, c)) dievaluasi dengan cara yang sama seperti Excel mengevaluasinya

Kelengkapan itu lebih penting dari kedengarannya. LET adalah cara pengguna menghindari menghitung ulang subexpression yang sama lima kali dalam satu formula, sehingga workbook nyata memakainya persis dalam bentuk bersarang dalam yang salah ditangani oleh implementasi parsial. Jika sebelumnya Anda mengakali celah itu dengan mengekspansi binding LET sebelum evaluasi, akal-akalan itu sudah bisa dihapus

Koma atau titik koma: keduanya, sekarang

Teks formula di HotXLS sekarang menerima koma sebagai pemisah argumen di samping titik koma klasik. Ini bukan pengaturan locale; ini adalah aturan penerimaan di dalam parser. Ini penting karena formula datang dari tempat-tempat yang tidak Anda kendalikan: ditempel dari sebuah support ticket, disalin dari dokumentasi, dihasilkan oleh sebuah skrip yang mengeluarkan sintaks kanonik Excel, diimpor dari sebuah CSV berisi string formula

Efek praktisnya adalah SUM(A1,A2) dan SUM(A1;A2) sama-sama bisa dikompilasi. Round-tripping mempertahankan apa pun pemisah yang dipakai sumbernya, sehingga workbook yang Anda muat ditulis kembali dengan pemisah aslinya, bukannya dinormalisasi diam-diam di belakang pengguna

Apa yang round-trip, dan apa yang perlu diperiksa

Teks formula disimpan secara verbatim, sehingga sebuah LAMBDA di dalam defined name tetap utuh setelah siklus load dan save serta terbuka di Excel sebagai fungsi yang sama. LAMBDA telanjang yang tersimpan sebagai hasil cell, artinya sebuah formula yang dievaluasi menjadi sebuah closure alih-alih sebuah nilai, tetap memakai perilaku skip-without-value yang sudah ada: teksnya dipertahankan, tidak ada hasil numerik yang di-cache yang direka-reka untuknya. Itulah hasil yang jujur, karena memang tidak ada skalar untuk di-cache

Ada dua kebiasaan yang layak diterapkan. Beri named lambda scope workbook kecuali ada alasan untuk tidak melakukannya, karena sebuah fungsi ber-scope sheet yang lenyap saat sebuah sheet disalin menghasilkan sebuah name error di tempat yang jauh dari penyebabnya; aturan scoping-nya dibahas di defined name dan formula lintas sheet. Dan saat sebuah workbook penuh named lambda ditujukan untuk sebuah laporan yang harus stabil, pertimbangkan untuk membekukan hasilnya dengan ConvertFormulasToValues sehingga consumer downstream melihat angka, bukan fungsi yang mungkin tidak mereka dukung

Untuk recalculation yang berat, body LAMBDA adalah expression biasa di dalam dependency graph dan dijadwalkan seperti formula lainnya, yang dijelaskan di incremental recalculation dan dependency graph. Jika model Anda memanggil satu fungsi bernama di ribuan baris, biayanya ada di body-nya, bukan di mesin pemanggilannya, dan saran optimisasi yang sama berlaku seperti pada formula berulang lainnya

HotXLS adalah komponen spreadsheet Delphi dan C++Builder native yang membaca dan menulis XLS, XLSX, dan ODS tanpa Excel atau Office automation apa pun. Formula engine, defined name, dan API recalculation didokumentasikan di halaman komponen spreadsheet Delphi HotXLS