1 คะแนน โดย GN⁺ 2025-11-08 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • คอมไพเลอร์ Zig รองรับการคอมไพล์โค้ด C และการ cross-compile ในตัว โดยไม่ต้องตั้งค่าเพิ่มเติม และเป็นภาษาที่น่าทึ่งที่สุดในบรรดาภาษาที่ผู้เขียนเคยพบมาตลอด 45 ปี
  • ด้วยความสามารถเฉพาะตัวอย่าง การรันตอนคอมไพล์, ตัวแปรที่กำหนดขนาดบิตได้ตามต้องการ, และสภาพแวดล้อมแบบ test block ทำให้มันไม่ใช่แค่ตัวแทนของ C/C++ แต่เสนอวิธีเขียนโปรแกรมแบบใหม่อย่างแท้จริง
  • ไวยากรณ์ที่กระชับและชัดเจน เช่น การประกาศตัวแปรผ่าน type inference, anonymous struct, และ labeled break ทำให้เรียนรู้ได้รวดเร็ว
  • รองรับการดีบักโค้ดที่ optimize แล้วด้วย การทดสอบโมดูลแบบแยกอิสระผ่าน test block และ built-in function @breakpoint
  • รองรับการเขียนโปรแกรมระดับล่างด้วย bit field และการดำเนินการระดับบิต จึงได้ทั้งประสิทธิภาพและความทนทาน พร้อมรวมข้อดีของภาษาแบบ interpreter เข้ามาไว้ในภาษาแบบคอมไพล์

บทนำ

  • ตลอดประสบการณ์ 45 ปี ไม่มีภาษาใดน่าทึ่งเท่า Zig
    • Zig ไม่ใช่แค่ภาษาใหม่ แต่เป็น เครื่องมือที่เปลี่ยนวิธีเขียนโปรแกรมอย่างถึงราก
  • หากมองว่าเป็นเพียงตัวแทนของ C หรือ C++ ก็ถือว่า ประเมินต่ำเกินไปมาก
  • จุดประสงค์ของบทความนี้คือแนะนำ ความสามารถที่เรียบง่ายแต่มีเสน่ห์ ของ Zig และช่วยให้โปรแกรมเมอร์เริ่มต้นได้เร็ว
  • ยังมีความสามารถอีกมากที่ส่งผลต่อการยอมรับ Zig ในภาคอุตสาหกรรม

คอมไพเลอร์ Zig

  • Zig รองรับการคอมไพล์โค้ด C และการ cross-compile เป็นค่าพื้นฐานโดยไม่ต้องตั้งค่าแยก ซึ่งส่งผลอย่างมากต่อภาคอุตสาหกรรม
  • การติดตั้งทำได้โดยดาวน์โหลดคอมไพเลอร์ตามชนิดโปรเซสเซอร์/OS จากหน้าดาวน์โหลดของ Ziglang แล้วแตกไฟล์และคัดลอกไปยังไดเรกทอรีที่ต้องการ
    • บน Windows 10 สามารถคัดลอกไฟล์ zip แบบ x86_64 ไปไว้ใน "Program Files" และเปลี่ยนชื่อไดเรกทอรีรากเป็น "zig-windows-x86_64" เพื่อไม่ต้องแก้ตัวแปรสภาพแวดล้อม Path ทุกครั้งที่อัปเดตเวอร์ชัน
    • เมื่อเพิ่มพาธของไดเรกทอรีรากลงในตัวแปรสภาพแวดล้อม Path แล้ว ก็ใช้งานคอมไพเลอร์ในโหมด CLI ได้
  • สำหรับการ build โปรแกรม "Hello World!" แนะนำให้อ้างอิงส่วน "Getting Started" ในเว็บไซต์ทางการ

แนวคิดและคำสั่งสำคัญ

การประกาศตัวแปร

  • การประกาศตัวแปรประกอบด้วยส่วนแรกคือการเข้าถึงได้ (pub หรือไม่ระบุ), var/const และชื่อตัวแปร, ส่วนที่สองคือการระบุชนิดข้อมูล, และส่วนที่สามคือการกำหนดค่าเริ่มต้น
    • จำเป็นเฉพาะส่วนแรกและส่วนที่สามเท่านั้น และ ชนิดข้อมูลสามารถอนุมานได้จากค่าเริ่มต้น
    • ตัวอย่าง: var sum : usize = 0;
  • ตัวแปรที่ประกาศโดยไม่มี pub จะเข้าถึงได้เฉพาะภายในโมดูล (คล้ายตัวแปร static ใน C)
  • ไม่แนะนำให้ประกาศตัวแปรแบบ pub และควรลดจำนวนฟังก์ชัน pub เพื่อให้ coupling ต่ำลงและ cohesion สูงขึ้น

