- คอมไพเลอร์ 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 เพื่อคืนค่าจากบล็อก
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 ความคิดเห็น
ความเห็นจาก Hacker News
บทความนี้เปิดมาด้วยการอ้างว่า “Zig ไม่ใช่แค่ภาษาที่เรียบง่าย แต่เป็น วิธีเขียนโปรแกรมแบบใหม่ทั้งหมด” แต่ในความเป็นจริงกลับแทบไม่ได้พูดถึงฟีเจอร์เฉพาะตัวของ Zig เลย
การอนุมานชนิดข้อมูล, anonymous struct, labeled break ล้วนมีอยู่ในภาษาอื่นมานานแล้ว
สิ่งที่เป็นเอกลักษณ์จริง ๆ คือ comptime แต่กลับไม่ถูกกล่าวถึงเลย
แม้มันจะไม่ใช่แนวคิดใหม่แบบเดียวกับ Lisp macro แต่แนวทางที่ Zig ใช้มัน แทน generics ก็น่าสนใจ
อย่างไรก็ตาม ข้ออ้างในบทความให้ความรู้สึกว่าเกินจริงมาก
Rust สามารถแสดงจังหวะเวลาการรันโค้ดได้อย่างชัดเจน และการออกแบบที่คล้าย query engine ที่สำรวจพื้นที่โค้ดทั้งหมด ก็น่าประทับใจ
ดู เอกสาร D
ถ้าเป็น const-expression ก็จะรันให้อัตโนมัติ
เพราะมันต่างกันมากพอ ๆ กับ Java/Scala
Zig สะอาดกว่า C++ template แต่ให้ความรู้สึกว่าเป็นทางเลือกที่ใช้งานได้จริง มากกว่าจะเป็นการปฏิวัติ
ส่วนตัวก็ไม่ค่อยเข้าใจความตื่นเต้นเกินเหตุแบบเดียวกับตอน Rust
อ่านเอกสาร Zig จบหมดแล้วก็ยังไม่เจออะไรที่น่าตกใจ เลยรู้สึกงงนิดหน่อย
ปัญหาใหญ่ที่สุดของ Zig คือ ไม่สามารถแนบข้อมูลไปกับ error ได้
error ถูกส่งผ่านได้แค่ช่องทางเสริม ทำให้ดีบักยาก และสุดท้ายนักพัฒนาก็มักจะละทิ้งข้อมูลของ error ไป
ดู issue ที่เกี่ยวข้อง
แค่โค้ดง่าย ๆ อย่าง AccessDenied ก็ยากจะรู้สาเหตุที่แท้จริง
ในทางปฏิบัติ ต่อให้ใช้
Errorobject ที่ซับซ้อน ก็ยังมักต้องมี diagnostic channel แยกต่างหากอยู่ดีเพราะมีทั้ง performance overhead และปัญหาเรื่องสถานะของระบบ ทำให้บางกรณีการจัดการแบบ late binding ปลอดภัยกว่า
Zig มีปรัชญาที่ให้ความสำคัญกับ ความแม่นยำและความกำหนดแน่นอน แบบนี้
ดู issue ที่เกี่ยวข้อง
แต่สิ่งที่จำเป็นจริง ๆ คือ structured logging และความสามารถในการตามบริบทจาก call stack
std.zonถูกยกเป็นตัวอย่างที่ดี และในชุมชนก็มีความพยายามรวบรวม แพตเทิร์นการจัดการ error แบบต่าง ๆ เพื่อสะท้อนเข้าไปในมาตรฐานและกันไม่ให้นักพัฒนาที่ขี้เกียจแปะข้อมูลทุกอย่างเข้าไปแบบไม่ยั้ง
เห็นด้วยกับข้ออ้างที่ว่า “แนวทางพัฒนา Zig เองก็เป็นแนวทางพัฒนาภาษาแบบใหม่”
กระบวนการวิวัฒนาการอย่างช้า ๆ ที่ พิจารณาฟีเจอร์อย่างระมัดระวังและตัดสิ่งไม่จำเป็นออก นั้นน่าประทับใจ
เลยอยากฟังให้ชัดกว่านี้ว่าอะไรคือจุดที่ Zig แตกต่างจริง ๆ
ชอบที่สามารถ ติดตั้ง Zig ผ่าน PyPI ได้
ติดตั้ง แพ็กเกจ ziglang ด้วย
pip install ziglangแล้วใช้งานได้ทันทีและยังใช้
uvxเพื่อบิลด์โค้ด C ได้ด้วยน่าเสียดายที่ฟีเจอร์ซึ่งมีอยู่แล้วในภาษาอย่าง Ada, Object Pascal, Modula-2 กลับถูกนำมาห่อใหม่เป็น “นวัตกรรม” ของ Zig
พอถูกนำเสนอใหม่ด้วย ไวยากรณ์สไตล์ C ก็เลยทำให้ไอเดียเมื่อ 40 ปีก่อนดูเหมือนของใหม่ ซึ่งก็น่าสนใจดี
ช่วงต้นของบทความดีอยู่หรอก แต่หลังจากนั้นก็กลายเป็นแค่การไล่รายการฟีเจอร์ของ Zig
ไวยากรณ์ที่เข้าใจง่าย และ control flow ที่ชัดเจน ของ Zig (
deferเป็นต้น) นั้นมีเสน่ห์มากและด้วย comptime ก็ไม่จำเป็นต้องเรียนรู้ไวยากรณ์ macro แยกต่างหาก
องค์ประกอบทุกอย่างประกบกันอย่างเป็นธรรมชาติ จนแม้จะเพิ่งเริ่มใช้ก็รู้สึกเหมือนเป็นเครื่องมือที่ใช้มานานแล้ว
ไวยากรณ์
for (0..9)ของ Zig เข้าใจง่ายก็จริง แต่เพราะเป็น ช่วงเปิดด้านท้าย เลยทำให้งงได้บ่อยคล้าย Python
range(0, 9)ที่มักลืมได้ง่ายว่ารวมค่าท้ายหรือไม่0..9และ0..=9เพราะขนาดช่วงคำนวณได้จากผลต่างตรง ๆ และการวนย้อนกลับก็ทำได้ง่าย
0..<5(เปิด) และ0...5(ปิด)ไม่ค่อยชอบ กฎการตั้งชื่อ identifier ของ Zig
การปนกันระหว่าง snake_case กับ camelCase ดูแปลก ๆ
ถึงอย่างนั้น ระบบ build, allocator, และประสบการณ์ตอนคอมไพล์ก็ยอดเยี่ยมมาก
แม้จะใช้ Rust เป็นหลัก แต่ก็ยัง อยากรู้อยากลอง Zig อยู่เรื่อย ๆ
กฎเรื่อง prefix ของไลบรารี C ก็ชวนรำคาญเหมือนกัน
เสน่ห์ของ Zig ไม่ได้อยู่ที่ฟีเจอร์ใดฟีเจอร์หนึ่ง แต่เป็น ผลสะสมของการตัดสินใจเชิงปฏิบัติ หลายอย่าง
ตัวเลือกที่ตอนแรกดูหัวรุนแรง พอเข้าใจลึกขึ้นกลับยิ่งรู้สึกว่ามีเหตุผล
Zig เป็น ภาษาที่ให้รางวัลกับนักพัฒนาที่ช่างสงสัย
หนึ่งในเหตุผลที่ Zig ดี คือมัน ยอมรับความจริงของโค้ดระบบระดับล่าง
หลายภาษามักเมินเรื่องพวกนี้ด้วยเหตุผลด้านความงาม แต่ Zig ไม่เป็นแบบนั้น
ดู เอกสาร page_allocator