- Zig ใช้ ไวยากรณ์แบบวงเล็บปีกกา คล้ายกับ Rust แต่ปรับปรุงด้วยความหมายของภาษาที่เรียบง่ายกว่าและตัวเลือกด้านไวยากรณ์ที่ประณีตกว่า
- ลิเทอรัลจำนวนเต็ม ทุกตัวเริ่มต้นเป็น
comptime_int และจะถูกแปลงแบบชัดเจนเมื่อมีการกำหนดค่า ส่วน ลิเทอรัลสตริง ใช้รูปแบบ raw string แบบย่อที่อิงกับ \\
- record literal ในรูปแบบ
.x = 1 ทำให้ค้นหาการเขียนค่าให้ฟิลด์ได้ง่าย และทุกชนิดข้อมูลถูกแสดงอย่างสม่ำเสมอด้วยรูปแบบ prefix
- ใช้
and·or เป็นคีย์เวิร์ดควบคุมการไหล และคำสั่ง if·loop สามารถละวงเล็บปีกกาได้ตามต้องการ โดยตัว formatter จะช่วยรับประกันความปลอดภัย
- โดยไม่มีเนมสเปซ Zig จัดการ ทุกอย่างเป็น expression เพื่อรวมไวยากรณ์ของชนิดข้อมูล·ค่า·แพตเทิร์นเข้าด้วยกัน และใช้งาน generic·record literal·ฟังก์ชันในตัว (
@import, @as เป็นต้น) ได้อย่างกระชับ
ภาพรวม
- Zig มีรูปลักษณ์คล้าย Rust แต่เลือกใช้โครงสร้างภาษาที่เรียบง่ายกว่า
- การออกแบบไวยากรณ์มุ่งเน้นที่ ความเป็นมิตรต่อ grep, ความสม่ำเสมอของไวยากรณ์, และ การลดสิ่งรบกวนทางสายตาที่ไม่จำเป็น
ลิเทอรัลจำนวนเต็ม
const an_integer = 92;
assert(@TypeOf(an_integer) == comptime_int);
const x: i32 = 92;
const y = @as(i32, 92);
- ลิเทอรัลจำนวนเต็มทั้งหมดมีชนิดเป็น
comptime_int
- เมื่อต้องกำหนดค่าให้ตัวแปร ต้องระบุชนิดอย่างชัดเจนหรือใช้
@as เพื่อแปลง
- รูปแบบ
var x = 92; ใช้งานไม่ได้ และต้องระบุชนิดอย่างชัดเจน
ลิเทอรัลสตริง
const raw =
\\Roses are red
\\ Violets are blue,
\\Sugar is sweet
\\ And so are you.
\\
;
- แต่ละบรรทัดเป็นโทเค็นแยกกัน จึงไม่มีปัญหาเรื่องการเยื้อง
- ไม่จำเป็นต้อง escape
\\ เอง
record literal
const p: Point = .{
.x = 1,
.y = 2,
};
- รูปแบบ
.x = 1 ช่วยแยกความต่างระหว่างการอ่าน/เขียนได้ดี
- สัญกรณ์
.{} แยกจาก block ได้ชัดเจน และแปลงเป็นชนิดผลลัพธ์โดยอัตโนมัติ
สัญกรณ์ชนิดข้อมูล
u32 // จำนวนเต็ม
[3]u32 // อาร์เรย์ความยาว 3
?[3]u32 // อาร์เรย์ที่เป็น null ได้
*const ?[3]u32 // พอยน์เตอร์ค่าคงที่
- ชนิดข้อมูลทั้งหมดใช้สัญกรณ์แบบ prefix
- การ dereference ใช้สัญกรณ์แบบ suffix (
ptr.*)
identifier
const @"a name with space" = 42;
- ใช้หลีกเลี่ยงการชนกับคีย์เวิร์ด หรือกำหนดชื่อพิเศษได้
การประกาศฟังก์ชัน
pub fn main() void {}
fn add(x: i32, y: i32) i32 {
return x + y;
}
- คีย์เวิร์ด
fn กับชื่อฟังก์ชันอยู่ติดกัน ทำให้ค้นหาได้ง่าย
- การระบุชนิดคืนค่าไม่ใช้
->
การประกาศตัวแปร
const mid = lo + @divFloor(hi - lo, 2);
var count: u32 = 0;
- ใช้
const และ var
- การระบุชนิดอยู่ในลำดับ
ชื่อ: ชนิด
การควบคุมการไหล: and/or
while (count > 0 and ascii.isWhitespace(buffer[count - 1])) {
count -= 1;
}
and, or เป็นคีย์เวิร์ดควบคุมการไหล
- การคำนวณแบบบิตใช้
&, |
คำสั่ง if
.direction = if (prng.boolean()) .ascending else .descending;
- วงเล็บจำเป็น แต่วงเล็บปีกกาเป็นทางเลือก
zig fmt ช่วยรับประกันการจัดรูปแบบที่ปลอดภัย
ลูป
for (0..10) |i| {
print("{d}\n", .{i});
} else @panic("loop safety counter exceeded");
- ทั้ง
for และ while รองรับส่วน else
- จัดวาง iterator และชื่อตัวแปรองค์ประกอบได้อย่างเข้าใจง่าย
เนมสเปซและการ resolve ชื่อ
const std = @import("std");
const ArrayList = std.ArrayList;
- ไม่อนุญาตให้ shadow ตัวแปร
- ไม่มีเนมสเปซและไม่มี glob import
ทุกอย่างคือ expression
const E = enum { a, b };
const e: if (true) E else void = .a;
- รวมไวยากรณ์ของชนิดข้อมูล·ค่า·แพตเทิร์นเข้าด้วยกัน
- สามารถวาง conditional expression ในตำแหน่งของชนิดข้อมูลได้
generic
fn ArrayListType(comptime T: type) type {
return struct {
fn init() void {}
};
}
var xs: ArrayListType(u32) = .init();
- generic แสดงด้วยไวยากรณ์แบบการเรียกฟังก์ชัน (
Type(T))
- อาร์กิวเมนต์ชนิดข้อมูลต้องระบุอย่างชัดเจนเสมอ
ฟังก์ชันในตัว
const foo = @import("./foo.zig");
const num = @as(i32, 92);
- เรียกใช้ความสามารถที่คอมไพเลอร์มีให้ด้วย prefix
@
@import แสดงพาธไฟล์อย่างชัดเจน
- อาร์กิวเมนต์ต้องเป็น string literal เท่านั้น
บทสรุป
- ไวยากรณ์ของ Zig เป็นตัวอย่างของการที่ ตัวเลือกเล็กๆ หลายอย่าง รวมกันจนกลายเป็น ภาษาที่อ่านง่าย
- เมื่อจำนวนฟีเจอร์ลดลง ไวยากรณ์ที่จำเป็นก็ลดลงด้วย และ โอกาสเกิดการชนกันระหว่างไวยากรณ์ก็ลดลง
- ยืม แนวคิดที่ดีจากภาษาเดิม มาใช้ แต่เมื่อจำเป็นก็กล้านำไวยากรณ์ใหม่มาใช้
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
บทความนี้เจาะลึกถึง trade-off หลายอย่างที่เกิดขึ้นในการออกแบบไวยากรณ์ภาษา และทำให้รู้สึกประทับใจมากกับความมินิมอล ความสม่ำเสมอ และการโฟกัสเรื่องความอ่านง่ายแบบไม่ปรานีของไวยากรณ์ Zig สิ่งที่ชอบคือมันไม่ใช่ความงามเชิงนามธรรม แต่เป็น "บรทัลลิสม์" ที่ไม่มีอะไรให้ต้องแปลกใจสำหรับการใช้งานเชิงอุตสาหกรรม การออกแบบไวยากรณ์ที่สมดุลแบบนี้หาได้ยากจริง ๆ และคิดว่า Zig ทำออกมาได้ดีมาก
น่าเสียดายที่บทความไม่ได้พูดถึงการจัดการข้อผิดพลาดเลย วิธี
try/catchของ Zig ยอดเยี่ยมมาก เป็นแนวทางจัดการข้อผิดพลาดที่ชอบที่สุดในบรรดาหลายภาษา ถ้าได้แนะนำส่วนนี้ด้วยก็น่าจะดียิ่งขึ้นเสน่ห์ที่แท้จริงของ Zig ไม่ใช่ "ความอ่านง่ายที่สวยงามในระดับผิวเผิน" แต่คือความงามที่สม่ำเสมอซึ่งได้มาจากการทำ abstraction อย่างเหมาะสม คล้ายอุปมาเรื่อง S-expression กับ M-expression คือวิธีที่ดีสำหรับกรณีทั่วไปมักจะดีกว่าในระยะยาว เมื่อเทียบกับการออกแบบพิเศษสำหรับกรณียกเว้นหลายแบบ ถ้าเพิ่มกรณียกเว้นสารพัดแบบ C++ สุดท้ายภาระก็จะตกอยู่ที่การต้องท่องจำกฎทั้งหมด ในการออกแบบภาษา ถ้ามัวแต่ไล่ตามความเรียบง่ายและความสม่ำเสมอแบบสุดโต่ง ก็อาจตกไปสู่ "Turing tarpit" ที่ผู้ใช้ต้องแบกรับความซับซ้อนเอง ดังนั้นแนวทางที่ให้กรณีพิเศษถูกแก้ได้อย่างเป็นธรรมชาติจากกฎทั่วไปจึงสำคัญ ตัวอย่างแนวคิดนี้เห็นได้ในคอมิก XKCD เรื่อง New Pet
ถ้ามีตัวอย่างที่รู้สึกประทับใจ อยากให้ช่วยแชร์หน่อย
สำหรับการระบุชนิดแบบ
ชื่อ:ชนิดของ Zig ที่คล้าย Rust นั้น ส่วนตัวยังชอบแบบดั้งเดิมที่เอาชนิดขึ้นก่อนมากกว่า เวลาย้อนกลับไปดูการประกาศตัวแปร สิ่งที่อยากรู้ที่สุดก็คือชนิดของตัวแปรนั้น ถ้าหาได้ไม่เร็วก็รู้สึกไม่สะดวก โดยเฉพาะใน Rust ที่มีอะไรอย่างlet mutซ้ำ ๆ โดยไม่จำเป็นจนยิ่งดูยุ่งยาก ส่วนแบบ C, C++ ที่เอาชนิดไว้ก่อนก็ดีเหมือนกัน จริง ๆ แล้วคิดว่าอุดมคติคือใช้ type inference เท่าที่จำเป็นในจุดที่ต้องใช้เท่านั้นคีย์เวิร์ด
letก็มีความจำเป็นในแง่ที่ช่วยให้ชัดเจนว่านี่คือประโยคประกาศ ไม่อย่างนั้นอาจเจอปัญหาการ parse ไวยากรณ์กำกวมแบบใน C++ ได้ฉันเองก็มักจะมองหาชนิดของตัวแปรก่อนเสมอ เลยชอบรูปแบบที่ชนิดอยู่ข้างหน้า ในมุม parser การจัดการชื่อก่อนก็สะดวกกว่า และก็เข้าใจว่า TypeScript ใช้โครงสร้างนี้เพราะต้องเข้ากับ JavaScript ท้ายที่สุดคิดว่าสิ่งสำคัญคือ standard library ที่ใช้ง่าย มากกว่าจะยัดทุกสถานะให้เป็นชนิดแบบตัวอย่างการใช้ type system เกินขอบเขต การสื่อเจตนาให้ชัดเจนสำคัญกว่า
เวลากลับขึ้นไปดูชนิดของตัวแปรในโค้ด ถ้าชนิดอยู่ข้างหน้ากลับทำให้หาจุดประกาศของตัวแปรที่กำลังตามหาได้ยากกว่า เพราะชื่อชนิดอยู่ต้นบรรทัดและมีความยาวไม่แน่นอน ทำให้สายตาต้องกวาดซ้ายขวาซ้ำ ๆ เลยรู้สึกว่าไม่มีประสิทธิภาพ
ในหลายกรณี ถ้าเอาเมาส์ไปชี้ใน editor ก็เห็นข้อมูลชนิดได้ทันที ตำแหน่งของชนิดในโค้ดจึงอาจไม่สำคัญมากนัก ที่ Rust ดู verbose นั้นส่วนหนึ่งเป็นเหตุผลด้าน implementation เพื่อหลีกเลี่ยงความกำกวมในการ parse ถ้าใช้แบบ C, C++ ที่เอาชนิดไว้ก่อน ก็จะ grep หา declaration ของตัวแปรชื่อหนึ่ง ๆ ได้ยากกว่า และสไตล์ที่เอา return type ไว้ข้างหน้าแม้จะเกิดจากเรื่อง template แต่ในบางกรณีก็ช่วยให้อ่านและค้นหาโค้ดได้ง่ายขึ้น
ส่วนตัวชอบรูปแบบระบุชนิดแบบ Pascal มากกว่า แม้ตอนใช้ type inference ก็ไม่ต้องมีลูกเล่นอ้อม ๆ แบบ
autoและในมุมการ parse ก็กำกวมน้อยกว่า ในMyClass xมันไม่ชัดทันทีว่า MyClass เป็นชนิดหรือชื่อตัวแปร รูปแบบนี้จึงช่วยลดความกำกวมได้สำหรับไวยากรณ์ raw/multiline string ของ Zig วิธีที่ต้องใช้
\\หลายครั้งดูสับสนและสุดโต่งเกินไปมากถ้าเคยจัด format multiline string ใน Python, C++, Rust มาก่อน ก็น่าจะเข้าใจความลำบากนี้ดี เพราะต้องคอยกังวลว่า indentation จะถูกรวมเข้าไปในเนื้อหาสตริงด้วย ส่วนภาษาที่มีโหมดตัด indentation แบบ YAML กลับยิ่งทำให้สับสนไปอีก วิธีของ Zig ชัดเจนมากในเรื่อง indentation
ตอนแรกก็รู้สึกว่าไวยากรณ์นี้ใช้งานลำบากมาก แต่พอใช้ Zig ไปเรื่อย ๆ ก็เริ่มชินและกลับมองเห็นข้อดีของมัน Zig เป็นภาษาที่ตอนแรกอาจรู้สึกไม่ชอบบางอย่าง แต่พอใช้จริงแล้วจะค่อย ๆ เห็นว่ามันดีตรงไหน
อันที่จริงไม่ใช่ว่าไวยากรณ์มันบ้า แต่เป็นเพราะปัญหานี้มันบ้าต่างหาก—ปัญหาการใส่ multiline string ซ้อนอยู่ใน multiline string อีกชั้นอย่างปลอดภัย ใน Zig ไม่ต้องมี escape เพิ่มและไม่ต้องกังวลเรื่อง indentation ซึ่งเป็นข้อดีมาก
สำหรับฉัน
trimIndentของ Kotlin, text block ของ Go หรือ Java และโดยเฉพาะ raw string แบบ backtick ของ Go ลื่นไหลกว่ามาก ใน Zig กลับต้องเลี่ยงไปใช้@embedFileเพราะ\\แม้ในเชิงภาพจะไม่ค่อยชอบ
\\เท่าไร แต่ก็คิดว่านี่เป็นวิธีที่แก้ปัญหา multiline literal และเรื่อง indentation ได้อย่างสะอาดตา ไม่ค่อยรู้จักภาษาอื่นที่แก้ปัญหานี้ได้โดยไม่ต้องพึ่งฟังก์ชันไวยากรณ์ของ Zig ให้ความรู้สึกกระจัดกระจาย มีทั้งรูปแบบอย่าง
@TypeOfที่ขึ้นต้นด้วย@หรือไวยากรณ์ initializer แบบ.{.x}ที่ดูแปลกตา อาจเป็นเพราะยังไม่ชำนาญในการใช้ Zig แต่โดยรวมแล้วให้ความรู้สึกว่าอ่านโค้ดยากฉันชอบไวยากรณ์ของ Odin มากกว่า เพราะมินิมอลและขัดเกลามากกว่า ส่วน Zig ให้ความรู้สึกค่อนข้างกระจัดกระจาย
.ใน Zig ทำหน้าที่เป็น placeholder สำหรับชนิดที่อนุมานได้ เช่น สามารถ initialize object แบบนี้หรือถ้าอยากระบุให้ชัดว่าต้องการ type inference ก็เขียนได้ว่า
ใน argument ของฟังก์ชันก็ละชนิดได้เช่นกัน จึงกระชับกว่า ใน Rust จำเป็นต้องเขียนชนิดอย่างชัดเจนในสถานการณ์แบบนี้
แม้แต่การ initialize struct แบบซ้อนกัน วิธีอนุมานของ Zig ก็มีประโยชน์กว่ามาก ถ้าต้องเขียนชนิดชัดเจนทุกที่แบบ Rust โค้ดจะรกเร็วมาก ถึงอย่างนั้นก็ยังคิดว่าถ้าตัด dot ที่อยู่ข้างหน้าออกได้จะสะดวกกว่า แต่ดูเหมือนคงไว้เพื่อทำให้ parser ง่ายขึ้น รูปแบบ
x: 123หรือ.x = 123นั้นยืมมาจาก JS และ C99 ตามลำดับ ส่วนตัวใช้ทั้งสองแบบบ่อยเลยไม่รู้สึกแปลกชอบ raw string literal ของ C# 11 มากกว่าเยอะ มันใช้ indentation ของบรรทัดแรกเป็นเกณฑ์แล้วจัดบรรทัดที่เหลือให้ตรงกันอัตโนมัติ และยังสามารถใช้วงเล็บปีกกาเป็นตัวอักษรธรรมดาได้ด้วย ถ้ามี
$หลายตัว วงเล็บปีกกาจะถูกมองเป็นค่าตัวอักษรทั้งหมด"""ปิดท้ายเป็นเกณฑ์ และยังอนุญาตให้ย่อหน้าบรรทัดแรกได้ด้วย ดีใจที่มีคนชอบฟีเจอร์นี้ และกล้าพูดเลยว่ามันเป็นฟีเจอร์ที่ดีไวยากรณ์ของ Zig ก็โอเค แต่เมื่อเทียบกับ Go ที่เขียนได้สะอาดพอโดยไม่ต้องมี semicolon หรือ
:ก็ยังไม่ถึงขั้นเรียกว่า "น่ารัก" ถ้าจะเทียบก็บอกได้ว่าเหนือกว่า Rust มากจริง แต่ Go เองก็ยอดเยี่ยมพอแล้วในทางกลับกัน ไวยากรณ์ที่มินิมอลเกินไปแบบ Go บางครั้งกลับอ่านและตีความยากกว่า เวลาที่ใช้ในการอ่านโค้ดมักมากกว่าเวลาที่ใช้เขียน ความกระชับเกินความจำเป็นจึงอาจทำให้ผิดพลาดและ debug ยากขึ้น ตัวอย่างชัด ๆ คือไวยากรณ์ที่ย่อมากเกินไปอย่าง CoffeeScript หรือ J
ฉันไม่คิดว่าการตัดองค์ประกอบทางไวยากรณ์ออกจะทำให้ไวยากรณ์ดีขึ้นเสมอไป ถ้าเป็นอย่างนั้นจริง ทุกคนก็คงเขียนกันแบบ Lisp และข้อความก็คงเขียนแบบ scriptio continua (รูปแบบการเขียนโบราณที่ไม่มีเว้นวรรค) ไปแล้ว ดู วิกิพีเดียของ scriptio continua
โดยรวมพอใจกับ Zig แต่ก็ยังเสียดายปัญหาต่อไปนี้
a?.b?.c) ถ้ามีการรองรับ monadic type ก็น่าจะ chain แบบทั่วไปได้มากกว่านี้ แต่ตอนนี้ยังไม่พอcatchblock ได้อยู่แล้ว ถ้ามี lambda เพิ่มก็น่าจะยืดหยุ่นขึ้นมากเรื่องการใช้
voidเป็นชื่อชนิดนั้น จริง ๆ ใน type theory คำว่า void ไม่ได้ทำหน้าที่เป็นunitแต่หมายถึงชนิดแบบ "uninhabited" ที่ไม่มีค่าใดอยู่ได้ ตามธรรมเนียมแล้ว()หรือunitคือชนิดที่มีสมาชิกหนึ่งค่า ส่วน void เป็น return type ของฟังก์ชันอย่างabortใน C, C++ คำว่า void ก็ใช้งานกันมาได้ดีพอสมควร และโปรแกรมเมอร์สายระบบจำนวนมากก็คุ้นเคยกับมัน ผมคิดว่าการถกเถียงเรื่องคำศัพท์ใน type theory ไม่ค่อยมีความหมายกับการใช้งานจริง คนจำนวนมากที่มาใช้ Zig มีพื้นฐานจาก C, C++ ดังนั้นใช้ void ก็โอเคพอแล้ว
abortเป็นชนิดสำหรับสถานะ "ไปต่อไม่ได้" แบบเดียวกับชนิด!ของ Rust ส่วน void จะใกล้กับ unit หรือ()มากกว่า คือเป็นชนิดที่ไม่มีค่าอย่างที่ใช้งานได้จริง เกร็ดสนุกคือใน TypeScript ถ้าใช้ void กับ generic constraint จะทำให้พารามิเตอร์นั้นเป็น optional ได้ชนิด void มีประวัติยาวนานมาก ย้อนกลับไปได้ถึง ALGOL 68 ซึ่งนิยามชนิด VOID ว่าเป็นชนิดที่มีสมาชิกเพียงค่าเดียว (EMPTY)
ค่อนข้างแปลกใจที่ "Zig ไม่มี lambda" ใน C++ ใช้ lambda แทบทุกที่ แล้วอย่างตอน sort array จะนิยาม comparator กันยังไงนะ
โดยทั่วไปคือต้องประกาศฟังก์ชันแยกต่างหาก ซึ่งมองว่า Zig ไม่ค่อยสะดวกในจุดนี้
สามารถอ้างอิง anonymous struct และฟังก์ชันข้างในมันแบบ inline ได้ จริง ๆ แล้วฟีเจอร์ capture ที่นิยมใช้ใน lambda ไม่มีใน Zig แต่สามารถแทนด้วยการส่ง context parameter (มักเป็น struct) เข้าไปได้
โดยพื้นฐานก็เหมือน C คือประกาศฟังก์ชัน comparator แยกไว้ แล้วส่ง pointer ของมันเข้าไปให้ฟังก์ชัน sort
หลายคนชอบบอกว่า "ไวยากรณ์ไม่สำคัญ" แต่เอาเข้าจริงกลับกลายเป็นว่า "ไวยากรณ์ไม่สำคัญ เพราะงั้นมาใช้แบบที่ฉันชอบกันเถอะ" ตัวฉันเองก็คุ้นกับไวยากรณ์สาย C ที่แตกแขนงมาแบบ Rust/Zig/Go และยังรู้สึกว่าแนว Haskell/OCaml ที่ใช้ช่องว่างแยกการเรียกฟังก์ชันนั้นแปลกอยู่ เลยมองว่ามันเป็นอุปสรรคต่อการแพร่หลาย สิ่งที่ภาษาอื่นน่าจะเรียนรู้จากความสำเร็จของ Rust คือการผสม "ผักโขม" ของ functional programming เข้าไปใน "บราวนี" ของภาษาระบบได้อย่างลงตัว
ไม่เห็นด้วยกับคำพูดที่ว่าไวยากรณ์ไม่สำคัญ สุดท้ายแล้วไวยากรณ์คืออินเทอร์เฟซหลักที่ผู้ใช้ใช้โต้ตอบกับภาษา ทุกครั้งที่อ่านภาษาใดภาษาหนึ่ง องค์ประกอบทางไวยากรณ์เหล่านั้นจะเด่นขึ้นมาในระดับจิตใต้สำนึกเสมอ
ถ้าอยากได้ภาษาฟังก์ชันที่มีไวยากรณ์แบบสาย C ขอแนะนำ Gleam: gleam.run โค้ดก็สวยมาก
Reason ก็แนะนำได้เหมือนกัน เป็นภาษา based on OCaml แต่มีไวยากรณ์แบบสาย C: reasonml.github.io