struct, anonymous struct, test block

  • anonymous struct literal ที่ล้อมด้วย .{ และ } ใช้สำหรับกำหนดค่าให้สมาชิกของ struct อื่น หรือสร้าง struct ใหม่ที่มีสมาชิกถูกกำหนดค่าแล้ว
  • .{ } คือ anonymous struct literal แบบว่าง
  • รูปแบบ struct { } คือการประกาศ struct
  • test block สามารถคอมไพล์และรันทดสอบได้โดยไม่ต้องมี executable

bit field

  • bit field ถูกประกาศเป็นฟิลด์ที่มีชนิดข้อมูลขนาดเฉพาะภายใน packed struct
  • pointer สามารถชี้ไปยัง bit field เฉพาะได้
โฆษณา

ลูป For

  • ไวยากรณ์ของ Zig ชัดเจนกว่า C แต่ใช้ ช่วงเปิด [0..9) แทน [0..8]
  • การประกาศชนิด, การกำหนดค่าเริ่มต้น, การทดสอบ, และการเพิ่มค่าของตัวแปรลูป i ถูกจัดการให้อัตโนมัติ

อาร์เรย์

  • [_] ใช้กำหนดอาร์เรย์ที่ไม่รู้ขนาดล่วงหน้า แล้วตามด้วยชนิดของสมาชิกและค่าเริ่มต้น
    • ตัวอย่าง: var grid = [_]u8{0} ** 81; คือการกำหนดค่าเริ่มต้นสมาชิกชนิด u8 จำนวน 81 ตัวให้เป็น 0
    • ขนาดของอาร์เรย์ถูกอนุมานจากอาร์กิวเมนต์ของการทำซ้ำค่าเริ่มต้น
  • ในสภาพแวดล้อมการทดสอบสามารถวนดูสมาชิกของอาร์เรย์แล้วนำมาบวกสะสมได้
  • ตัวแปรที่ประกาศไว้ระหว่าง | ใน for loop จะถูกถือว่าเป็นชนิดเดียวกับสมาชิกของอาร์เรย์โดยอัตโนมัติ
  • usize คือจำนวนเต็มไม่มีเครื่องหมายตามธรรมชาติของแพลตฟอร์ม (u64 บน 64 บิต, u32 บน 32 บิต)

multi-item pointer

  • หาก pointer ของอาร์เรย์จะใช้ pointer arithmetic ต้องประกาศเป็น multi-item pointer อย่างชัดเจน เช่น [*]const i32
  • ถึงอาร์เรย์จะเป็น const แต่ pointer ยังประกาศเป็น var ได้

การ dereference pointer

  • pointer ที่ได้รับที่อยู่ของตำแหน่งเดี่ยวในอาร์เรย์จะอัปเดตด้วย pointer arithmetic ไม่ได้
  • การ dereference pointer ใช้ ptr.*

labeled break

  • สามารถทำงานหลากหลายอย่างในช่วงคอมไพล์ได้ เช่น การกำหนดค่าเริ่มต้นอาร์เรย์
  • labeled break ใช้โดยใส่ : หลังชื่อบล็อก แล้วใช้ break เพื่อคืนค่าจากบล็อก
    • ตัวอย่าง: break :init m;
  • 0.. คือช่วงไม่สิ้นสุดที่เริ่มจาก 0
  • ใน for loop ตัวแปรจะถูกกำหนดค่าเริ่มต้นและเพิ่มค่าให้อัตโนมัติ และลูปจะจบหลังจัดการตำแหน่งสุดท้ายของอาร์เรย์
  • อาร์เรย์ไม่จำเป็นต้องกำหนดค่าเริ่มต้นเป็น undefined อย่างชัดเจน

ฟังก์ชันของ Zig

  • ฟังก์ชันประกาศด้วย fn และเป็น static โดยปริยาย (ใช้ได้เฉพาะภายในไฟล์)
    • หากประกาศเป็น pub fn จะ import จากไฟล์อื่นได้
  • ฟังก์ชันสามารถเป็น "inlined" ได้
  • function pointer จะมี const นำหน้าและตามด้วย function prototype
โฆษณา

การเขียนโปรแกรมเชิงวัตถุใน Zig

  • struct สามารถมีฟังก์ชันได้
  • ในตัวอย่างสแตก สามารถเก็บสมาชิกได้สูงสุด 81 ตัว (ชนิด StkNode)
  • Zig ไม่มีตัวดำเนินการ ++ และ -- โดยใช้ += และ -= แทน
  • stack pointer เป็นจำนวนเต็มที่ใช้เป็นดัชนีของอาร์เรย์ stk
  • pointer self ไม่ได้ถูกส่งเป็นพารามิเตอร์อย่างชัดเจน แต่จะถูกถือโดยปริยายว่าเป็น pointer ของอินสแตนซ์สแตกที่เรียกฟังก์ชันนั้น
    • เมื่อเรียกแบบ stack.pop() ค่า self จะเป็น pointer ไปยัง stack (คล้าย this ใน Java/C++)
  • ฟังก์ชัน init() คือ constructor ของสแตก
  • ฟังก์ชัน pop และ push เป็น "inlined"

