- Zig dựa trên cú pháp dùng dấu ngoặc nhọn tương tự Rust, nhưng được cải tiến với ngữ nghĩa ngôn ngữ đơn giản hơn và các lựa chọn cú pháp tinh gọn hơn
- Literal số nguyên bắt đầu với kiểu
comptime_int cho mọi giá trị và được chuyển đổi tường minh khi gán, còn literal chuỗi dùng ký pháp chuỗi thô ngắn gọn dựa trên \\
- Literal bản ghi dạng
.x = 1 giúp việc tìm kiếm chỗ ghi vào trường dễ hơn, và mọi kiểu đều được biểu diễn nhất quán bằng ký pháp tiền tố
- Dùng
and·or làm từ khóa điều khiển luồng, còn các câu lệnh if·loop có thể tùy chọn lược bỏ dấu ngoặc nhọn, với formatter đảm bảo tính an toàn
- Không có namespace và mọi thứ đều là biểu thức, qua đó hợp nhất cú pháp kiểu·giá trị·mẫu, đồng thời dùng gọn gàng generics·literal bản ghi·hàm dựng sẵn (
@import, @as v.v.)
Tổng quan
- Zig có bề ngoài giống Rust nhưng chọn cấu trúc ngôn ngữ đơn giản hơn
- Thiết kế cú pháp tập trung vào thân thiện với grep, nhất quán cú pháp, và giảm nhiễu thị giác không cần thiết
Literal số nguyên
const an_integer = 92;
assert(@TypeOf(an_integer) == comptime_int);
const x: i32 = 92;
const y = @as(i32, 92);
- Mọi literal số nguyên đều có kiểu
comptime_int
- Khi gán vào biến, cần chỉ định kiểu tường minh hoặc dùng
@as để chuyển đổi
- Dạng
var x = 92; không hoạt động, cần có kiểu tường minh
Literal chuỗi
const raw =
\\Roses are red
\\ Violets are blue,
\\Sugar is sweet
\\ And so are you.
\\
;
- Mỗi dòng là một token riêng nên không có vấn đề về thụt lề
- Không cần escape chính
\\
Literal bản ghi
const p: Point = .{
.x = 1,
.y = 2,
};
- Dạng
.x = 1 thuận lợi cho việc phân biệt đọc/ghi
- Ký pháp
.{} tự tách biệt với block và tự động chuyển sang kiểu kết quả
Ký pháp kiểu
u32 // số nguyên
[3]u32 // mảng độ dài 3
?[3]u32 // mảng có thể null
*const ?[3]u32 // con trỏ hằng
- Mọi kiểu đều dùng ký pháp tiền tố (prefix)
- Giải tham chiếu dùng ký pháp hậu tố (
ptr.*)
Định danh
const @"a name with space" = 42;
- Có thể tránh xung đột với từ khóa hoặc đặt tên đặc biệt
Khai báo hàm
pub fn main() void {}
fn add(x: i32, y: i32) i32 {
return x + y;
}
- Từ khóa
fn đi liền với tên hàm nên dễ tìm kiếm
- Không dùng
-> để ghi kiểu trả về
Khai báo biến
const mid = lo + @divFloor(hi - lo, 2);
var count: u32 = 0;
- Dùng
const và var
- Ghi kiểu theo thứ tự
tên: kiểu
Điều khiển luồng: and/or
while (count > 0 and ascii.isWhitespace(buffer[count - 1])) {
count -= 1;
}
and, or là từ khóa điều khiển luồng
- Phép toán bit dùng
&, |
Câu lệnh if
.direction = if (prng.boolean()) .ascending else .descending;
- Bắt buộc có ngoặc đơn, ngoặc nhọn là tùy chọn
zig fmt đảm bảo định dạng an toàn
Vòng lặp
for (0..10) |i| {
print("{d}\n", .{i});
} else @panic("loop safety counter exceeded");
- Cả
for và while đều hỗ trợ mệnh đề else
- Bộ lặp và tên phần tử được sắp xếp trực quan
Namespace và phân giải tên
const std = @import("std");
const ArrayList = std.ArrayList;
- Cấm shadowing biến
- Không có namespace và cũng không có glob import
Mọi thứ đều là biểu thức
const E = enum { a, b };
const e: if (true) E else void = .a;
- Hợp nhất cú pháp kiểu·giá trị·mẫu
- Có thể đặt biểu thức điều kiện ở vị trí kiểu
Generics
fn ArrayListType(comptime T: type) type {
return struct {
fn init() void {}
};
}
var xs: ArrayListType(u32) = .init();
- Generics được biểu diễn bằng cú pháp gọi hàm (
Type(T))
- Tham số kiểu luôn được ghi tường minh
Hàm dựng sẵn
const foo = @import("./foo.zig");
const num = @as(i32, 92);
- Dùng tiền tố
@ để gọi các tính năng do compiler cung cấp
@import hiển thị rõ đường dẫn tệp
- Đối số bắt buộc phải là literal chuỗi
Kết luận
- Cú pháp Zig là một ví dụ cho thấy tập hợp các lựa chọn nhỏ có thể tạo nên một ngôn ngữ dễ đọc
- Khi giảm số lượng tính năng, lượng cú pháp cần thiết cũng giảm theo, và khả năng xung đột giữa các cú pháp cũng giảm
- Mượn những ý tưởng hay từ các ngôn ngữ hiện có, nhưng khi cần thì vẫn mạnh dạn đưa vào cú pháp mới
1 bình luận
Ý kiến trên Hacker News
Bài này đào sâu nhiều đánh đổi trong thiết kế cú pháp, và tôi thực sự ấn tượng với chủ nghĩa tối giản, tính nhất quán, cũng như sự tập trung gần như tàn nhẫn vào khả năng đọc của cú pháp Zig. Điều tôi thích là đây không phải vẻ đẹp trừu tượng, mà là kiểu “brutalism” không tạo ra bất ngờ trong mục đích sử dụng công nghiệp. Kiểu thiết kế cú pháp cân bằng như vậy thật sự hiếm, và tôi nghĩ Zig đã làm rất tốt
Hơi tiếc là bài viết không nhắc đến xử lý lỗi. Cách
try/catchcủa Zig rất xuất sắc, và là cách xử lý lỗi tôi thích nhất trong nhiều ngôn ngữ. Giá mà phần này cũng được giới thiệu thì hay hơnSức hấp dẫn thật sự của Zig không nằm ở “tính dễ đọc đẹp mắt bề ngoài”, mà ở vẻ đẹp nhất quán có được nhờ trừu tượng hóa. Giống như phép so sánh giữa S-expression và M-expression, một cách tiếp cận tốt cho trường hợp phổ biến về lâu dài thường tốt hơn một thiết kế đặc biệt cho nhiều tình huống ngoại lệ. Nếu thêm đủ kiểu ngoại lệ như C++, cuối cùng bạn chỉ làm tăng gánh nặng phải ghi nhớ mọi quy tắc. Trong thiết kế ngôn ngữ, nếu theo đuổi sự đơn giản và nhất quán một cách mù quáng, bạn có thể rơi vào “Turing tarpit”, nơi người dùng phải tự gánh hết độ phức tạp, nên cách tiếp cận quan trọng là để các trường hợp đặc biệt được giải quyết tự nhiên từ những quy tắc chung. Có thể thấy một ví dụ như vậy trong comic XKCD New Pet
Nếu có ví dụ nào đặc biệt ấn tượng thì tôi rất muốn được chia sẻ
Về việc Zig dùng kiểu khai báo kiểu theo dạng
tên:kiểugiống Rust, tôi lại thích cách truyền thống là kiểu đứng trước hơn. Khi kiểm tra lại một khai báo biến, thứ tôi muốn biết nhất là kiểu của biến đó, và nếu không thể tìm ra nhanh thì khá bất tiện. Đặc biệt ở Rust có nhiều yếu tố lặp lại không cần thiết nhưlet mut, thành ra còn rườm rà hơn, còn kiểu như C, C++ với kiểu đứng trước cũng rất ổn. Thực tế, tôi nghĩ lý tưởng nhất là chỉ dùng suy luận kiểu ở mức tối thiểu, đúng nơi cần thiếtTừ khóa
letcũng có ích vì nó làm rõ rằng đây là một câu lệnh khai báo. Nếu không thì bạn có thể gặp các vấn đề phân tích cú pháp mơ hồ như trong C++Tôi cũng luôn cố nhìn kiểu của biến trước nên thích cách kiểu đứng đầu hơn. Xét từ phía parser thì xử lý tên trước sẽ tiện hơn, và tôi hiểu TypeScript chọn cấu trúc này vì tính tương thích với JavaScript. Cuối cùng điều quan trọng vẫn là thư viện chuẩn dễ dùng. Như trong những ví dụ lạm dụng hệ thống kiểu quá mức, thay vì cố biểu diễn mọi trạng thái bằng kiểu, điều quan trọng hơn là truyền đạt rõ ràng ý định
Tôi có kéo lên trên để xem kiểu biến trong mã, nhưng nếu kiểu đứng trước thì lại càng khó tìm đúng khai báo biến mà tôi muốn. Tên kiểu nằm ở đầu, lại có độ dài thay đổi, khiến mắt phải lia qua lại trái phải liên tục nên tôi thấy kém hiệu quả
Trong đa số trường hợp, editor chỉ cần hover chuột là hiện ngay thông tin kiểu, nên vị trí của kiểu trong mã có thể không còn quá quan trọng. Lý do Rust verbose phần lớn là về mặt triển khai để tránh mơ hồ khi phân tích cú pháp. Nếu kiểu đứng trước như C, C++ thì khó grep để tìm các biến được khai báo với một tên cụ thể, và kiểu return đặt phía trước là phong cách được đưa vào vì template, nhưng trong một số trường hợp lại giúp đọc và tìm mã dễ hơn
Cá nhân tôi thích kiểu khai báo kiểu theo phong cách Pascal hơn. Ngay cả khi suy luận kiểu cũng không cần tới một cơ chế vòng vo như
auto, và xét từ góc độ parsing thì cũng ít mơ hồ hơn. VớiMyClass x, không dễ biết ngayMyClasslà kiểu hay tên biến, nên cách này giúp giảm sự nhập nhằng đóVề cú pháp raw/multiline string của Zig, cách phải dùng nhiều dấu \ trông quá rối rắm và cực đoan
Nếu từng format multiline string trong Python, C++, Rust... thì bạn sẽ hiểu sự bất tiện đó. Luôn có vấn đề chuyện thụt lề bị đưa vào nội dung chuỗi, còn những trường hợp có chế độ xóa thụt lề như YAML thì lại càng tăng thêm bối rối. Cách của Zig rất rõ ràng khi nói đến thụt lề
Ban đầu tôi cũng thấy cú pháp này quá bất tiện, nhưng dùng Zig một thời gian thì dần quen và còn thấy điểm hay của nó. Zig khá mới lạ nên lúc đầu có thể không thích, nhưng dùng thật rồi sẽ nhận ra ưu điểm
Thật ra đây không phải một cú pháp điên rồ, mà là một vấn đề điên rồ cần giải quyết cho tốt: làm sao nhúng an toàn multiline string bên trong multiline string. Ở Zig không cần escape riêng và cũng không phải lo chuyện thụt lề, điểm đó rất hay
trimIndentcủa Kotlin, text block của Go hay Java, và đặc biệt kiểu raw string bằng backtick của Go cho tôi cảm giác mượt hơn. Trong Zig, vì\\nên tôi thường lách qua bằng@embedFileVề mặt thị giác tôi không thích
\\, nhưng tôi nghĩ đây là cách giải quyết gọn gàng cho vấn đề multiline literal và thụt lề. Tôi không biết ngôn ngữ nào khác giải quyết chuyện này mà không cần đến hàmCú pháp Zig cho cảm giác hơi rối. Những cấu trúc bắt đầu bằng @ như
@TypeOfhay cú pháp khởi tạo như.{.x}tạo cảm giác khá lạ. Có thể là vì tôi chưa đủ quen với Zig, nhưng nhìn chung tôi có ấn tượng là mã hơi khó đọcTôi thích cú pháp của Odin hơn nhiều vì tối giản và được gọt giũa tốt hơn. Zig cho cảm giác hơi lộn xộn
.trong Zig đóng vai trò placeholder cho kiểu được suy luận. Ví dụ, bạn có thể khởi tạo đối tượng như sauhoặc nếu muốn nói rõ suy luận kiểu thì
Trong đối số hàm cũng có thể lược bỏ kiểu nên gọn hơn. Trong Rust, ở những tình huống như vậy bạn phải ghi kiểu một cách tường minh
Ngay cả khi khởi tạo struct lồng nhau, cách suy luận của Zig cũng hữu ích hơn nhiều. Ở Rust, phải ghi kiểu tường minh ở khắp nơi thì mã có thể nhanh chóng trở nên rối mắt. Dù vậy, tôi vẫn nghĩ bỏ dấu chấm đứng trước sẽ tiện hơn, nhưng có vẻ họ giữ nó để đơn giản hóa việc triển khai parser. Cú pháp
x: 123hoặc.x = 123lần lượt được mượn từ JS và C99. Cá nhân tôi dùng cả hai thường xuyên nên không thấy lạTôi thích cách raw string literal của C# 11 hơn nhiều. Nó tự động căn thụt lề các dòng còn lại theo mức thụt lề của dòng đầu tiên. Ngoài ra còn có thể dùng dấu ngoặc nhọn như ký tự bình thường. Nếu
$xuất hiện nhiều lần thì dấu ngoặc nhọn sẽ được xử lý hoàn toàn như giá trị ký tự"""cuối cùng, và dòng đầu tiên cũng có thể được thụt lề. Tôi rất vui khi bạn thích tính năng này, và tôi cũng tự hào đây là một tính năng tốtCú pháp của Zig cũng tốt, nhưng xét ở chỗ vẫn có thể viết rất gọn như Go mà không cần dấu chấm phẩy hay
:, tôi không nghĩ nó đạt đến mức “đáng yêu”. Nếu phải so thì đúng là nó cải thiện nhiều hơn Rust, nhưng Go cũng đã đủ xuất sắc rồiNgược lại, cú pháp quá tối giản như Go đôi khi lại khó diễn giải hơn khi đọc. Thời gian đọc mã luôn nhiều hơn thời gian tự viết mã, nên sự ngắn gọn quá mức đôi khi lại dễ gây lỗi và làm debug khó hơn. CoffeeScript hay J là những ví dụ tiêu biểu của cú pháp bị rút gọn quá đà
Tôi không nghĩ cứ lược bớt yếu tố cú pháp là cú pháp sẽ tốt hơn. Nếu đúng vậy thì ai cũng sẽ viết như Lisp, và văn bản cũng sẽ được viết như scriptio continua (kiểu viết cổ không có khoảng trắng). Xem Wikipedia về scriptio continua
Nhìn chung tôi hài lòng với Zig, nhưng vẫn thấy tiếc ở vài điểm sau
a?.b?.c). Nếu có hỗ trợ kiểu monad thì có thể chain tổng quát hơn, nhưng hiện vẫn còn thiếucatch, nếu có thêm lambda thì có lẽ sẽ linh hoạt hơnVề việc dùng
voidtrong tên kiểu, thực ra trong type theory thìvoidkhông phải vai trò củaunitmà là kiểu “uninhabited”, tức kiểu không có giá trị. Theo truyền thống thì()hayunitmới là kiểu có một phần tử.voidlà kiểu trả về của những hàm nhưabortTrong C, C++,
voidvẫn được dùng khá ổn nên rất quen thuộc với nhiều lập trình viên hệ thống. Tranh cãi thuật ngữ theo lý thuyết hình thức theo tôi là vô nghĩa trong sử dụng thực tế. Nhiều người đến với Zig có nền tảng C, C++, nên dùngvoidlà hoàn toàn ổnabortthuộc về kiểu dành cho trạng thái “không thể tới được”, giống kiểu!trong Rust.voidđúng hơn là gần vớiunithay(), và là kiểu không có giá trị. Có một mẹo thú vị là trong TypeScript, nếu dùngvoidtrong generic constraint thì có thể biến tham số đó thành optionalKiểu
voidcó truyền thống rất lâu đời, truy ngược được tới ALGOL 68. Ở đó, kiểuVOIDđược định nghĩa là kiểu chỉ có một phần tử duy nhất (EMPTY)Tôi ngạc nhiên khi biết “Zig không có lambda”. Trong C++ tôi gần như dùng lambda ở khắp nơi, vậy khi sort mảng chẳng hạn thì comparator được định nghĩa kiểu gì?
Thường là phải khai báo hàm riêng, và ở điểm đó tôi thấy Zig khá bất tiện
Có thể dùng struct ẩn danh và tham chiếu inline tới hàm bên trong nó. Thật ra Zig không có cơ chế capture như lambda hay dùng, nhưng có thể thay thế bằng cách truyền context parameter, thường là một struct
Về cơ bản cũng như C: khai báo một hàm riêng rồi truyền con trỏ hàm đó vào hàm sắp xếp
Nhiều người nói “cú pháp không quan trọng”, nhưng trên thực tế lại là “cú pháp không quan trọng nên hãy dùng kiểu tôi thích”. Tôi cũng quen với cú pháp phát sinh từ họ C như Rust/Zig/Go, còn kiểu gọi hàm phân tách bằng khoảng trắng như Haskell/OCaml thì vẫn còn lạ và theo tôi là một rào cản với số đông. Cũng như thành công của Rust, việc hòa trộn tốt “rau bina” của lập trình hàm vào “brownie” của ngôn ngữ hệ thống là điều các ngôn ngữ khác có thể học hỏi
Tôi không đồng ý với câu “cú pháp không quan trọng”. Cuối cùng thì cú pháp là giao diện chính để người dùng tương tác với ngôn ngữ. Mỗi khi đọc một ngôn ngữ nào đó, các yếu tố cú pháp luôn nổi bật hơn trong nhận thức một cách vô thức
Nếu bạn muốn một ngôn ngữ hàm với cú pháp họ C thì tôi gợi ý Gleam: gleam.run Mã cũng rất đẹp
Reason cũng đáng thử. Nó dựa trên OCaml nhưng có cú pháp họ C: reasonml.github.io