บทความเทคนิค

LAMBDA และ LET ใน Delphi: โคลเชอร์สูตรของ HotXLS

HotXLS ประมวลผล LAMBDA ของ Excel เป็นค่าฟังก์ชันชั้นหนึ่งอย่างแท้จริง ชื่อที่กำหนดไว้ซึ่งข้อความ 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 สร้างส่วนที่ขาดหายไปนี้ขึ้นมาเป็นค่าโคลเชอร์บวกกับสแตกขอบเขตแบบรันไทม์ การเรียกโคลเชอร์จะพุชสภาพแวดล้อมที่จับไว้ก่อน แล้วพุชค่าอาร์กิวเมนต์ภายใต้ชื่อพารามิเตอร์ ประมวลผลเนื้อหา แล้วตัดสแตกกลับไปยังจุดที่มาร์กไว้ ลำดับนี้สำคัญมาก และหัวข้อถัดไปจะอธิบายว่าทำไม

แผนภาพการเรียก closure LAMBDA ของ HotXLS ใน Delphi: ค่า closure แบกพารามิเตอร์ เนื้อความ และ binding ฐานที่จับจาก LET รอบนอก การเรียก push สภาพแวดล้อมที่จับไว้ก่อนอาร์กิวเมนต์ v = 5, เนื้อความประเมินได้ 105 และ scope stack ถูกตัดกลับไปที่เครื่องหมายเริ่มของมันภายใต้ guard finally
การเรียก closure จะดัน environment ที่ถูกจับไว้วางก่อน binding ของอาร์กิวเมนต์ และลำดับนี้คือสิ่งที่ทำให้พารามิเตอร์บังชื่อภายนอก

สามวิธีที่ 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 ของสตริงสูตร

แผนภาพเส้นทาง resolve การเรียกสามเส้นทางของ HotXLS ใน Delphi ที่ลองตามลำดับกับชื่อฟังก์ชันที่ไม่รู้จักอย่าง f(21): binding ขอบเขต LET หรือ LAMBDA ท้องถิ่นที่ถือ closure, defined name ของเวิร์กบุ๊กที่ข้อความ RefersTo ขึ้นต้นด้วย LAMBDA และ handler ฟังก์ชันผู้ใช้แบบคลาสสิก ขณะที่ binding ที่ถือตัวเลขถูกปฏิเสธด้วย value error ก่อนมีอะไรถูกประเมินเลย
HotXLS ลองขอบเขตโลคัลก่อน ตามด้วย defined name แบบ LAMBDA และตัวจัดการ user-function แบบคลาสสิก และปฏิเสธการเรียก binding ที่ไม่ได้ถือ closure

ผลในทางปฏิบัติคือทั้ง SUM(A1,A2) และ SUM(A1;A2) คอมไพล์ได้ทั้งคู่ การเขียนกลับจะรักษาตัวคั่นที่ต้นฉบับใช้ไว้ ดังนั้นเวิร์กบุ๊กที่คุณโหลดมาจะถูกเขียนกลับด้วยตัวคั่นดั้งเดิม แทนที่จะถูกทำให้เป็นมาตรฐานเดียวกันโดยที่ผู้ใช้ไม่รู้ตัว

อะไรเขียนกลับได้ และอะไรควรตรวจสอบ

ข้อความสูตรถูกเก็บไว้เหมือนเดิมทุกประการ ดังนั้น LAMBDA ในชื่อที่กำหนดไว้จะรอดผ่านรอบการโหลดและบันทึกได้ครบถ้วน และเปิดใน Excel ในฐานะฟังก์ชันเดียวกัน LAMBDA เปล่าที่เก็บไว้เป็นผลลัพธ์ของเซลล์ หมายถึงสูตรที่ประมวลผลได้เป็นโคลเชอร์แทนที่จะเป็นค่า จะคงพฤติกรรมข้ามไปโดยไม่มีค่าแบบเดิมไว้: ข้อความถูกเก็บรักษาไว้ แต่ไม่มีการสร้างผลลัพธ์ตัวเลขที่แคชไว้ปลอม ๆ ให้ นั่นคือผลลัพธ์ที่ตรงไปตรงมา เพราะไม่มีค่าสเกลาร์ให้แคชอยู่แล้ว

มีสองนิสัยที่ควรนำมาใช้ ให้แลมบ์ดาแบบมีชื่อมีขอบเขตระดับเวิร์กบุ๊ก เว้นแต่จะมีเหตุผลที่ไม่ทำเช่นนั้น เพราะฟังก์ชันที่มีขอบเขตระดับชีตซึ่งหายไปเมื่อชีตถูกคัดลอก จะสร้างข้อผิดพลาดเรื่องชื่อในจุดที่ห่างไกลจากสาเหตุ กฎเรื่องขอบเขตอธิบายไว้ใน ชื่อที่กำหนดไว้และสูตรข้ามชีต และเมื่อเวิร์กบุ๊กที่เต็มไปด้วยแลมบ์ดาแบบมีชื่อมีปลายทางเป็นรายงานที่ต้องเสถียร ควรพิจารณาตรึงผลลัพธ์ด้วย ConvertFormulasToValues เพื่อให้ผู้บริโภคปลายทางเห็นตัวเลขแทนฟังก์ชันที่พวกเขาอาจไม่รองรับ

สำหรับการคำนวณใหม่หนัก ๆ เนื้อหาของ LAMBDA เป็นเพียงนิพจน์ธรรมดาในกราฟการพึ่งพา และถูกจัดคิวเหมือนสูตรอื่น ๆ ทุกตัว ตามที่อธิบายไว้ใน การคำนวณใหม่แบบเพิ่มขึ้นและกราฟการพึ่งพา ถ้าโมเดลของคุณเรียกฟังก์ชันที่มีชื่อตัวเดียวข้ามหลายพันแถว ต้นทุนอยู่ที่เนื้อหาฟังก์ชัน ไม่ใช่กลไกการเรียกใช้ และคำแนะนำการปรับให้เหมาะสมแบบเดียวกันก็ใช้ได้กับสูตรที่ทำซ้ำใด ๆ

HotXLS เป็นคอมโพเนนต์สเปรดชีตแบบเนทีฟสำหรับ Delphi และ C++Builder ที่อ่านและเขียน XLS, XLSX และ ODS ได้โดยไม่ต้องใช้ Excel หรือระบบอัตโนมัติของ Office ใด ๆ เอนจินสูตร ชื่อที่กำหนดไว้ และ API การคำนวณใหม่มีเอกสารอธิบายไว้ที่ หน้าคอมโพเนนต์สเปรดชีตสำหรับ Delphi ของ HotXLS