1 คะแนน โดย GN⁺ 2025-01-08 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • เมตาโปรแกรมมิงแบบ compile time (comptime) ของ Zig อ่านได้เหมือนโค้ด runtime แต่สามารถสร้างโค้ดที่รันได้อย่างมีประสิทธิภาพเทียบเท่าฟังก์ชัน C ที่เขียนด้วยมือสำหรับโค้ดซ้ำ ๆ เช่น การรวมค่าฟิลด์ของ struct
  • แทนที่จะมีไวยากรณ์ generic แยกต่างหาก จุดใช้งานหลักของ Zig comptime คือการจัดการ generic programming ด้วย comptime T: type และ anytype
  • runtime, comptime และ build system ใช้ภาษา Zig เดียวกันร่วมกัน ดังนั้นแม้แต่ฟังก์ชันอย่าง Fizz Buzz ก็สามารถรันระหว่างคอมไพล์เพื่อสร้างเป็นอาร์เรย์ที่คำนวณไว้ล่วงหน้าได้
  • คอมไพเลอร์จะประเมินส่วนที่ทำได้ก่อน และเหลือไว้เฉพาะส่วนที่ต้องใช้ค่า runtime เป็น bytecode เอาต์พุต ที่จะกลายเป็น machine code ภายหลัง
  • Zig comptime สามารถแก้ปัญหาคล้ายกับการสร้างโค้ดแบบข้อความได้ แต่ไม่แทนที่คีย์เวิร์ดแบบตามใจเหมือน macro ของ C จึงลดภาระในการอ่านโค้ดและดีบัก

มุมมองในการอ่าน Zig comptime

  • comptime ของ Zig เป็นฟีเจอร์เมตาโปรแกรมมิง แต่เวลาอ่านพฤติกรรมสุดท้าย ไม่จำเป็นต้องกังวลกับการแยก compile time กับ runtime มากนัก
  • ข้อที่สามของ zig zen คือ “Favor reading code over writing code” เป็นเกณฑ์สำคัญ
    • เมตาโปรแกรมมิงที่อิง macro หรือการสร้างโค้ดอาจสร้างสองชั้น คือโค้ดต้นฉบับและโค้ดที่ถูกขยายออก
    • ชั้นทางอ้อมแบบนี้ทำให้การอ่านโค้ด การดีบัก และการเปลี่ยนพฤติกรรมยากขึ้น
  • มีการเปรียบเทียบโค้ด runtime สำหรับหาผลรวมของอาร์เรย์ [3]i64 = .{1,2,3} กับโค้ด comptime ที่รวมค่าฟิลด์ a, b, c ของ struct MyStruct วางคู่กัน
    • ใช้ inline for (comptime std.meta.fieldNames(MyStruct)) เพื่อวนผ่านชื่อฟิลด์
    • ใช้ @field(my_struct, field_name) เพื่อเข้าถึงฟิลด์ด้วยชื่อฟิลด์ที่รู้ตั้งแต่ compile time
  • ตัวอย่างการรวมค่าฟิลด์ของ struct ดูเหมือน runtime reflection แต่ในไฟล์ executable จะเหลือโค้ดที่มีประสิทธิภาพเหมือนเขียนฟังก์ชันรวมค่าสำหรับ struct type นั้นด้วยมือ
  • กระบวนการแปลงในตัวอย่างไม่ได้จำลองการทำงานจริงของ Zig แบบตรงตัว แต่ใกล้เคียงกับ คำอธิบายเชิงแนวคิด
  • ทีม core ของ Zig กำลังทำงานกับ debugger ที่สามารถรันโค้ดที่ผสม comptime และ runtime ทีละขั้นได้เหมือนในตัวอย่าง

จัดการ generic ด้วย comptime

  • Zig ไม่มี ฟีเจอร์ generic แยกต่างหาก
  • หากต้องการทำ type ให้เป็น generic ให้เขียนฟังก์ชันที่รับ type และคืนค่าเป็น type
    • ตัวอย่าง: pub fn GenericMyStruct(comptime T: type) type
    • ฟิลด์ a, b, c ของ struct ที่คืนค่ากลับมา ล้วนใช้ type T
  • ฟังก์ชัน generic ก็เขียนด้วยวิธีเดียวกันได้
    • ตัวอย่าง: fn quadratic(comptime T: type, a: T, b: T, c: T, x: T) T
    • สามารถระบุ type argument ได้ เช่น quadratic(f32, ...) และ quadratic(i64, ...)
  • ถ้า type ของอาร์กิวเมนต์ไม่สำคัญต่อส่วนอื่นของ function signature ก็สามารถให้อนุมานด้วย type พิเศษ anytype ได้
  • วิธีนี้อธิบาย comptime ทั้งหมดไม่ได้ แต่เป็นจุดเริ่มต้นสำหรับทำความเข้าใจ งาน generic ที่พบได้บ่อยใน Zig