การ build และรันโปรแกรม Zig

การ build executable

  • การสร้าง executable ต้องมีฟังก์ชัน main ที่เป็นจุดเริ่มต้นของโปรแกรม
  • โปรแกรมง่าย ๆ สามารถใส่ฟังก์ชัน main ไว้ในไฟล์เดียวกันได้
  • เพื่อดีบักโมดูลแบบแยกอิสระ สามารถแทรกฟังก์ชัน main ไว้ท้ายไฟล์ แล้วคอมเมนต์ทิ้งหลังดีบักเสร็จได้
  • คำสั่งคอมไพล์: zig build-exe -O ReleaseFast program.zig

การรัน test block ของโมดูล

  • นี่คือหนึ่งในความสามารถที่ดีที่สุดของ Zig ใช้ได้ทั้งกับการทดสอบและการทำ prototype
  • test block เริ่มด้วย test "message" { และจบด้วย }
    • "message" คือสตริงที่จะแสดงตอนรันทดสอบ
  • test block ทำงานแยกจาก executable และ executable สุดท้ายจะไม่รันทดสอบเหล่านี้
  • คำสั่งทดสอบ: zig test module.zig
  • test block ใน example.zig ทดสอบฟังก์ชัน set และ print โดย set รับสตริงเลขฐานสิบเป็นพารามิเตอร์ และ print จะแสดงหัวข้อ "Input Grid" ก่อนพิมพ์ grid

เอาต์พุตของ Zig

  • คำสั่ง std.debug.print คือการเรียกฟังก์ชัน print ที่อยู่ใน debug.zig ของไลบรารีมาตรฐาน Zig ชื่อ std
  • พารามิเตอร์ตัวแรกคือ format string และตัวที่สองคือ anonymous struct ที่มีรายการตัวแปรสำหรับแสดงผล
  • หากไม่มีรูปแบบ โครงสร้างดังกล่าวจะว่าง
  • โดยปริยายจะแสดงไปยัง stderr
  • ต่างจาก printf ของ C ตรงที่ Zig สามารถจัดการ literal string และรายการตัวแปรได้ตั้งแต่ช่วงคอมไพล์

การดีบัก executable

  • การใช้ดีบักเกอร์ไม่ใช่เรื่องง่ายนัก นอกจากจะใช้ IDE ที่มีดีบักเกอร์ในตัว (Eclipse, IntelliJ IDEA) หรือชุดพัฒนาแบบรวม (w64devkit)
  • การผสาน symbol ทำให้โค้ดพองขึ้นและต้องคอมไพล์แบบ Debug ส่งผลให้โค้ดที่ได้มีประสิทธิภาพลดลงอย่างมาก
  • Zig มีทางออกที่สะดวกเพื่อหลีกเลี่ยงปัญหานี้

built-in function @breakpoint

  • สามารถแทรก @breakpoint(); ลงในซอร์สโค้ด เพื่อให้โปรแกรมหยุดที่จุดนั้นเมื่อรันผ่านดีบักเกอร์
  • เป็นความสามารถที่มีประโยชน์สำหรับการดีบักโค้ด Zig ที่ optimize แล้วโดยไม่ต้องพึ่ง symbol
  • หากใช้ std.debug.print แสดงตัวแปรที่ต้องการติดตามก่อน @breakpoint(); ก็จะตรวจดูค่าตัวแปรในจังหวะนั้นได้
  • ในตัวอย่าง debug_example.zig มีการแทรกโค้ดพิมพ์ค่า grid และตัวแปรต่าง ๆ ภายในฟังก์ชัน set พร้อม @breakpoint();
  • คำสั่ง build: zig build-exe debug_example.zig
  • จากนั้นเรียก debug_example.exe ผ่านดีบักเกอร์อย่าง gdb แล้วใช้คำสั่ง r เพื่อรันโปรแกรม
  • ใช้คำสั่ง c เพื่อทำงานต่อ พร้อมติดตามค่า grid และตัวแปรต่าง ๆ
  • หากกด Enter ซ้ำเพื่อดำเนินการต่อ ก็จะยืนยันได้ว่าค่าใน grid ตรงกับ test block ของ example.zig
โฆษณา

การเขียนโปรแกรมระดับล่างของ Zig

