Bài viết kỹ thuật

HotXLS: LAMBDA và LET dạng closure trong Delphi

HotXLS tính toán LAMBDA của Excel như một giá trị hàm hạng nhất thực thụ. Một tên được định nghĩa có văn bản RefersTo là một LAMBDA có thể được gọi theo tên như =MyFunc(5), một closure được gắn bên trong LET có thể được gọi như =LET(f, LAMBDA(x, x*2), f(21)), và môi trường từ vựng được bắt tại thời điểm định nghĩa sẽ đi theo cùng closure đó. Văn bản công thức round-trip nguyên vẹn vào sổ làm việc

Đây chính là tính năng phân biệt một bộ máy công thức với một bộ phân tích công thức. Mọi thứ trước LAMBDA đều có thể được tính toán bằng cách duyệt một cây giá trị. LAMBDA đòi hỏi một ngăn xếp phạm vi, và một khi bạn có ngăn xếp phạm vi, cả một lớp logic bảng tính do người dùng tự viết bắt đầu hoạt động trong ứng dụng Delphi của bạn thay vì chỉ trong Excel

Vì sao hầu hết các bộ máy không phải Excel dừng lại ở từ khóa LAMBDA?

Vì một bộ tính toán bảng tính kinh điển chỉ có đúng một loại giá trị: một số, một chuỗi, một boolean, một lỗi, hoặc một tham chiếu đến các ô chứa những giá trị đó. Không có chỗ nào để đặt một hàm. Khi Excel 365 giới thiệu LAMBDA, nó bổ sung một kiểu giá trị mới mang theo tên tham số, một biểu thức thân hàm, và các liên kết nhìn thấy được tại nơi nó được viết ra. Một bộ máy thiếu kiểu giá trị đó có thể phân tích LAMBDA(x, x*2) và lưu văn bản, nhưng ngay khi một ô cố gắng gọi nó, không có gì để gọi cả

HotXLS triển khai phần còn thiếu đó dưới dạng một giá trị closure cộng với một ngăn xếp phạm vi runtime. Gọi một closure sẽ đẩy môi trường đã bắt của nó, rồi đẩy các giá trị đối số dưới các tên tham số, tính toán thân hàm, rồi cắt ngăn xếp trở lại dấu mốc ban đầu. Thứ tự đó quan trọng, và phần tiếp theo sẽ giải thích vì sao

Ba cách một LAMBDA được gọi

HotXLS giải quyết một lượt gọi đến một tên hàm chưa biết qua ba đường, thử theo thứ tự, và biết đường nào được kích hoạt sẽ giải thích được hầu hết các bất ngờ. Thứ nhất, một tên được gắn trong phạm vi LET hoặc LAMBDA hiện tại: nếu f là một liên kết cục bộ giữ một closure, f(21) sẽ áp dụng nó. Thứ hai, một tên đã định nghĩa của sổ làm việc có văn bản công thức bắt đầu bằng LAMBDA: MyFunc(5) biên dịch thân hàm của tên đó rồi áp dụng nó. Thứ ba, trình xử lý hàm người dùng kinh điển, không thay đổi, cho mọi thứ mà hai đường trên không nhận

Một liên kết cục bộ giữ thứ gì đó khác một closure thì không thể gọi được. Gắn f với số 3 rồi viết f(21) và bạn sẽ nhận một lỗi giá trị, không phải một phép thử nhân. Điều này nghiêm ngặt hơn một ngôn ngữ động, và đó là chủ ý: một lỗi đánh máy biến một lượt gọi hàm thành một tham chiếu vô tình là một câu trả lời sai âm thầm, kết quả tệ nhất mà một bộ máy bảng tính có thể tạo ra

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

    // Một hàm đặt tên có thể tái sử dụng, phạm vi sổ làm việc
    Book.DefinedNames.Add('NetOf', 'LAMBDA(amount, rate, amount*(1-rate))');

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

    // Một closure được gắn và áp dụng bên trong một công thức
    Sheet.Cells[3, 2].Formula := 'LET(double, LAMBDA(x, x*2), double(21))';

    // LET lồng nhau: mọi liên kết đều nhìn thấy được với các liên kết sau nó
    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;

Shadowing được giải quyết như thế nào khi các tên trùng nhau?

Tham số luôn thắng. Khi HotXLS áp dụng một closure, nó đẩy môi trường từ vựng đã bắt trước rồi mới đến các liên kết đối số, nên một tham số tên rate che khuất một liên kết bên ngoài cùng tên rate và cũng che khuất một tham chiếu cột trùng tên trong công thức xung quanh. Thứ tự đó chính là điều khiến một hàm đặt tên an toàn để tái sử dụng: bên gọi không thể vô tình làm thay đổi ý nghĩa của thân hàm bằng cách có một liên kết trùng tên đang trong phạm vi