รันโค้ด Zig เดียวกันใน compile time

  • Zig ใช้ภาษาเดียวกันใน runtime, comptime และ build system
  • ตัวอย่าง Fizz Buzz เริ่มจากเขียนฟังก์ชัน fizzBuzz(writer) เป็นฟังก์ชัน output แบบ runtime ทั่วไป
    • วนตั้งแต่ 1 ถึง 100
    • แสดง fizzbuzz สำหรับจำนวนที่เป็นพหุคูณของ 3 และ 5, แสดง fizz สำหรับพหุคูณของ 3, แสดง buzz สำหรับพหุคูณของ 5, และแสดงตัวเลขสำหรับกรณีอื่น
  • เมื่อนำฟังก์ชัน fizzBuzz เดียวกันไปรันในบล็อก comptime จะสามารถคำนวณ output ทั้งหมดไว้ล่วงหน้าได้
    • ใช้ std.io.countingWriter(std.io.null_writer) เพื่อคำนวณจำนวนไบต์ที่ต้องใช้ก่อน
    • สร้างอาร์เรย์ buffer: [cw.bytes_written]u8 ด้วยความยาวที่คำนวณได้
    • output อีกครั้งไปยัง std.io.fixedBufferStream(&buffer) เพื่อสร้างค่า full_fizzbuzz
  • หากวัดเฉพาะช่วงหลัก เวอร์ชันที่คำนวณไว้ล่วงหน้าจะรันเร็วขึ้นประมาณ 9 เท่า
    • ตัวอย่างมีขนาดเล็ก จึงทำให้เวลารันทั้งหมดขึ้นกับปัจจัยอื่นมากกว่า
  • comptime และ runtime มีความแตกต่างกันในค่ากับฟังก์ชันที่เข้าถึงได้
    • ค่า comptime_int, comptime_float, type เข้าถึงได้เฉพาะใน comptime
    • ฟังก์ชันบางตัวรับเฉพาะอาร์กิวเมนต์ comptime จึงแทบจะเป็นฟังก์ชันสำหรับ comptime โดยเฉพาะ
    • system call หรือฟีเจอร์ที่ใช้ system call เข้าถึงได้เฉพาะใน runtime
  • โค้ดที่ไม่ได้ใช้ฟีเจอร์เฉพาะเหล่านี้จะทำงานเหมือนกันทั้งใน comptime และ runtime

ทำความเข้าใจ comptime ผ่าน partial evaluation

  • comptime สามารถมองได้ว่าเป็น partial evaluation ที่เกิดขึ้นระหว่างกระบวนการคอมไพล์
  • partial evaluation คือวิธีที่เมื่อฟังก์ชันได้รับอาร์กิวเมนต์เพียงบางส่วน จะนำ expression ที่ใช้เฉพาะค่าที่รู้แล้วมาแทนที่และคำนวณก่อน แล้วสร้างฟังก์ชันใหม่ที่รับเฉพาะอาร์กิวเมนต์ที่ยังไม่รู้
  • ตัวอย่างการรวมค่าอาร์เรย์แสดงกระบวนการประเมินทีละขั้น
    • แยก for loop ออกเป็นคำสั่งรายรอบของแต่ละ iteration
    • แทน array[0], array[1], array[2] ด้วย 1, 2, 3 ตามลำดับ
    • อัปเดต sum จาก 0 → 1 → 3 → 6
    • สุดท้ายถูกทำให้ง่ายลงจนถึงรูป std.debug.print(..., .{6})
  • ตัวอย่างการรวมค่าฟิลด์ของ struct ก็อ่านได้ในแบบเดียวกัน
    • inline for รันใน comptime จึงถูกขยายเป็นคำสั่งรายตัวสำหรับฟิลด์ "a", "b", "c"
    • @field(my_struct, "a") ถูกเปลี่ยนเป็น my_struct.a
    • ฟังก์ชันสุดท้ายมีรูปที่มี sum += my_struct.a, sum += my_struct.b, sum += my_struct.c โดยตรง
  • วิธีนี้ช่วยให้แสดงเจตนาเป็นโค้ดโดยตรง พร้อมลดความจำเป็นที่จะต้องฝากคอมเมนต์บอกให้แก้ฟังก์ชันรวมค่าด้วยมือเมื่อฟิลด์เปลี่ยน

การประเมินใน compile time และการปล่อยโค้ด runtime

  • Zig comptime ยังสามารถมองได้ว่าเป็นการผสมระหว่าง compile-time evaluation กับการปล่อยโค้ด runtime
  • โค้ดอินพุตถูกคอมไพเลอร์รัน
    • คำสั่งที่รู้ได้ใน compile time จะถูกประเมินทันที
    • คำสั่งที่ต้องใช้ค่า runtime จะถูกเพิ่มเข้าไปในโค้ดเอาต์พุต
  • มุมมองนี้ใกล้เคียงกับวิธีประมวลผลจริงของคอมไพเลอร์ Zig มากที่สุด
    • Zig เริ่มจาก parse ไวยากรณ์
    • แปลงโค้ดเป็น bytecode สำหรับ virtual machine
    • VM ประเมินส่วนที่ทำได้ และปล่อยส่วนที่ต้องประมวลผลใน runtime เป็น bytecode ใหม่
    • bytecode ใหม่นี้จะถูกแปลงเป็น machine code ต่อไป
  • เงื่อนไขอย่าง if ที่มีอินพุต runtime จะปล่อยทั้งสองเส้นทางออกมา
  • dead code จะไม่ถูกวิเคราะห์ความหมาย
    • แม้เขียนฟังก์ชันผิด ก็อาจไม่เกิด compile error จนกว่าจะถูกใช้งานจริง
    • คุณสมบัตินี้ทำให้การคอมไพล์มีประสิทธิภาพขึ้น และทำให้ conditional compilation เป็นธรรมชาติโดยไม่ต้องใช้ #ifdef
  • ชื่อ type ใน Zig ก็เป็น expression ที่ประเมินค่า type ใน comptime
    • แม้ชื่อ type ที่ดูเรียบง่ายอย่าง type ของอาร์กิวเมนต์ฟังก์ชัน ก็เป็นผลลัพธ์ของ comptime evaluation
    • คุณสมบัตินี้ทำให้ตัวอย่าง generic ก่อนหน้าเป็นไปได้
    • หากจำเป็น สามารถคำนวณ type ด้วย expression ที่ซับซ้อนกว่านี้ได้
  • static analysis ของ Zig ซับซ้อนกว่าภาษา static type อื่นจำนวนมาก
    • หากต้องการรู้ type ทั้งหมด คอมไพเลอร์ส่วนใหญ่ต้องถูกเรียกใช้งาน
    • จนกว่าเครื่องมือจะตามทัน ฟีเจอร์ใน editor อย่าง code completion อาจไม่ได้ทำงานดีเสมอไป