การแทนเมทริกซ์

  • เลขฐานสิบจะถูกเก็บในเมทริกซ์เป็นจำนวนเต็มมาตรฐาน u8
  • input grid อยู่ในรูปสตริง แต่ตัวอักษร ASCII จะถูกแปลงเป็นจำนวนเต็ม u8 ภายใน
  • การเก็บตัวเลขใช้การจัดเรียงเชิงเส้นทีละบรรทัดลงในอาร์เรย์ grid ที่มี 81 ตำแหน่ง: var grid = [_]u8{0} ** 81;
  • เพื่อยืนยันความถูกต้องของ grid จำเป็นต้องเข้าถึงสมาชิกตามแต่ละแถวและคอลัมน์
  • มีการสร้างอาร์เรย์ของ pointer จำนวน 9 ตัว โดยแต่ละตัวชี้ไปยังจุดเริ่มต้นของแต่ละแถว
  • ใช้ labeled break เพื่อคืนค่าจากบล็อกโค้ด: break :fill9x9 m; เพื่อกำหนดค่าเริ่มต้น matrix ด้วย m
  • รูปแบบการเข้าถึงสมาชิกคือ: element = matrix[i][j]

การแทนเลขฐานสิบด้วยบิต

  • แนวคิดหลักคือแทนเลขฐานสิบจำนวนเต็ม i ด้วยจำนวนเต็ม code
    • i ∈ [1,9] → code = 2ⁱ⁻¹
    • i = 0 → code = 0
  • ตำแหน่งของบิตเพียงตัวเดียวที่ถูกตั้งเป็น 1 ใน code คือ i-1 (เมื่อ i อยู่ระหว่าง 1 ถึง 9) มิฉะนั้นทุกบิตจะเป็น 0
  • มีตารางแสดงค่า code ของแต่ละตัวเลข (1→1, 2→2, 3→4, ..., 9→256)

การคำนวณ code ใน Zig

  • เมื่อ c ไม่เป็น 0 จึงคำนวณค่า code ด้วยตัวดำเนินการ left shift: code = @as(u9,1) << (c-1);
  • ใน Zig ค่าคงที่ต้องมีขนาดเหมาะสมเพื่อให้สามารถคอมไพล์นิพจน์และกำหนดผลลัพธ์ลงตัวแปรได้
  • code ถูกประกาศเป็นชนิด u9 (เพราะค่าสูงสุด 256 ต้องใช้ขั้นต่ำ 9 บิต)
  • Zig สามารถมีตัวแปรที่กำหนดขนาดบิตได้ตามต้องการ
  • ใช้ built-in function @as เพื่อ cast ค่าคงที่ 1 ให้เป็นชนิด u9

การแทน grid ด้วย bit field

bit field grid ตามแถว

  • อาร์เรย์ lines แทนแต่ละแถวเป็นจำนวนเต็ม 9 บิต เพื่อสะท้อน grid ทั้งหมด: var lines = [_]u9{0} ** 9;
  • เมื่อเข้าถึงด้วยแถว i สามารถตรวจสอบว่ามีตัวเลขนั้นอยู่ในแถวนั้นแล้วหรือไม่ด้วย bitwise AND (&): lines[i] & code
  • หากผลลัพธ์เป็น 0 แปลว่ายังไม่มีตัวเลขนั้นในแถว i ไม่เช่นนั้นถือว่าซ้ำ

bit field grid ตามคอลัมน์

  • อาร์เรย์ columns แทนแต่ละคอลัมน์เป็นจำนวนเต็ม 9 บิต เพื่อสะท้อน grid ทั้งหมด: var columns = [_]u9{0} ** 9;
  • เมื่อเข้าถึงด้วยคอลัมน์ j สามารถตรวจสอบว่ามีตัวเลขนั้นอยู่ในคอลัมน์นั้นแล้วหรือไม่ด้วย bitwise AND: columns[j] & code
  • หากผลลัพธ์เป็น 0 แปลว่ายังไม่มีตัวเลขนั้นในคอลัมน์ j ไม่เช่นนั้นถือว่าซ้ำ

กฎของ Sudoku

  • เมื่อต้องแทรกตัวเลขใหม่ลงใน grid ของ Sudoku ที่ว่าง ตัวเลขนั้นต้องยังไม่ปรากฏอยู่ในทั้งแถว คอลัมน์ และเซลล์ที่ครอบตำแหน่งดังกล่าว
  • เซลล์คือ grid ขนาด 3x3 ทั้ง 9 ส่วนที่ถูกแบ่งด้วยเส้นหนา
  • แต่ละสมาชิกเฉพาะใน grid 9x9 จะมีแถว คอลัมน์ และเซลล์ที่สังกัดอยู่เพียงชุดเดียว
  • ในตัวอย่าง grid เซลล์แรกมี 3, 5, 6, 8, 9 อยู่แล้ว และขาด 1, 2, 4, 7
  • อาร์เรย์ lines และ columns ใช้ตรวจสอบค่าซ้ำในแถวและคอลัมน์
  • จึงต้องมีอาร์เรย์ใหม่สำหรับตรวจสอบค่าซ้ำในเซลล์

