7 điểm bởi GN⁺ 2025-08-22 | 1 bình luận | Chia sẻ qua WhatsApp
  • 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 constvar
  • 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ả forwhile đề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

 
GN⁺ 2025-08-22
Ý 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/catch củ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ơn

    • Sứ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ểu giố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ết

    • Từ khóa let cũ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ới MyClass x, không dễ biết ngay MyClass là 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

    • trimIndent củ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 @embedFile

    • Về 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àm

  • Cú pháp Zig cho cảm giác hơi rối. Những cấu trúc bắt đầu bằng @ như @TypeOf hay 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ó đọc

    • Tô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ư sau

      const p = Point{ .x = 123, .y = 234 };
      

      hoặc nếu muốn nói rõ suy luận kiểu thì

      const p: Point = .{ .x = 123, .y = 234 };
      

      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

      takePoint(Point{ x: 123, y: 234 });
      

      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: 123 hoặc .x = 123 lầ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ự

    string json = $"""
       {title}
    
         Welcome to {sitename}.
    
       """;
    string json = $$"""
       {{title}}
    
         Welcome to {{sitename}}, which uses the {sitename} syntax.
    
       """;
    
    • (Với tư cách là tác giả tính năng raw string literal của C#) thực ra chuẩn căn lề là theo dòng """ 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ốt
  • Cú 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ồi

    • Ngượ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

    • Khó chỉ định giá trị trả về của block. Sẽ hay hơn nếu như Rust, biểu thức cuối cùng được tự động nhận là giá trị trả về, nhưng ở Zig phải dùng label các kiểu nên khá phiền
    • Không thể chain optional type (ví dụ 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ếu
    • Không có hỗ trợ lambda. Dù hiện đã có các khối hàm trong vòng lặp hay block catch, nếu có thêm lambda thì có lẽ sẽ linh hoạt hơn
  • Về việc dùng void trong tên kiểu, thực ra trong type theory thì void không phải vai trò của unit mà là kiểu “uninhabited”, tức kiểu không có giá trị. Theo truyền thống thì () hay unit mới là kiểu có một phần tử. void là kiểu trả về của những hàm như abort

    • Trong C, C++, void vẫ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ùng void là hoàn toàn ổn

    • abort thuộ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ới unit hay (), và là kiểu không có giá trị. Có một mẹo thú vị là trong TypeScript, nếu dùng void trong generic constraint thì có thể biến tham số đó thành optional

    • Kiểu void có truyền thống rất lâu đời, truy ngược được tới ALGOL 68. Ở đó, kiểu VOID đượ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

      fn spawn_greeter(i: Int) {
       process.spawn(fn() {
        let n = int.to_string(i)
        io.println("Hello from "  n)
       })
      }
      

      Reason cũng đáng thử. Nó dựa trên OCaml nhưng có cú pháp họ C: reasonml.github.io