ความสัมพันธ์กับการสร้างโค้ดแบบข้อความ

  • Zig comptime มีรูปแบบต่างจากการสร้างโค้ดแบบข้อความ แต่สามารถแก้ปัญหาคล้ายกันได้
  • ฟังก์ชันสร้างโค้ด writeSumFn ใช้ writer.print เพื่อพิมพ์ซอร์สโค้ดของฟังก์ชัน sumFields
    • โค้ดที่รันใน generator สอดคล้องกับส่วน Zig comptime
    • โค้ดที่ generator output ออกมาสอดคล้องกับโค้ด runtime
  • เวอร์ชัน comptime ของ Zig แสดง logic เดียวกันได้ตรงกว่า
    • ใช้ std.meta.fieldNames(MyStruct) เพื่อรับชื่อฟิลด์
    • ใช้ inline for และ @field เพื่อประกอบโค้ดเข้าถึงฟิลด์
  • การสร้างโค้ดที่ใช้ข้อมูล type เป็นอินพุตทำได้ง่ายกว่าใน Zig
    • วิธีสร้างข้อความต้องแก้ต่างหากว่าจะเอาข้อมูลชื่อ type และชื่อฟิลด์มาจากไหน
    • Zig สามารถจัดการตัว type เองและข้อมูลฟิลด์โดยตรงใน comptime
  • หากใช้อินพุตอย่างสเปกภายนอก ก็สามารถรวมเนื้อหาไฟล์ด้วย @embedFile แล้ว parse ตามปกติได้
  • ในตัวอย่าง struct แบบ generic comptime จัดการการอ้างอิงชื่อ type ได้ตรงกว่าการสร้างโค้ดแบบข้อความ
    • การสร้างข้อความต้องต่อชื่ออย่าง MyStruct_{s} เป็นสตริงและทำให้สอดคล้องกัน
    • comptime ใช้ฟังก์ชันโดยตรง เช่น GenericMyStruct(T)
  • มีข้อยกเว้นเช่นกัน
    • สามารถสร้าง type ที่ชื่อฟิลด์ถูกกำหนดใน comptime ได้
    • ในกรณีนี้ต้องเรียก built-in function ด้วยสเปกที่บรรจุรายการนิยามฟิลด์
    • type แบบนั้นไม่สามารถนิยาม declaration อย่าง method ได้
    • ไม่ได้จำกัดความสามารถในการแสดงโค้ดโดยตรง แต่จำกัดรูปแบบ API ที่เปิดเผยให้โค้ดอื่นได้
  • งานที่สมเหตุสมผลส่วนใหญ่ที่ทำได้ด้วย text macro แบบ C ก็ทำได้ด้วย comptime เช่นกัน แต่รูปร่างของโค้ดอาจแทบไม่เหมือนกัน
  • Zig ไม่มีฟีเจอร์แทนที่คีย์เวิร์ดด้วยชื่ออื่นเหมือน text macro

อ่านเพิ่มเติมและตัวอย่างจาก standard library

  • ตัวภาษา Zig เองไม่ได้พึ่งพา comptime เพียงอย่างเดียว และสามารถเรียนรู้เพิ่มเติมได้ที่ เว็บไซต์ทางการ
  • language reference อธิบายฟีเจอร์เฉพาะของ comptime
  • standard library ของ Zig มีตัวอย่างการใช้ comptime
    • ฟังก์ชัน format ที่ std.debug.print ใช้ เป็นฟังก์ชัน generic ที่ทรงพลัง
      • หลายภาษาจะ parse format string ใน runtime และอาจเพิ่ม validator แยกต่างหากเพื่อจับ error ล่วงหน้า
      • Zig parse format string ใน comptime เพื่อสร้างโค้ด output ที่มีประสิทธิภาพ และทำ validation ทั้งหมดใน compile time
    • ArrayList เป็น generic container ที่ค่อนข้างเรียบง่ายแต่มีฟีเจอร์ครบถ้วน
    • ฟังก์ชัน main ของ Zig สามารถมี return type ได้หลายแบบ และสิ่งนี้ไม่ใช่เวทมนตร์ของคอมไพเลอร์ แต่จัดการด้วย โค้ด comptime ทั่วไป