bit field grid ตามเซลล์

  • อาร์เรย์ cells แทนแต่ละเซลล์เป็นจำนวนเต็ม 9 บิต เพื่อสะท้อน grid ทั้งหมด: var cells = [_]u9{0} ** 9;
  • ถ้าเข้าถึง cells เป็นเมทริกซ์ 3x3 จะง่ายกว่า
  • มีการเติมอาร์เรย์ cell คล้ายกับที่ทำกับเมทริกซ์ 9x9
  • จำเป็นต้องกำหนดแถวและคอลัมน์ของเมทริกซ์ cell จากแถวและคอลัมน์ของสมาชิกใน grid 9x9 เดิม
  • เนื่องจากการหารจำนวนเต็มช้ามาก จึงใช้ อาร์เรย์ cindx = [_]usize{ 0,0,0, 1,1,1, 2,2,2 }; เพื่อให้ผลลัพธ์แทนการหาร
  • เมื่อเข้าถึงเมทริกซ์ด้วยแถว i และคอลัมน์ j ของสมาชิกใน grid 9x9 สามารถตรวจสอบว่าตัวเลขหนึ่งมีอยู่แล้วในเซลล์นั้นหรือไม่ด้วย bitwise AND: cell[cindx[i]][cindx[j]] & code
  • หากผลลัพธ์เป็น 0 แปลว่ายังไม่มีตัวเลขนั้นในเซลล์ ไม่เช่นนั้นถือว่าซ้ำ

การทดสอบค่าซ้ำของสมาชิก

  • เมื่อรวมสมาชิกก่อนหน้าทั้งหมดในแถว คอลัมน์ และเซลล์เดียวกันด้วย bitwise OR (|) แล้วนำไปทำ bitwise AND กับ code ของสมาชิกนั้น ก็จะตรวจสอบค่าซ้ำได้ครบถ้วน
โฆษณา
if (((lines[i]|columns[j]|cell[cindx[i]][cindx[j]])&code) != 0) {  
    unreachable;  
}  
  • หากผลลัพธ์เป็น 0 แปลว่าสมาชิกนั้นยังไม่อยู่ในแถว คอลัมน์ หรือเซลล์
  • หากผลลัพธ์ไม่เป็น 0 โปรแกรมจะหยุดด้วยคำสั่ง unreachable
  • นี่คือวิธีที่ง่ายที่สุดในการแสดง runtime error อย่างชัดเจนใน Zig
  • ในโค้ดจริงยังมีการพิมพ์รายละเอียดตำแหน่งที่เกิดข้อผิดพลาดด้วย
  • ตัวอย่างเช่น หากแทนที่ '0' หลัง '8' ตัวแรกในสตริงอินพุตด้วย '5' จะเกิดข้อผิดพลาด เพราะมีเลข 5 อยู่แล้วที่แถว 3 คอลัมน์ 1

การอัปเดตโครงสร้างข้อมูล

  • ในฟังก์ชัน set มีลูป for ซ้อนสองชั้นที่ทำงานทีละแถว เพื่อคัดลอกสมาชิกใหม่แต่ละตัวจากสตริงอินพุต s ลงใน grid
    • ตัวแปร k ใช้เก็บดัชนีของอักขระอินพุตตัวใหม่ในสตริง s
  • อักขระจะถูกแปลงเป็น u4 (ตัวแปร c) ด้วยการลบ '0'
  • หากสมาชิกใหม่ที่จะใส่ลง grid ไม่เป็น 0 (c != 0) ก็จะ คัดลอก code ที่คำนวณด้วยคำสั่ง left shift ไปยังแต่ละ mirror grid
    • โดยใช้ bitwise OR (|=) กับ mirror grid ที่เกี่ยวข้อง:
lines[i] |= code;  
columns[j] |= code;  
cell[cindx[i]][cindx[j]] |= code;  
  • ไม่จำเป็นต้องทดสอบอย่างชัดเจนว่า c อยู่ระหว่าง 1 ถึง 9 หรือไม่ เพราะจะเกิด overflow เมื่อรันคำสั่ง shift หากค่าไม่ถูกต้อง
  • ตัวอย่างเช่น หากแทน '0' หลัง '8' ตัวแรกในสตริงอินพุตด้วย ':' จะเกิด runtime error
  • แม้แทน '0' เดียวกันด้วย '/' ก็จะเกิด runtime error ลักษณะคล้ายกัน
  • โปรแกรมนี้ทำงานได้ก็ต่อเมื่อค่าอยู่ในช่วง 1 ถึง 9 กล่าวคือ input grid ต้องมีเฉพาะเลขฐานสิบ
  • แต่เนื่องจาก grid Sudoku จำนวนมากบนเว็บใช้ '.' แทน '0' จึงมีบรรทัด if (s[k] == '.') c = 0; อยู่ในฟังก์ชัน set
  • สิ่งนี้ช่วยข้ามคำสั่ง shift ได้อย่างสะดวก เพราะค่า c เป็น 0