Số lượng đối số được kiểm tra trước khi bất cứ thứ gì được tính toán. Một lượt gọi mà số đối số không khớp với số tham số của closure sẽ trả về một lỗi giá trị ngay lập tức, thay vì tính toán một vài đối số rồi mới thất bại, điều này giữ cho việc tính toán không có side-effect thực sự không có bất kỳ công việc dang dở nào. Ngăn xếp phạm vi được cắt về dấu mốc đầu vào của nó trong một khối finally, nên một lỗi bên trong thân hàm không thể để lại các liên kết cũ hiển thị cho công thức tiếp theo

var
  Book: TXLSXWorkbook;
  Name: TXLSXDefinedName;
begin
  Book := TXLSXWorkbook.Create;
  try
    if Book.Open('customer-model.xlsx') = 1 then
    begin
      // Kiểm tra những gì người dùng đã viết trước khi tin vào một lượt tính lại
      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 không còn dang dở nữa

Các phiên bản HotXLS trước đây chỉ triển khai LET đủ để xử lý trường hợp một liên kết đơn phổ biến. Cách triển khai hiện tại đã hoàn chỉnh: mọi liên kết đều nhìn thấy được với tất cả các liên kết sau nó và với biểu thức thân, và LET lồng nhau kết hợp bình thường, nên LET(a, 1, b, a+1, LET(c, b*2, c)) được tính toán đúng như cách Excel tính toán nó

Sự hoàn chỉnh đó quan trọng hơn vẻ ngoài của nó. LET là cách người dùng tránh phải tính lại cùng một biểu thức con năm lần trong một công thức, nên các sổ làm việc thực tế dùng nó theo đúng những hình dạng lồng sâu mà một cách triển khai dang dở xử lý sai. Nếu bạn từng khắc phục tạm thời bằng cách mở rộng các liên kết LET trước khi tính toán, giải pháp tạm đó có thể bỏ đi

Dấu phẩy hay dấu chấm phẩy: giờ cả hai đều được

Văn bản công thức trong HotXLS giờ chấp nhận dấu phẩy làm ký tự phân cách đối số bên cạnh dấu chấm phẩy kinh điển. Đây không phải một thiết lập vùng miền; đó là một quy tắc chấp nhận trong trình phân tích cú pháp. Điều này quan trọng vì công thức đến từ những nơi bạn không kiểm soát: dán từ một ticket hỗ trợ, sao chép từ tài liệu, tạo bởi một script phát ra cú pháp chuẩn của Excel, nhập từ một CSV chứa các chuỗi công thức

Hiệu quả thực tế là SUM(A1,A2)SUM(A1;A2) đều biên dịch được. Việc round-trip giữ nguyên bất kể ký tự phân cách nào nguồn đã dùng, nên một sổ làm việc bạn đã tải sẽ được ghi lại với đúng ký tự phân cách gốc thay vì bị chuẩn hóa âm thầm sau lưng người dùng

Điều gì round-trip, và điều gì cần kiểm tra

Văn bản công thức được lưu nguyên văn, nên một LAMBDA trong một tên đã định nghĩa vẫn nguyên vẹn qua một chu kỳ tải rồi lưu và mở trong Excel dưới dạng đúng hàm đó. Một LAMBDA trần được lưu làm kết quả ô, nghĩa là một công thức tính ra một closure thay vì một giá trị, vẫn giữ hành vi bỏ-qua-không-có-giá-trị hiện có: văn bản được giữ nguyên, không có kết quả số nào được cache bịa ra cho nó. Đó là kết quả trung thực, vì không có gì là vô hướng để cache cả

Có hai thói quen đáng để áp dụng. Hãy cho các lambda đặt tên phạm vi sổ làm việc trừ khi có lý do khác, vì một hàm phạm vi trang tính biến mất khi trang tính bị sao chép sẽ tạo ra một lỗi tên ở một nơi rất xa nguyên nhân; các quy tắc phạm vi được trình bày trong tên đã định nghĩa và công thức liên trang. Và khi một sổ làm việc đầy các lambda đặt tên được dùng cho một báo cáo cần ổn định, hãy cân nhắc đóng băng kết quả bằng ConvertFormulasToValues để các bên tiêu thụ ở hạ nguồn thấy số thay vì các hàm mà họ có thể không hỗ trợ

Đối với việc tính lại nặng, các thân LAMBDA là các biểu thức thông thường trong đồ thị phụ thuộc và được lên lịch như bất kỳ công thức nào khác, điều này được mô tả trong tính lại gia tăng và đồ thị phụ thuộc. Nếu mô hình của bạn gọi một hàm đặt tên qua hàng nghìn hàng, chi phí nằm ở thân hàm, không phải ở cơ chế gọi, và cùng lời khuyên tối ưu hóa đó áp dụng như với bất kỳ công thức lặp lại nào

HotXLS là một thành phần bảng tính gốc cho Delphi và C++Builder đọc và ghi XLS, XLSX và ODS mà không cần Excel hay bất kỳ tự động hóa Office nào. Bộ máy công thức, tên đã định nghĩa và API tính lại được tài liệu hóa tại trang thành phần bảng tính HotXLS cho Delphi