1 ความคิดเห็น

 
GN⁺ 2025-01-08
ความคิดเห็นจาก Hacker News
  • อยากให้พูดถึงประเด็นนี้ให้ลึกกว่านี้ แทนที่จะชื่นชม การเขียนโปรแกรมช่วงคอมไพล์ ต่อปัญหาที่เคยถูกค้นพบมาแล้วแบบไม่วิจารณ์
    การเขียนโปรแกรมแบบหลายขั้นตอนไม่ใช่เรื่องใหม่ และตระกูล Lisp ก็ทำสิ่งคล้ายกันมาหลายสิบปีแล้ว แต่ระหว่างนั้นก็เผยให้เห็นการแลกเปลี่ยนเชิงออกแบบและปัญหาหลายอย่าง ตัวอย่างเช่น generic แบบนี้ทำลาย parametricity ทำให้อนุมานได้ยากจากการดูแค่ type signature ของฟังก์ชัน ยังไม่ชัดเจนด้วยว่า Zig จัดการกับ recursive generic type อย่างไร และการโต้ตอบกันระหว่างการตรวจชนิดกับการคำนวณช่วงคอมไพล์เกิดขึ้นเมื่อไรและตามลำดับใดก็เป็นทางเลือกสำคัญ บทความบอกว่าโค้ดช่วงคอมไพล์สามารถสร้างได้ไม่ใช่แค่ค่าแต่รวมถึงโค้ดด้วย แต่ไม่ได้พูดถึง hygiene เลย ดูบทความนี้ประกอบได้ที่ https://typesanitizer.com/blog/zig-generics.html

    • ผมค่อนข้างชอบ Zig ติดตามมาหลายปีและใช้บ้างเป็นครั้งคราว แต่ comptime โดดเด่นมากในกรณีอย่าง generic, การกำหนดค่าเริ่มต้นของโครงสร้างข้อมูลซับซ้อนในช่วงคอมไพล์, หรือการสร้างโค้ดแยกตามเป้าหมาย
      แต่ในสถานการณ์อื่น พอเห็น comptime ในโค้ด Zig ก็มีความรู้สึกแบบ “อ่า...” อยู่บ้าง มันเหมือน macro ของ Lisp ตรงที่เอาไปใช้หลบปัญหาที่จริง ๆ ไม่ควรมี หรือหายไปได้ถ้าจัดโครงสร้างโค้ดให้ดีกว่านี้ ตัวอย่างในบทความต้นฉบับที่ไล่วนฟิลด์ของ struct เพื่อรวมค่าต่าง ๆ เป็นตัวอย่างคลาสสิกของวิธีที่คนใช้ comptime กันจริง แต่หลายกรณีใช้โครงสร้างข้อมูลที่วนได้จริงอย่าง std.enums.EnumArray จะดีกว่า
    • สงสัยว่าที่บอกว่า parametricity ถูกทำลายนั้นหมายถึงในระดับที่โปรแกรมเมอร์สายปฏิบัติเข้าใจพฤติกรรมของฟังก์ชัน หรือในระดับการอนุมานภายใต้ ระบบทฤษฎีชนิด อย่าง type lambda calculus
      ในเชิงทฤษฎีความกังวลนี้ชัดเจน แต่จะสำคัญแค่ไหนในภาษาที่เน้นใช้งานจริงก็เป็นอีกเรื่อง C++ template ก็ทำลาย parametricity ผ่าน template specialization เหมือนกัน แต่ในทางปฏิบัติก็มักไม่ใช่ปัญหาใหญ่ และกลับช่วยให้ทำ optimization ได้ เช่น implementation อย่าง std::vector<bool> ที่เก็บแต่ละองค์ประกอบเป็น 1 บิตจริง ๆ ก็เกิดขึ้นได้เพราะมีความยืดหยุ่นแบบนี้
    • ในฐานะคนเขียนบทความ บทความนี้เขียนขึ้นเพื่อรวบรวมความคิดหลังจากอธิบายสิ่งที่จะพูดในมีตอัปได้ไม่ดีนัก จึงเน้นการอธิบายมากกว่าการโต้แย้ง
      มองว่า comptime แบบ Zig คือการแลกเปลี่ยนโดยตรงระหว่าง ความสามารถในการอนุมาน กับพลังในการแสดงออก เมื่อเทียบกับ function signature ที่กำหนดตายตัวกว่านี้ มันมีผลเฉพาะกับโค้ดที่ทำการอนุมานแบบนั้นจาก type และเป็นสิ่งที่เลือกใช้ได้ตามต้องการ เวลาที่ type signature ไม่พอ ผมมักอ่าน source เพิ่มอีกไม่กี่สิบบรรทัดก็พอเข้าใจวิธีใช้ได้ และไม่ค่อยเคยสับสนอยู่นาน ตัวอย่าง recursive generic type ถ้าใช้ชื่อที่กำลังประกาศอยู่จะได้ข้อความ “dependency loop detected” แต่มีวิธีอ้อมได้ และตัวอย่าง generic ในบทความก็อ้างถึงตัวเองเหมือนกัน การตรวจชนิดเกิดขึ้นระหว่าง comptime เช่น @compileLog("Hi") ตัวแรกจะรัน แต่ถ้าหลังจากนั้น const a: u32 = "42"; ทำให้เกิด type error ก็จะไปไม่ถึง log ตัวที่สอง อาจมีปัญหาละเอียดอ่อนจากการตรวจชนิดระหว่าง comptime กับรันไทม์ได้บ้าง แต่จะเห็นในโค้ดที่ซับซ้อนมากและแก้ได้ไม่ยาก อีกทั้งทีมหลักก็รับรู้อยู่และคาดว่าจะจัดการก่อน 1.0 ส่วน hygiene หมายถึงอะไรอย่างแม่นยำนั้นคงต้องมีคำอธิบายเพิ่ม
    • การอนุมานได้จากการดูแค่การประกาศฟังก์ชันเป็นเรื่องแยกจาก การประมวลผลช่วงคอมไพล์
      ถ้าฟังก์ชันมีจุดประสงค์เชิงตรรกะที่ชัดเจน และชื่อกับชื่อพารามิเตอร์ถูกตั้งมาดี ก็อนุมานได้จากแค่ declaration เช่นกัน ส่วนพวก label ของพารามิเตอร์ ชื่อ type และสิ่งอื่นที่โปรแกรมเมอร์กำหนดได้ ก็ถือเป็นส่วนหนึ่งของชื่อได้ บทความที่ลิงก์มานั้นผมเห็นด้วยกับข้อสรุปในชื่อเรื่อง แต่ตัวบทความเองใกล้เคียงงานเขียนแบบทีมโต้วาทีที่พยายามเก็บคะแนน มากกว่าจะเป็นข้อถกเถียงที่มีสาระ กรอบที่ดีกว่าคือ ความยืดหยุ่น กับความซับซ้อน ระบบ generic แบบตายตัวหากออกแบบดีจะเรียบง่ายกว่าระบบที่โปรแกรมได้ แต่ก็ยืดหยุ่นน้อยกว่า จุดแข็งของ Zig คือภาษา metaprogramming แทบจะเหมือนกับภาษาปกติ และภาษาปกตินั้นเองก็เรียบง่าย จึงลดต้นทุนความซับซ้อนที่เพิ่มเข้ามาได้ อย่างไรก็ตาม ก็ยังมีความซับซ้อนจากการที่โค้ดช่วงคอมไพล์กับโค้ดรันไทม์ปะปนกันและมีเบาะแสน้อย ทำให้โปรแกรมเมอร์แยกได้ยากว่าอะไรจะรันเมื่อไร เลยคิดว่าถ้ามี “comptime shader” ใน language server หรือปลั๊กอิน editor ที่ลงสีพื้นหลังต่างกันให้โค้ด comptime ก็น่าจะดี
    • เรื่องน่าหงุดหงิดคือหัวข้อนี้ถูกถกกันบนพื้นฐานของ เดโม 15 นาที และยังขาดความเข้าใจต่อการแลกเปลี่ยนที่เกิดขึ้นจริง บทความของ Varun Gandhi ที่ลิงก์ไว้นั้นยอดเยี่ยมมาก
      จากประสบการณ์กับ Rust หลายสิ่งที่คนพยายามทำด้วย const generic น่าจะง่ายกว่ามากถ้ามีฟีเจอร์แบบ comptime การอนุญาตให้มี arithmetic บน const generic โดยยังรักษา parametricity ไว้เป็นเรื่องที่ implement ยาก และถ้าสิ่งที่ต้องการจริง ๆ มีแค่ประมาณ “trait ของ hash function ที่ให้ขนาดผลลัพธ์เป็น N” การยอมทิ้ง parametricity เพื่อให้ trait ถูกสร้างจาก N ในขั้นสร้างโค้ดก่อนหน้านั้นก็อาจสมเหตุสมผลได้ แต่ Rust macro ทั้งยืดหยุ่นเกินไปและน่ารำคาญเกินกว่าจะใช้ในแนวนั้น ถึงอย่างนั้น ทันทีที่คุณแทนที่ parametric polymorphism ด้วยฟีเจอร์สร้างโค้ดแบบตรงไปตรงมาที่ไร้เดียงสา ชีวิตลำบากก็เริ่มต้นทันที
  • D มีสิ่งนี้มาตั้งแต่ 17 ปีก่อน ฟีเจอร์ของ D ก็ถูกย้ายไปยังภาษาอื่นอย่างต่อเนื่อง
    Zig ใช้คีย์เวิร์ด comptime เพื่อระบุบล็อกที่จะรันระหว่างคอมไพล์ แต่ D ไม่ได้ใช้คีย์เวิร์ด โดยตัดสินจากการเป็น นิพจน์ค่าคงที่ ว่าจะรันหรือไม่ ตัวอย่างเช่น int s = sum(3, 4); จะรันตอนรันไทม์ และ enum e = sum(3, 4); จะรันตอนคอมไพล์ หากหลีกเลี่ยง global ที่ไม่คงที่, การรับส่งข้อมูลเข้าออก, และการเรียกฟังก์ชันระบบอย่าง malloc() ฟังก์ชันจำนวนมากก็สามารถรันตอนคอมไพล์ได้โดยแทบไม่ต้องแก้ไขอะไรเลย และด้วยการจัดการหน่วยความจำอัตโนมัติของ D จึงสามารถจัดสรรหน่วยความจำได้ด้วย

    • หนึ่งในการใช้งานที่ชอบคือ การสร้างตารางแบบสแตติก
      เมื่อก่อนต้องเขียนโปรแกรมแยกต่างหากเพื่อสร้างตารางแบบสแตติก แต่พอมีการรันฟังก์ชันตอนคอมไพล์ก็ไม่จำเป็นอีกแล้ว ค่าเริ่มต้นของอาร์เรย์ tytab มาจากแลมบ์ดาที่คำนวณอาร์เรย์แล้วคืนค่า ตัวอย่างเต็มอยู่ที่ https://github.com/dlang/dmd/blob/master/compiler/src/dmd/ba... อีกการใช้งาน CTFE ที่พบบ่อยคือการสร้าง DSL
    • ไม่อยากพลาดโอกาสที่จะบอกว่าขอบคุณสำหรับการสร้าง D แต่ก็สงสัยเรื่อง ข้อจำกัดของการรันตอนคอมไพล์
      ถ้าเครื่องที่รันคอมไพเลอร์กับเครื่องที่รันโปรแกรมเป็นคนละเครื่อง D คอมไพเลอร์รับประกันความถูกต้องอย่างไร? เช่น อยากรู้ว่าคอมไพเลอร์รู้ได้อย่างไรว่า int s = sum(100000, 1000000) จะได้ค่าเดียวกันบนเครื่อง x86 ทุกเครื่อง อาจมีความต่างเล็กน้อยตามแต่ละรุ่นของ CPU แล้วจะรับประกันได้อย่างไรว่าผลการคำนวณบนโฮสต์จะออกมาเหมือนกันบนเครื่องเป้าหมาย หรือแค่สมมติว่าถ้าสถาปัตยกรรมเดียวกัน โฮสต์กับเป้าหมายนั้นใกล้เคียงกันพอ
    • ImportC ของ D ก็สามารถทำ CTFE กับโค้ด C ได้
      ถ้ามี int sum(int a, int b) { return a + b; } ก็สามารถเขียนแบบ _Static_assert(sum(3, 4) == 7, "look ma, check at compile time!"); ได้ ไม่รู้ว่าทำไมมาตรฐาน C ถึงไม่เพิ่มสิ่งนี้เข้าไป และมันก็ทำงานได้ดี
    • พูดอย่างยุติธรรม Zig ก็เรียกฟังก์ชันเดียวกันได้ทั้งในบริบทรันไทม์และบริบท comptime
      square(2) คือการเรียกตอนรันไทม์ และ comptime square(3) คือการเรียกตอนคอมไพล์ การเรียกแบบ comptime จะทำให้เกิดข้อผิดพลาดตอนคอมไพล์หากมีอะไรไม่รองรับการคอมไพล์ไทม์ ซึ่งมองว่าเป็นฟีเจอร์สำคัญ เพราะมันเป็นสัญญาณเตือนเมื่อโค้ดที่คาดว่าจะรันตอนคอมไพล์ถูกผลักไปเป็นการประเมินผลตอนรันไทม์โดยไม่ตั้งใจเพราะอินพุตเปลี่ยน
  • Zig ดูน่าสนใจ แต่ก็อยากให้มี operator overloading
    เหตุผลคัดค้าน operator overloading ส่วนใหญ่ฟังแล้วไม่ค่อยน่าเชื่อ ข้ออ้างว่ามันทำให้ไม่รู้ว่าเบื้องหลังเกิดอะไรขึ้นนั้นไม่พอ เพราะฟังก์ชันชื่อ add ก็อาจถูกทำให้คูณแทนได้เหมือนกัน มักยก iostreams หรือ boost::spirit ของ C++ มาเป็นตัวอย่างการใช้เกินเลย แต่ในภาษาอื่นที่มี operator overloading ก็ไม่ค่อยเห็นปัญหาแบบนั้น จึงดูเหมือนเป็นปัญหาเฉพาะของ C++ มากกว่า

    • แนวทางแบบ OCaml น่าจะเข้ากับงานของ Zig ได้ดี
      ใน OCaml สามารถนิยาม operator ใหม่ได้ แต่ทำได้เฉพาะภายในบริบทของโมดูลอื่น เช่น ถ้า override + ในโมดูล Vec3 ก็จะใช้แบบ Vec3.(a + b + c + d) หรือ let open Vec3 in a + b + c + d ได้ เวลาอ่านซอร์สก็จะไม่สงสัยว่า “+ ตัวนี้มาจากไหน?” และมันดีกว่า a.add(b).add(c).add(d) มาก แต่ Zig ก็ดูเหมือนกำลังเริ่มนิ่งตัวแล้ว เลยไม่น่าจะใส่การเปลี่ยนแปลงใหญ่เพื่อแก้เรื่องนี้ได้ง่าย
    • ใน Python และ PHP อาจไม่รู้เลยว่ามีเวทมนตร์มากแค่ไหนซ่อนอยู่หลังฟังก์ชัน __
      แนวทางของ Zig จึงดูสดใหม่ และความสามารถในการไล่ตามโค้ดได้สำคัญกว่าการพิมพ์เพิ่มอีกไม่กี่ตัวอักษรไม่กี่วินาที
    • หลายเหตุผลที่คนไม่ชอบ operator overloading น่าจะเกี่ยวกับ ประสิทธิภาพรันไทม์ ที่คาดไม่ถึง
      ทางออกในอุดมคติคือภาษาควรมี operator ที่ผู้ใช้กำหนดเองได้และแสดงชัดเจนว่าเป็นการ overload เช่น ถ้าเป็นรูปแบบอย่าง let c = a |+| b คนอ่านโค้ดก็จะรู้ได้ว่า operator |+| นี้จริง ๆ คือการเรียกฟังก์ชัน แม้จะยังใช้เกินเลยได้อยู่ แต่ก็ลดความกังวลหลักข้อหนึ่งลงได้
    • ใน C++ เคยใช้ operator overloading แค่กับ คณิตศาสตร์เวกเตอร์/เมทริกซ์ และในกรณีนั้นมันมีประโยชน์มาก
      ถ้าภาษามีไวยากรณ์คณิตศาสตร์เวกเตอร์ในตัวแบบภาษา shading ก็น่าจะโอเค อย่างน้อย Zig ก็มีชนิด @Vector() ที่คล้าย Clang Vector Extension อยู่บ้าง แต่น่าเสียดายที่ไม่ใช่ Extended Vector Extension ดู https://ziglang.org/documentation/master/#Vector และ https://clang.llvm.org/docs/LanguageExtensions.html#vectors-...
    • operator พื้นฐานสำหรับพีชคณิตเชิงเส้นของอาร์เรย์ตัวเลข อาจควรถูกใส่ไว้ในภาษาโดยตรงแทนการใช้ overload
      ไม่แน่ใจว่ามีข้อเสนอแบบนั้นอยู่แล้วหรือยัง
  • fieldNames ดูคล้าย fieldPairs ของ Nim มาก และเป็นโครงสร้างที่สะดวกอย่างยิ่ง
    มันทำให้การทำ serialization ที่มีประสิทธิภาพกลายเป็นเรื่องง่ายมาก และไม่นานมานี้ก็ใช้ fieldPairs ทำ การตรวจสอบ thread safety ของ type ตอนคอมไพล์ ได้ในราว 20 บรรทัด คิดว่าฟีเจอร์แบบนี้ควรเป็นความสามารถมาตรฐานของภาษาโปรแกรม หนึ่งในสิ่งที่เสียดายที่สุดของ Rust ก็อยู่ตรงนี้ ตอนที่ลองล่าสุดมันยังถูกจำกัดอยู่ที่แมโครแบบไม่มี type เท่านั้น ซึ่งจำกัดมาก ทำได้แค่หวังว่า struct ใน crate จะมี serde ติดมาให้ และก็ไม่สามารถสร้าง struct ที่มีฟิลด์เหมือนกันขึ้นมาเองแบบโปรแกรมได้

    • เคยมีการพูดถึง compile-time reflection อยู่ช่วงหนึ่ง และน่าจะรวมความสามารถแบบนี้เข้าไปได้ แต่เหมือนจะหายไปพร้อมดราม่าที่เกี่ยวข้อง
      น่าเสียดายพอสมควร ถ้ามี compile-time reflection พวกอย่าง serde ก็น่าจะทำได้ง่ายกว่านี้มาก
    • ถ้ามี compile-time reflection ก็จะสร้างเฟรมเวิร์กอย่าง ORM หรือเว็บเฟรมเวิร์กได้
      ข้อแลกเปลี่ยนเพียงอย่างเดียวคือไลบรารีแบบนั้นจะต้องถูกรวมเข้ามาในรูปซอร์สโค้ด
  • หลังจากเคยลองเขียนไลบรารีพาร์เซอร์ภาษา C ที่ค่อนข้างสมบูรณ์มาระดับหนึ่ง ก็ไม่ค่อยแน่ใจนักกับข้ออ้างที่ว่าภาษาเองจำเป็นต้องมี เมตาโปรแกรมมิง
    ถ้าอยากสร้าง struct, serialization, property, instrumentation ฯลฯ ก็แค่เขียนโปรแกรม C ทั่วไปที่ประมวลผลไฟล์ซอร์สแล้วสร้างไฟล์ซอร์สออกมา จากนั้นก็รันมันก่อนในสคริปต์ build ก็พอ อยากรู้เหมือนกันว่าคนที่ทำเมตาโปรแกรมแบบนี้ดีบักและทดสอบกันอย่างไร ของผมเป็นโปรแกรม C ธรรมดา ก็เลยใช้ดีบักเกอร์และเครื่องมือเดียวกับโค้ดอื่นได้ตรง ๆ

    • นั่นแหละคือสิ่งที่คนส่วนใหญ่ไม่อยากทำ
    • C# หรือจะพูดให้แม่นกว่านั้นคือ Roslyn/dotnet รองรับเรื่องนี้ได้ดีพอสมควร
      ตัวคอมไพเลอร์เองก็เขียนด้วยภาษาเดียวกัน จึงสามารถใส่ปลั๊กอินที่เข้าถึง AST แบบอ่านอย่างเดียวและปล่อยซอร์ส C# ออกมาได้ เรื่องดีบักอาจต้องลงแรงเพิ่มเล็กน้อยเพื่อทำเฟรมเวิร์กทดสอบที่ดี แต่หลังจากนั้นก็สามารถรันคอมไพเลอร์ที่ใส่ปลั๊กอินไว้ภายในเฟรมเวิร์ก unit test มาตรฐาน และดูด้วย interactive debugger ได้
    • นี่คือแนวทางเดียวกับที่ Ryan Fleury และคนอื่น ๆ สนับสนุน และมันก็เป็นวิธีที่ดีเพียงพอ
      “การรันโค้ดตอน compile time แบบตามอำเภอใจใน C: cl /nologo /Zi metaprogram.c && metaprogram.exe, cl /nologo /Zi program.c. โค้ด compile-time รันด้วยความเร็วเนทีฟ ดีบักได้ และเป็นแบบ procedural เต็มรูปแบบกับทำอะไรก็ได้ คอมไพเลอร์ไม่จำเป็นต้องเป็นคนรันโค้ดให้” https://x.com/ryanjfleury/status/1875824288487571873
    • ไม่ค่อยรู้เรื่อง Zig มากนัก แต่พลังของ Lisp อยู่ที่ S-expression หรือก็คือการจัดการ AST
      ถ้าจะทำแบบนั้นใน C ก็ต้องเขียนพาร์เซอร์ C แบบสมบูรณ์สำหรับโปรแกรม C ที่ใช้ประมวลผลไฟล์ซอร์ส
    • เมื่อก่อนเคยทำอะไรคล้าย ๆ กันใน Python ร่วมกับ numba JIT
      คือเขียนโค้ด Python ที่สร้างโค้ด Python แล้วให้มันคอมไพล์อีกที มันเปราะบางและยุ่งเหยิงน่ากลัวมาก และนี่ก็เป็นหนึ่งในเหตุผลใหญ่ที่ทำให้ผมเลิกใช้ Python มันคล้ายกับการถามว่าทำไมไม่ส่งอาร์กิวเมนต์ของฟังก์ชันทั้งหมดเป็นสตริงไปเลย คนที่เขียนโค้ดแบบอิงสตริงก็มีอยู่ แต่ควรเป็นกรณีที่พบไม่บ่อย และภาษาควรมีทางให้หลีกเลี่ยงสิ่งนี้ได้
  • อีกแพตเทิร์นที่น่าสนใจอย่างหนึ่งคือสามารถ สร้าง struct ตอน compile time ได้
    ผมเคยลองทำการทดลองที่สร้างไฟล์ JSON จาก PyTorch แล้วอ่านด้วย @embedFile ของ Zig จากนั้นสร้าง struct ที่มีเมธอด run แบบเฉพาะเพื่ออิมพลีเมนต์ neural network ในทางทฤษฎีแล้วมันทำให้คอมไพเลอร์สามารถ optimize neural network ได้โดยตรง ผมยังพิสูจน์ข้อดีที่ชัดเจนไม่ได้ แต่ก็มีจุดหนึ่งคือทั้งเครือข่ายอยู่บน stack จึงไม่มีการจัดสรรหน่วยความจำแบบไดนามิก ไม่แน่ใจว่านี่เป็นข้อดีจริงไหม

    • ผมเคยทำเรื่องแบบนี้โดยเขียนตัวสร้างโค้ดด้วย Python แทน comptime
      ไม่แน่ใจว่า comptime ของ Zig จะเร็วเป็นพิเศษจริงหรือไม่ และก็ไม่อยากรันพาร์เซอร์ JSON ที่ใช้สร้าง struct ทุกครั้งด้วย
    • อยากรู้ว่ามันมีผลต่อเวลา compile แค่ไหน
  • มันก็น่าสนใจอยู่ แต่ในกรณีแบบนี้ผมกลับเห็นด้วยกับคอมเมนต์แนว “D ทำมาก่อนแล้ว”

    • ใช่ และทำแบบ hygienic ด้วย ไม่ใช่แนว preprocessor
  • ถ้าคุณทึ่งกับ comptime ของ Zig ก็ควรลองดู Nim ด้วย เพราะมันมีทั้ง การประเมินโค้ดตอน compile time และระบบแมโคร AST แบบเต็มตัว

    • Nim เป็นภาษาที่สนุก แต่คงไม่เลือกใช้กับงานที่ “จริงจัง”
      มันมีปัญหาเรื่อง ecosystem แบบเดียวกับภาษากลุ่ม niche อื่น ๆ แล้วยังมีปัจจัยอย่างผู้ดูแลที่คนชอบไม่เหมือนกัน ผู้มีส่วนร่วมหลักที่ดูเหมือนจะไม่อยู่ยาว และการที่แหล่งทุนหลักมาจากบริษัทคริปโต ถ้าเป็นเมื่อ 10 ปีก่อนสิ่งเหล่านี้อาจไม่รบกวนใจผม แต่ตอนนี้ไม่ใช่แล้ว
    • Zig มีข้อดีตรงที่ ไม่มี exception
      ดูเหมือน Nim ก็พยายามถอยออกจาก exception เช่นกัน แต่ exception ทำให้ฟังก์ชันปนเปื้อน และทำให้คุณต้องคอยกังวลแม้จะไม่ได้ใช้ฟังก์ชันที่โยน exception ก็ตาม[1] ชีวิตมันสั้นเกินกว่าจะมานั่งรับมือกับ control flow ที่มองไม่เห็น [1]: https://github.com/status-im/nim-stew
  • โดยรวมแล้ว Zig เป็นภาษาที่ดีมากพอสมควรและทำสิ่งที่มันต้องทำ
    การไม่หลุดออกจากจุดประสงค์เป็นเรื่องสำคัญมาก และเพราะแบบนั้นผมจึงไม่ค่อยชอบเวลาบางภาษาถูกเอาไปใช้ทุกที่เพียงเพราะมันเป็นภาษาที่คนนิยมใช้

  • อยากให้มีอะไรสักอย่างที่รวม ความสามารถด้านเมตาโปรแกรมมิง ของ Zig เข้ากับ ecosystem ขนาดใหญ่ ชุมชน และความปลอดภัยของ Rust
    ถ้ามองแค่การออกแบบภาษา ผมชอบ Zig มากกว่า Rust มาก แต่ในฐานะโปรแกรมเมอร์สมัครเล่นที่ยังไม่เก่ง อย่างน้อยตอนนี้ผมยังไม่มั่นใจว่าจะเขียนอะไรที่มีประโยชน์หรือเชื่อถือได้จริงด้วย Zig ได้

    • ในฐานะคนที่คุ้นกับ Rust ผมลอง Zig แป๊บเดียวแล้วก็เลิกทันที
      เพราะคอมไพเลอร์ไม่ได้ช่วยจับปัญหาแบบนั้นให้ตั้งแต่ compile time ผมรู้ว่า Zig ไม่ได้สัญญาเรื่องนั้นอยู่แล้ว แต่สำหรับผมมันเป็นข้อเสียที่ตัดสินชะตาเลย จึงคิดว่า Zig คงไม่ใช่ภาษาที่เหมาะกับผม ในทางกลับกัน แนวคิดของ comptime กลับถูกใจผมมากกว่าแมโครของ Rust
    • ผมอยากให้ชุมชน Rust อยู่ห่างจาก Zig ไว้หน่อย พูดเล่นนะ แต่ส่วนใหญ่ก็พูดจริง