การทำ prototype และความทนทาน

  • ข้อผิดพลาดแบบบังคับในสองส่วนข้างต้นเป็นการสาธิตความสามารถสำคัญของ Zig
  • อย่างหนึ่งคือ ความทนทานของ Zig — ในกรณีของคำสั่ง shift จะไม่ยอมให้มีพฤติกรรมผิดพลาดแบบเงียบ ๆ และสามารถจับได้ตอนรัน
  • แม้ทุกอย่างจะดูมุ่งไปที่ประสิทธิภาพ แต่ นี่เป็นตัวอย่างคลาสสิกที่ไม่ได้แลกความทนทานทิ้งเพื่อเอาประสิทธิภาพ
  • ใน C หากการ shift ทำให้บิตหายก็เป็นปัญหาของโปรแกรมเมอร์เอง และสิ่งนี้แลกมากับประสิทธิภาพที่ดีกว่าจากคำสั่งแอสเซมบลีบางแบบ
  • ความสามารถอีกอย่างคือ ศักยภาพในการใช้ test block สำหรับการทำ prototype
  • การประยุกต์ใช้นั้นมีได้มากมายนับไม่ถ้วน และตัวอย่างที่แสดงมีเพียงการดีบักสถานการณ์เฉพาะเมื่อเกิดข้อผิดพลาด
  • เพียงความสามารถเหล่านี้ก็ถือว่าน่าทึ่งและพบได้ยากมากในภาษาโปรแกรม โดยเฉพาะภาษาแบบคอมไพล์

บทสรุป

  • Zig ประกอบด้วยองค์ประกอบสำคัญ 3 ประการคือ ความเข้ากันได้กับ C, cross-compilation, และ การติดตั้งที่เรียบง่าย
  • คุณสมบัติเหล่านี้แสดงให้เห็นว่า Zig อาจกลายเป็น มาตรฐานใหม่ของภาษาสำหรับ system programming
  • ข้อดีจำนวนมากที่เคยพบได้เฉพาะในภาษาแบบ interpreter กำลังค่อย ๆ ย้ายเข้าสู่ภาษาแบบคอมไพล์เพื่อมอบประสิทธิภาพที่ดีกว่า
  • Zig มีความคล้ายกับภาษาแบบ interpreter อย่างโดดเด่นผ่านแนวคิดการรันตอนคอมไพล์
  • สิ่งนี้ทำให้ Zig ทั้งแตกต่างและทรงพลังเป็นพิเศษ ขณะเดียวกันก็ทำให้เข้าใจได้ยากด้วย

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

 
GN⁺ 2025-11-08
ความเห็นจาก Hacker News
  • บทความนี้เปิดมาด้วยการอ้างว่า “Zig ไม่ใช่แค่ภาษาที่เรียบง่าย แต่เป็น วิธีเขียนโปรแกรมแบบใหม่ทั้งหมด” แต่ในความเป็นจริงกลับแทบไม่ได้พูดถึงฟีเจอร์เฉพาะตัวของ Zig เลย
    การอนุมานชนิดข้อมูล, anonymous struct, labeled break ล้วนมีอยู่ในภาษาอื่นมานานแล้ว
    สิ่งที่เป็นเอกลักษณ์จริง ๆ คือ comptime แต่กลับไม่ถูกกล่าวถึงเลย
    แม้มันจะไม่ใช่แนวคิดใหม่แบบเดียวกับ Lisp macro แต่แนวทางที่ Zig ใช้มัน แทน generics ก็น่าสนใจ
    อย่างไรก็ตาม ข้ออ้างในบทความให้ความรู้สึกว่าเกินจริงมาก

    • Rust เองก็อาจเรียกได้ว่าเป็น “วิธีแบบใหม่ทั้งหมด” เหมือนกัน
      Rust สามารถแสดงจังหวะเวลาการรันโค้ดได้อย่างชัดเจน และการออกแบบที่คล้าย query engine ที่สำรวจพื้นที่โค้ดทั้งหมด ก็น่าประทับใจ
    • ภาษา D รองรับ การรันฟังก์ชันตอนคอมไพล์ มาตั้งแต่ปี 2007 แล้ว
      ดู เอกสาร D
      ถ้าเป็น const-expression ก็จะรันให้อัตโนมัติ
    • ตอนนี้การเหมารวม C/C++ เข้าด้วยกันไม่มีความหมายแล้ว
      เพราะมันต่างกันมากพอ ๆ กับ Java/Scala
    • comptime ไม่ได้เป็นสิ่งประดิษฐ์มหัศจรรย์ แต่เป็น เมตาโปรแกรมมิงเวอร์ชันสมัยใหม่ มากกว่า
      Zig สะอาดกว่า C++ template แต่ให้ความรู้สึกว่าเป็นทางเลือกที่ใช้งานได้จริง มากกว่าจะเป็นการปฏิวัติ
      ส่วนตัวก็ไม่ค่อยเข้าใจความตื่นเต้นเกินเหตุแบบเดียวกับตอน Rust
    • เห็นคำว่า “วิธีแบบใหม่ทั้งหมด” แล้วคาดหวังว่าจะได้เจอ กระบวนทัศน์ใหม่ แบบ LISP หรือ Prolog แต่จริง ๆ ไม่มี
      อ่านเอกสาร Zig จบหมดแล้วก็ยังไม่เจออะไรที่น่าตกใจ เลยรู้สึกงงนิดหน่อย
  • ปัญหาใหญ่ที่สุดของ Zig คือ ไม่สามารถแนบข้อมูลไปกับ error ได้
    error ถูกส่งผ่านได้แค่ช่องทางเสริม ทำให้ดีบักยาก และสุดท้ายนักพัฒนาก็มักจะละทิ้งข้อมูลของ error ไป
    ดู issue ที่เกี่ยวข้อง
    แค่โค้ดง่าย ๆ อย่าง AccessDenied ก็ยากจะรู้สาเหตุที่แท้จริง

    • ผมอ่านบทความของ matklad แล้ว รู้สึกว่าแนวทางแยก error code ออกจากข้อมูลวินิจฉัยนั้นน่าเชื่อถือ
      ในทางปฏิบัติ ต่อให้ใช้ Error object ที่ซับซ้อน ก็ยังมักต้องมี diagnostic channel แยกต่างหากอยู่ดี
    • ในภาษา systems การแนบข้อมูลไปกับ error ไม่ได้ดีเสมอไป
      เพราะมีทั้ง performance overhead และปัญหาเรื่องสถานะของระบบ ทำให้บางกรณีการจัดการแบบ late binding ปลอดภัยกว่า
      Zig มีปรัชญาที่ให้ความสำคัญกับ ความแม่นยำและความกำหนดแน่นอน แบบนี้
    • ใน Zig เองก็มีการคุยกันเรื่องฟีเจอร์ใส่ ข้อมูลที่ผู้ใช้กำหนดเอง ลงใน error stack trace
      ดู issue ที่เกี่ยวข้อง
      แต่สิ่งที่จำเป็นจริง ๆ คือ structured logging และความสามารถในการตามบริบทจาก call stack
    • std.zon ถูกยกเป็นตัวอย่างที่ดี และในชุมชนก็มีความพยายามรวบรวม แพตเทิร์นการจัดการ error แบบต่าง ๆ เพื่อสะท้อนเข้าไปในมาตรฐาน
    • การไม่เปิดให้แนบข้อมูลกับ error อาจกลับกลายเป็นการบังคับให้เกิด การออกแบบ error ที่ชัดเจน มากขึ้น
      และกันไม่ให้นักพัฒนาที่ขี้เกียจแปะข้อมูลทุกอย่างเข้าไปแบบไม่ยั้ง
  • เห็นด้วยกับข้ออ้างที่ว่า “แนวทางพัฒนา Zig เองก็เป็นแนวทางพัฒนาภาษาแบบใหม่”
    กระบวนการวิวัฒนาการอย่างช้า ๆ ที่ พิจารณาฟีเจอร์อย่างระมัดระวังและตัดสิ่งไม่จำเป็นออก นั้นน่าประทับใจ

    • แต่แนวทางแบบนี้ก็พบได้ทั่วไปใน Java หรือ Rust เช่นกัน
      เลยอยากฟังให้ชัดกว่านี้ว่าอะไรคือจุดที่ Zig แตกต่างจริง ๆ
  • ชอบที่สามารถ ติดตั้ง Zig ผ่าน PyPI ได้
    ติดตั้ง แพ็กเกจ ziglang ด้วย pip install ziglang แล้วใช้งานได้ทันที
    และยังใช้ uvx เพื่อบิลด์โค้ด C ได้ด้วย

    • เพราะ Python wheel สามารถ bundle ซอฟต์แวร์อะไรก็ได้ จึงทำให้วิธีติดตั้งแบบนี้เป็นไปได้
    • แต่แนวทางนี้ก็ให้ความรู้สึกเหมือน ประดิษฐ์สิ่งที่ทำให้ยุ่งกว่า nix ขึ้นมาใหม่
    • อยากให้ Nim มีตัวเลือกการติดตั้งแบบนี้เหมือนกัน
    • ส่วนตัวคิดว่า micromamba หรือ pixi เป็นวิธีจัดการแพ็กเกจที่ดีกว่า pip/uv
    • ด้วยเครื่องมือ AI ตอนนี้การเรียนรู้ภาษาไหน ๆ ก็ง่ายขึ้นมากแล้ว
  • น่าเสียดายที่ฟีเจอร์ซึ่งมีอยู่แล้วในภาษาอย่าง Ada, Object Pascal, Modula-2 กลับถูกนำมาห่อใหม่เป็น “นวัตกรรม” ของ Zig
    พอถูกนำเสนอใหม่ด้วย ไวยากรณ์สไตล์ C ก็เลยทำให้ไอเดียเมื่อ 40 ปีก่อนดูเหมือนของใหม่ ซึ่งก็น่าสนใจดี

  • ช่วงต้นของบทความดีอยู่หรอก แต่หลังจากนั้นก็กลายเป็นแค่การไล่รายการฟีเจอร์ของ Zig
    ไวยากรณ์ที่เข้าใจง่าย และ control flow ที่ชัดเจน ของ Zig (defer เป็นต้น) นั้นมีเสน่ห์มาก
    และด้วย comptime ก็ไม่จำเป็นต้องเรียนรู้ไวยากรณ์ macro แยกต่างหาก

    • เสน่ห์ที่แท้จริงของ Zig คือ การออกแบบที่ไม่มีความซ้ำซ้อนเกินจำเป็น
      องค์ประกอบทุกอย่างประกบกันอย่างเป็นธรรมชาติ จนแม้จะเพิ่งเริ่มใช้ก็รู้สึกเหมือนเป็นเครื่องมือที่ใช้มานานแล้ว
    • บทความวิเคราะห์ไวยากรณ์ Zig ของ matklad ก็น่าอ่านเช่นกัน
  • ไวยากรณ์ for (0..9) ของ Zig เข้าใจง่ายก็จริง แต่เพราะเป็น ช่วงเปิดด้านท้าย เลยทำให้งงได้บ่อย
    คล้าย Python range(0, 9) ที่มักลืมได้ง่ายว่ารวมค่าท้ายหรือไม่

    • Rust แยกชัดเจนด้วย 0..9 และ 0..=9
    • ความสม่ำเสมอแบบ Zig ที่ ใช้เฉพาะช่วงกึ่งเปิด กลับช่วยลดข้อผิดพลาดได้
      เพราะขนาดช่วงคำนวณได้จากผลต่างตรง ๆ และการวนย้อนกลับก็ทำได้ง่าย
    • Odin แยกให้ชัดกว่านี้ด้วย 0..<5 (เปิด) และ 0...5 (ปิด)
  • ไม่ค่อยชอบ กฎการตั้งชื่อ identifier ของ Zig
    การปนกันระหว่าง snake_case กับ camelCase ดูแปลก ๆ
    ถึงอย่างนั้น ระบบ build, allocator, และประสบการณ์ตอนคอมไพล์ก็ยอดเยี่ยมมาก
    แม้จะใช้ Rust เป็นหลัก แต่ก็ยัง อยากรู้อยากลอง Zig อยู่เรื่อย ๆ

    • ผมก็คล้ายกัน ส่วนตัวไม่ได้ทำตามกฎการตั้งชื่อของฟังก์ชัน private
      กฎเรื่อง prefix ของไลบรารี C ก็ชวนรำคาญเหมือนกัน
  • เสน่ห์ของ Zig ไม่ได้อยู่ที่ฟีเจอร์ใดฟีเจอร์หนึ่ง แต่เป็น ผลสะสมของการตัดสินใจเชิงปฏิบัติ หลายอย่าง
    ตัวเลือกที่ตอนแรกดูหัวรุนแรง พอเข้าใจลึกขึ้นกลับยิ่งรู้สึกว่ามีเหตุผล
    Zig เป็น ภาษาที่ให้รางวัลกับนักพัฒนาที่ช่างสงสัย

    • ผมเคยทำเกมเล็ก ๆ ด้วย Odin แล้วเป็นประสบการณ์ที่สนุกมาก
  • หนึ่งในเหตุผลที่ Zig ดี คือมัน ยอมรับความจริงของโค้ดระบบระดับล่าง
    หลายภาษามักเมินเรื่องพวกนี้ด้วยเหตุผลด้านความงาม แต่ Zig ไม่เป็นแบบนั้น

    • ถ้าลองกระโดดเข้าไปดูนิยามใน standard library ก็จะเห็นการรองรับ กรณีพิเศษ อย่าง Plan9 OS ด้วยตัวเอง
      ดู เอกสาร page_allocator
    • แต่ก็ยังควรมี ตัวอย่างที่เป็นรูปธรรม มาสนับสนุนข้ออ้างแบบนี้มากกว่านี้