1 điểm bởi GN⁺ 2025-01-08 | 1 bình luận | Chia sẻ qua WhatsApp
  • Siêu lập trình tại thời điểm biên dịch (comptime) của Zig có thể được đọc giống như mã runtime, nhưng vẫn tạo ra mã thực thi hiệu quả như các hàm C viết tay cho những đoạn lặp lại như cộng các trường của struct
  • Điểm sử dụng cốt lõi của Zig comptime là xử lý lập trình generic bằng comptime T: typeanytype thay vì có cú pháp generic riêng
  • Runtime, comptime và hệ thống build cùng chia sẻ một ngôn ngữ Zig, nên cả những hàm như Fizz Buzz cũng có thể chạy trong lúc biên dịch để tạo thành mảng đã được tính sẵn
  • Trình biên dịch sẽ đánh giá trước những phần có thể, và chỉ giữ lại các phần cần giá trị runtime dưới dạng bytecode đầu ra để sau đó trở thành mã máy
  • Zig comptime có thể giải quyết các vấn đề tương tự sinh mã văn bản, nhưng không thay thế từ khóa tùy ý như macro C nên giảm gánh nặng khi đọc và debug mã

Góc nhìn để đọc Zig comptime

  • Comptime của Zig là một tính năng siêu lập trình, nhưng khi đọc hành vi cuối cùng thì không nhất thiết phải quá để tâm đến ranh giới giữa thời điểm biên dịch và runtime
  • Mục thứ ba trong zig zen, “Favor reading code over writing code”, là một tiêu chí quan trọng
    • Siêu lập trình dựa trên macro hoặc sinh mã có thể tạo ra hai lớp: mã nguồn gốc và mã đã được mở rộng
    • Lớp gián tiếp như vậy khiến việc đọc mã, debug và thay đổi hành vi trở nên khó hơn
  • Mã runtime tính tổng mảng [3]i64 = .{1,2,3} và mã comptime cộng các trường a, b, c của struct MyStruct được đặt cạnh nhau để so sánh
    • inline for (comptime std.meta.fieldNames(MyStruct)) được dùng để lặp qua tên các trường
    • @field(my_struct, field_name) truy cập trường bằng tên trường đã biết tại thời điểm biên dịch
  • Ví dụ cộng các trường của struct trông giống reflection ở runtime, nhưng trong file thực thi sẽ chỉ còn lại mã hiệu quả như thể đã viết tay một hàm cộng riêng cho kiểu struct đó
  • Quá trình biến đổi trong ví dụ không phải là tái hiện nguyên xi cách Zig triển khai, mà gần với một giải thích mang tính khái niệm hơn
  • Nhóm lõi của Zig đang phát triển một trình gỡ lỗi có thể từng bước chạy mã trộn giữa comptime và runtime như ví dụ

Xử lý generic bằng comptime

  • Zig không có tính năng generic riêng biệt
  • Để tạo kiểu generic, ta viết một hàm nhận vào kiểu và trả về kiểu
    • Ví dụ: pub fn GenericMyStruct(comptime T: type) type
    • Các trường a, b, c của struct được trả về đều dùng kiểu T
  • Hàm generic cũng có thể được viết theo cùng cách
    • Ví dụ: fn quadratic(comptime T: type, a: T, b: T, c: T, x: T) T
    • Có thể chỉ rõ đối số kiểu như quadratic(f32, ...)quadratic(i64, ...)
  • Nếu kiểu đối số không quan trọng với các phần khác của chữ ký hàm, có thể suy luận bằng kiểu đặc biệt anytype
  • Cách này không thể giải thích toàn bộ comptime, nhưng là điểm khởi đầu để hiểu các tác vụ generic phổ biến trong Zig

Chạy cùng một mã Zig tại thời điểm biên dịch

  • Zig dùng cùng một ngôn ngữ cho runtime, comptime và hệ thống build
  • Ví dụ Fizz Buzz trước hết viết hàm fizzBuzz(writer) như một hàm xuất runtime thông thường
    • Lặp từ 1 đến 100
    • Với bội số của 35 thì in fizzbuzz, bội số của 3 thì in fizz, bội số của 5 thì in buzz, còn lại in số
  • Khi chạy chính hàm fizzBuzz đó bên trong khối comptime, có thể tính sẵn toàn bộ đầu ra
    • std.io.countingWriter(std.io.null_writer) được dùng để tính trước số byte cần thiết
    • Tạo mảng buffer: [cw.bytes_written]u8 với độ dài đã tính
    • Ghi lại vào std.io.fixedBufferStream(&buffer) để tạo giá trị full_fizzbuzz
  • Nếu chỉ đo phần cốt lõi, bản đã tính sẵn chạy nhanh hơn khoảng 9 lần
    • Vì bản thân ví dụ khá nhỏ nên thời gian chạy tổng thể còn bị chi phối nhiều hơn bởi các yếu tố khác
  • Comptime và runtime có khác biệt về các giá trị và tính năng có thể truy cập
    • Các giá trị comptime_int, comptime_float, type chỉ có thể truy cập ở comptime
    • Một số hàm chỉ nhận đối số comptime nên trên thực tế là chỉ dùng được ở comptime
    • System call và các tính năng sử dụng chúng chỉ truy cập được ở runtime
  • Mã không dùng các tính năng chuyên biệt đó sẽ hoạt động giống hệt nhau ở comptime và runtime

Hiểu comptime như đánh giá từng phần

  • Comptime có thể được xem là đánh giá từng phần (partial evaluation) diễn ra trong quá trình biên dịch
  • Đánh giá từng phần là cách, khi một hàm chỉ được cung cấp một phần đối số, trước hết thay thế và tính các biểu thức chỉ dùng giá trị đã biết, rồi tạo ra một hàm mới chỉ nhận các đối số chưa biết còn lại
  • Ví dụ cộng mảng cho thấy quá trình đánh giá theo từng bước
    • Vòng lặp for được tách thành các câu lệnh riêng cho từng lần lặp
    • array[0], array[1], array[2] lần lượt được thay bằng 1, 2, 3
    • sum được cập nhật theo 0 → 1 → 3 → 6
    • Cuối cùng được đơn giản hóa đến dạng std.debug.print(..., .{6})
  • Ví dụ cộng các trường của struct cũng có thể được đọc theo cách tương tự
    • inline for chạy ở comptime nên được trải thành các câu lệnh riêng cho các trường "a", "b", "c"
    • @field(my_struct, "a") được biến thành my_struct.a
    • Hàm cuối cùng trở thành dạng trực tiếp có sum += my_struct.a, sum += my_struct.b, sum += my_struct.c
  • Cách này giúp biểu đạt trực tiếp ý đồ trong mã, đồng thời giảm nhu cầu phải để lại chú thích yêu cầu cập nhật thủ công hàm cộng khi các trường thay đổi

Đánh giá tại thời điểm biên dịch và phát sinh mã runtime

  • Zig comptime cũng có thể được xem là sự kết hợp giữa đánh giá tại thời điểm biên dịch và phát sinh mã runtime
  • Mã đầu vào được trình biên dịch thực thi
    • Những câu lệnh có thể biết tại thời điểm biên dịch sẽ được đánh giá ngay
    • Những câu lệnh cần giá trị runtime sẽ được thêm vào mã đầu ra
  • Góc nhìn này gần nhất với cách xử lý thực tế của trình biên dịch Zig
    • Zig trước hết phân tích cú pháp
    • Biến mã thành bytecode cho máy ảo
    • VM đánh giá những phần có thể, còn phần cần xử lý ở runtime thì phát sinh thành bytecode mới
    • Bytecode mới sau đó được chuyển thành mã máy
  • Với các câu điều kiện như if phụ thuộc vào đầu vào runtime, cả hai nhánh đều được phát sinh
  • Mã chết không bị phân tích ngữ nghĩa
    • Dù viết một hàm sai, có thể sẽ chưa có lỗi biên dịch cho đến khi nó thực sự được sử dụng
    • Đặc tính này giúp biên dịch hiệu quả hơn và cho phép biên dịch có điều kiện một cách tự nhiên mà không cần #ifdef
  • Tên kiểu trong Zig cũng là biểu thức đánh giá một giá trị type ở comptime
    • Ngay cả những tên kiểu trông đơn giản như kiểu đối số hàm cũng là kết quả của đánh giá comptime
    • Đặc tính này cho phép các ví dụ generic phía trên
    • Khi cần, có thể tính kiểu bằng các biểu thức phức tạp hơn
  • Phân tích tĩnh của Zig phức tạp hơn nhiều ngôn ngữ kiểu tĩnh khác
    • Để xác định toàn bộ kiểu, cần chạy một phần đáng kể của trình biên dịch
    • Cho đến khi công cụ bắt kịp, các tính năng editor như tự động hoàn thành mã có thể không phải lúc nào cũng hoạt động tốt

Tương quan với sinh mã văn bản

  • Zig comptime khác về hình thức so với sinh mã văn bản, nhưng có thể giải quyết các bài toán tương tự
  • Hàm sinh mã writeSumFn dùng writer.print để xuất mã nguồn của hàm sumFields
    • Phần mã chạy trong bộ sinh tương ứng với phần comptime của Zig
    • Phần mã mà bộ sinh xuất ra tương ứng với mã runtime
  • Phiên bản comptime của Zig biểu đạt cùng logic đó trực tiếp hơn
    • Lấy tên trường bằng std.meta.fieldNames(MyStruct)
    • Dùng inline for@field để tạo mã truy cập trường
  • Sinh mã dùng thông tin kiểu làm đầu vào trở nên đơn giản hơn trong Zig
    • Cách sinh văn bản phải tự giải quyết riêng việc lấy tên kiểu và tên trường từ đâu
    • Zig có thể xử lý trực tiếp chính kiểu và thông tin trường tại comptime
  • Khi dùng đầu vào như đặc tả bên ngoài, có thể nhúng nội dung file bằng @embedFile rồi phân tích như bình thường
  • Trong ví dụ struct generic, comptime xử lý việc tham chiếu tên kiểu trực tiếp hơn so với sinh mã văn bản
    • Sinh văn bản phải nối chuỗi như MyStruct_{s} và giữ chúng nhất quán
    • Comptime dùng trực tiếp hàm như GenericMyStruct(T)
  • Cũng có ngoại lệ
    • Có thể tạo kiểu mà tên trường được quyết định ở comptime
    • Trong trường hợp này phải gọi hàm dựng sẵn với một đặc tả chứa danh sách định nghĩa trường
    • Không thể định nghĩa các khai báo như method cho các kiểu như vậy
    • Điều đó không giới hạn sức biểu đạt của mã nói chung, nhưng có giới hạn về hình thức API có thể lộ ra cho mã khác
  • Phần lớn các tác vụ hợp lý có thể làm bằng macro văn bản như trong C cũng có thể làm bằng comptime, nhưng hình dạng của mã có thể rất khác
  • Zig không cung cấp khả năng thay thế từ khóa bằng tên khác như macro văn bản

Tài liệu đọc thêm và ví dụ trong thư viện chuẩn

  • Zig bản thân nó không phải là một ngôn ngữ chỉ dựa vào riêng comptime, và có thể tìm hiểu thêm tại website chính thức
  • Tài liệu tham chiếu ngôn ngữ đề cập các tính năng cụ thể của comptime
  • Thư viện chuẩn của Zig có các ví dụ sử dụng comptime
    • Hàm định dạng mà std.debug.print sử dụng là một hàm generic rất mạnh
      • Nhiều ngôn ngữ phân tích chuỗi định dạng ở runtime, rồi có thể thêm một bộ kiểm tra riêng để phát hiện lỗi sớm
      • Zig phân tích chuỗi định dạng ở comptime để tạo mã xuất hiệu quả và thực hiện mọi kiểm tra ngay tại thời điểm biên dịch
    • ArrayList là một container generic tương đối đơn giản nhưng đầy đủ tính năng
    • Hàm main của Zig có thể có nhiều kiểu trả về, và điều này không phải nhờ ma thuật của trình biên dịch mà được xử lý bằng mã comptime thông thường

1 bình luận

 
GN⁺ 2025-01-08
Các ý kiến trên Hacker News
  • Mong bài viết đi sâu hơn vào những vấn đề đã được phát hiện trong lập trình tại thời điểm biên dịch, thay vì khen ngợi một cách thiếu phê phán
    Lập trình theo giai đoạn không phải là mới, và các họ Lisp cũng đã làm những việc tương tự trong nhiều thập kỷ, nhưng trong quá trình đó đã lộ ra nhiều đánh đổi và vấn đề về thiết kế. Ví dụ, generics theo cách này phá vỡ tính tham số (parametricity), khiến khó suy luận chỉ từ type signature của hàm. Cũng chưa rõ Zig xử lý kiểu generic đệ quy như thế nào, và việc kiểm tra kiểu cùng tính toán tại thời điểm biên dịch tương tác theo thứ tự nào, vào lúc nào cũng là một lựa chọn quan trọng. Bài viết nói rằng mã chạy tại thời điểm biên dịch có thể tạo ra không chỉ giá trị mà cả mã, nhưng không đề cập đến tính vệ sinh (hygiene). Một thảo luận liên quan khá hay là https://typesanitizer.com/blog/zig-generics.html

    • Tôi khá thích Zig, đã theo dõi trong vài năm và thỉnh thoảng dùng, nhưng comptime đặc biệt tỏa sáng trong các trường hợp như generics, khởi tạo tại thời điểm biên dịch cho các cấu trúc dữ liệu phức tạp, và sinh mã theo từng target
      Tuy vậy, trong những tình huống khác, khi thấy comptime trong mã Zig đôi lúc tôi lại nghĩ “à…”. Giống như macro của Lisp, nó rất dễ bị dùng để né những vấn đề vốn có thể không tồn tại, hoặc biến mất nếu tổ chức cấu trúc mã tốt hơn. Ví dụ trong bài gốc, duyệt qua các trường của struct để cộng giá trị là một mẫu điển hình về cách mọi người dùng comptime, nhưng trong nhiều trường hợp dùng một cấu trúc dữ liệu thực sự có thể duyệt được như std.enums.EnumArray sẽ tốt hơn
    • Tôi thắc mắc việc nói tính tham số bị phá vỡ là ở cấp độ lập trình viên thực tế hiểu hành vi của hàm, hay ở cấp độ suy luận trong một hệ thống lý thuyết kiểu như phép tính lambda có kiểu
      Về mặt lý thuyết thì rõ ràng có điều đáng lo, nhưng trong một ngôn ngữ thực dụng thì mức độ quan trọng lại là chuyện khác. Template C++ cũng phá vỡ tính tham số thông qua template specialization, nhưng trong thực tế thường không phải vấn đề lớn, và thậm chí còn cho phép tối ưu hóa. Chẳng hạn, một implementation lưu từng phần tử thật sự bằng 1 bit như std::vector<bool> chỉ có thể làm được nhờ sự linh hoạt này
    • Với tư cách tác giả, tôi viết bài này để hệ thống lại sau khi không giải thích tốt được nội dung định trình bày ở một buổi gặp mặt, nên trọng tâm là giải thích hơn là phê bình
      Tôi xem comptime kiểu Zig là một đánh đổi trực tiếp giữa khả năng suy luận và sức biểu đạt so với các signature hàm được định nghĩa tĩnh hơn. Nó chỉ ảnh hưởng đến mã thực hiện kiểu suy luận đó dựa trên kiểu, và là phần có thể chọn dùng khi cần. Ngay cả khi type signature không đủ, thường chỉ cần đọc vài chục dòng source là phần lớn cách dùng đã rõ, và tôi chưa từng bị rối lâu. Với ví dụ kiểu generic đệ quy, nếu dùng tên đang được khai báo thì sẽ báo “dependency loop detected”, nhưng có cách đi vòng, và ví dụ generic trong bài cũng tham chiếu đến chính nó. Kiểm tra kiểu diễn ra trong lúc comptime; ví dụ @compileLog("Hi") đầu tiên sẽ được chạy, nhưng nếu sau đó const a: u32 = "42"; gây lỗi kiểu thì sẽ không tới log thứ hai. Có thể nảy sinh vài vấn đề tinh vi trong kiểm tra kiểu giữa comptime và runtime, nhưng chúng chỉ xuất hiện trong mã khá khó hiểu, dễ giải quyết, và đội ngũ nòng cốt cũng biết nên tôi kỳ vọng sẽ được xử lý trước bản 1.0. Cần giải thích thêm chính xác “hygiene” có nghĩa là gì
    • Việc có thể suy luận chỉ từ khai báo hàm là chuyện tách biệt với thực thi tại thời điểm biên dịch
      Nếu một hàm có mục đích logic rõ ràng, tên hàm và tên tham số được đặt tốt, thì chỉ nhìn khai báo cũng có thể suy luận được. Nhãn tham số, tên kiểu và những phần lập trình viên có thể chỉ định cũng có thể xem là một phần của tên. Tôi đồng ý với kết luận trong tiêu đề của bài được link, nhưng bài đó giống một bài tranh luận kiểu đội debate để ghi điểm hơn là lập luận có ý nghĩa. Cách đặt vấn đề tốt hơn là tính linh hoạt so với độ phức tạp. Một hệ thống generic cố định, nếu được thiết kế tốt, sẽ đơn giản hơn hệ thống có thể lập trình được nhưng kém linh hoạt hơn. Ưu điểm của Zig là ngôn ngữ metaprogramming gần như giống với ngôn ngữ thông thường, và bản thân ngôn ngữ thông thường đó cũng đơn giản, nên giảm chi phí phức tạp bổ sung. Tuy nhiên, vẫn có độ phức tạp phát sinh khi mã thời điểm biên dịch và mã runtime bị trộn lẫn, lại có ít dấu hiệu, khiến lập trình viên khó phân biệt cái gì chạy lúc nào. Tôi nghĩ sẽ thú vị nếu language server hoặc plugin editor cung cấp một “comptime shader” tô màu nền khác cho mã comptime
    • Điều đáng nói là chủ đề này đang được bàn dựa trên một demo 15 phút và còn thiếu hiểu biết về các đánh đổi thực tế. Bài viết của Varun Gandhi được link rất xuất sắc
      Từ kinh nghiệm với Rust, nhiều việc mọi người muốn làm bằng const generics có lẽ sẽ dễ hơn nếu có tính năng như comptime. Việc cho phép số học trên const generics trong khi vẫn giữ tính tham số là khó triển khai, và nếu thứ thực sự muốn có chỉ là kiểu “trait cho hàm băm có kích thước đầu ra là N”, thì việc từ bỏ tính tham số cho mục đích đó và biến trait theo N thành một giai đoạn sinh mã trước đó cũng có thể chấp nhận được. Nhưng macro Rust lại quá linh hoạt và phiền toái để dùng theo kiểu ấy. Dù vậy, ngay khi thay thế đa hình tham số bằng một cơ chế sinh mã ngây thơ, con đường gian nan sẽ mở ra
  • D đã có tính năng này từ 17 năm trước. Các tính năng của D vẫn đều đặn được chuyển sang những ngôn ngữ khác
    Zig dùng từ khóa comptime để đánh dấu khối sẽ được thực thi trong lúc biên dịch, nhưng D không dùng từ khóa; việc có thực thi hay không được quyết định bởi nó có phải là biểu thức hằng hay không. Ví dụ int s = sum(3, 4); được thực thi lúc runtime, còn enum e = sum(3, 4); được thực thi tại thời điểm biên dịch. Nếu tránh các biến toàn cục không hằng, nhập/xuất và lời gọi hàm hệ thống như malloc(), khá nhiều hàm có thể chạy tại thời điểm biên dịch mà không cần sửa đổi. Nhờ cơ chế quản lý bộ nhớ tự động của D, việc cấp phát bộ nhớ cũng có thể thực hiện được

    • Một cách dùng tôi thích là tạo bảng tĩnh
      Trước đây phải viết một chương trình riêng để tạo bảng tĩnh, nhưng với việc thực thi hàm tại thời điểm biên dịch thì không cần làm vậy nữa. Giá trị khởi tạo của mảng tytab đến từ một lambda tính toán và trả về mảng. Ví dụ đầy đủ có ở https://github.com/dlang/dmd/blob/master/compiler/src/dmd/ba.... Một cách dùng phổ biến khác của CTFE là tạo DSL
    • Tôi không muốn bỏ lỡ cơ hội nói lời cảm ơn vì đã tạo ra D. Tuy vậy, tôi tò mò về giới hạn của thực thi tại thời điểm biên dịch
      Nếu máy chạy trình biên dịch và máy chạy chương trình khác nhau, trình biên dịch D đảm bảo tính đúng đắn như thế nào? Chẳng hạn tôi tò mò làm sao trình biên dịch biết int s = sum(100000, 1000000) sẽ có cùng giá trị trên mọi máy x86. Có thể có những khác biệt tinh vi giữa các thế hệ CPU, vậy làm sao đảm bảo phép tính thực hiện trên host cũng cho kết quả tương tự trên máy đích, hay chỉ giả định rằng nếu cùng kiến trúc thì host và target đủ giống nhau?
    • ImportC của D cũng có thể làm CTFE với mã C
      Với int sum(int a, int b) { return a + b; }, có thể dùng như _Static_assert(sum(3, 4) == 7, "look ma, check at compile time!");. Tôi không hiểu vì sao chuẩn C không bổ sung điều này, và nó hoạt động tốt
    • Công bằng mà nói, Zig cũng có thể gọi cùng một hàm trong cả ngữ cảnh runtime lẫn ngữ cảnh comptime
      square(2) là lời gọi runtime, còn comptime square(3) là lời gọi tại thời điểm biên dịch. Lời gọi comptime sẽ báo lỗi biên dịch nếu có thứ gì đó không tương thích với thời điểm biên dịch, và tôi xem đây là một tính năng quan trọng. Vì nó là tín hiệu cảnh báo khi đoạn mã vốn được kỳ vọng chạy tại thời điểm biên dịch vô tình bị đẩy sang đánh giá runtime do thay đổi đối số đầu vào
  • Zig trông thú vị, nhưng tôi ước nó có operator overloading
    Phần lớn lập luận phản đối operator overloading không thật sự thuyết phục. Lập luận rằng không biết bên trong thực sự xảy ra chuyện gì là chưa đủ, vì bạn cũng có thể viết một hàm tên add nhưng lại thực hiện phép nhân. iostreams hay boost::spirit của C++ thường được nêu như ví dụ lạm dụng, nhưng ở các ngôn ngữ khác có operator overloading thì không thấy điều đó nhiều, nên có vẻ giống vấn đề đặc thù của C++ hơn

    • Cách giải của OCaml có vẻ rất hợp với mục đích sử dụng của Zig
      Trong OCaml, bạn có thể định nghĩa lại toán tử, nhưng chỉ trong ngữ cảnh của một module khác. Ví dụ nếu định nghĩa lại + trong module Vec3, bạn có thể viết Vec3.(a + b + c + d) hoặc let open Vec3 in a + b + c + d. Khi đọc mã nguồn sẽ không có câu hỏi “dấu + này đến từ đâu?”, và nó tốt hơn nhiều so với a.add(b).add(c).add(d). Tuy nhiên Zig dường như đã dần ổn định, nên có vẻ khó đưa vào một thay đổi lớn để giải quyết vấn đề này
    • Có thể bạn không biết có bao nhiêu phép màu chạy phía sau các hàm __ của Python và PHP
      Cách tiếp cận của Zig rất mới mẻ, và khả năng lần theo mã quan trọng hơn vài giây tốn thêm để gõ thêm vài ký tự
    • Tôi nghĩ khá nhiều lý do ghét operator overloading liên quan đến hiệu năng runtime ngoài dự đoán
      Cách giải lý tưởng là ngôn ngữ cung cấp toán tử do người dùng định nghĩa nhưng thể hiện rõ đó là overloading. Ví dụ dạng tiền tố/hậu tố như let c = a |+| b thì người đọc mã biết phép toán |+| thực ra là một lời gọi hàm. Nó vẫn có thể bị lạm dụng, nhưng có thể giảm bớt một trong những mối lo chính
    • Trong C++, tôi chỉ dùng operator overloading cho toán vector/ma trận, và trong trường hợp đó nó thực sự hữu ích
      Nếu ngôn ngữ trực tiếp cung cấp cú pháp toán vector như các shading language thì có lẽ ổn. Zig ít nhất có kiểu @Vector() hơi giống Vector Extension của Clang, nhưng tiếc là không phải Extended Vector Extension. Xem https://ziglang.org/documentation/master/#Vectorhttps://clang.llvm.org/docs/LanguageExtensions.html#vectors-...
    • Có lẽ các toán tử đại số tuyến tính cơ bản cho mảng số nên được tích hợp thẳng vào ngôn ngữ thay vì dùng overloading
      Tôi không rõ đã có đề xuất như vậy chưa
  • fieldNames trông rất giống fieldPairs của Nim, và là một cấu trúc cực kỳ tiện lợi
    Nó khiến việc serialization hiệu quả trở nên rất dễ dàng, và gần đây tôi đã dùng fieldPairs để triển khai kiểm tra thread-safety tại thời điểm biên dịch cho một kiểu trong khoảng 20 dòng. Tôi nghĩ những tính năng như thế nên trở thành tính năng chuẩn của ngôn ngữ lập trình. Đây là một trong những điểm tôi tiếc nhất ở Rust, và lần cuối tôi thử thì nó vẫn bị giới hạn trong macro không có kiểu. Quá nhiều ràng buộc. Bạn chỉ còn cách hy vọng struct trong crate đã triển khai serde, và cũng không thể tự tạo struct có cùng các field theo cách lập trình được

    • Từng có thảo luận về reflection tại thời điểm biên dịch, và có lẽ tính năng như thế này cũng có thể được đưa vào, nhưng dường như nó đã biến mất cùng với các drama liên quan
      Khá đáng tiếc. Những thứ như serde sẽ dễ triển khai hơn rất nhiều nếu có reflection tại thời điểm biên dịch
    • Nếu có reflection tại thời điểm biên dịch, có thể xây dựng các framework như ORM hoặc web framework
      Thỏa hiệp duy nhất là phải đưa các thư viện đó vào dưới dạng mã nguồn
  • Sau khi từng viết một thư viện parser C tương đối hoàn thiện, tôi không chắc lắm về lập luận rằng bản thân ngôn ngữ nhất thiết phải có metaprogramming
    Nếu muốn sinh struct, serialization, property, instrumentation, v.v. thì chỉ cần viết một chương trình C bình thường xử lý file nguồn rồi xuất ra file nguồn, và cho chạy trước trong build script là được. Tôi tò mò những metaprogram như vậy được debug và test thế nào. Cái của tôi là chương trình C bình thường nên dùng nguyên debugger và công cụ như các mã khác

    • Đó chính là việc mà mọi người không muốn làm
    • C# — chính xác hơn là Roslyn/dotnet — cung cấp việc này khá tốt
      Bản thân compiler được viết bằng cùng ngôn ngữ, nên có thể gắn plugin truy cập AST ở chế độ chỉ đọc và xuất mã nguồn C#. Việc debug cần thêm chút công sức để tạo test framework tốt, nhưng sau đó có thể chạy compiler đã gắn plugin trong framework unit test tiêu chuẩn và xem bằng interactive debugger
    • Đây cũng là cách tiếp cận mà Ryan Fleury và những người khác ủng hộ, và là một cách đủ tốt
      “Thực thi tùy ý tại thời điểm biên dịch trong C: cl /nologo /Zi metaprogram.c && metaprogram.exe, cl /nologo /Zi program.c. Mã chạy lúc biên dịch chạy ở tốc độ native, có thể debug, hoàn toàn thủ tục và tùy ý. Compiler không cần phải chạy mã giúp bạn.” https://x.com/ryanjfleury/status/1875824288487571873
    • Tôi không rõ về Zig, nhưng sức mạnh của Lisp nằm ở S-expression, nói cách khác là thao tác trên AST
      Muốn làm như vậy trong C thì phải viết một parser C hoàn chỉnh cho chương trình C xử lý file nguồn
    • Trước đây tôi từng làm việc tương tự trong Python với numba JIT
      Tôi viết mã Python để sinh mã Python, rồi cho biên dịch lại. Nó là một mớ hỗn độn mong manh và kinh khủng, và việc phải làm những thứ như vậy là một trong những lý do lớn khiến tôi rời Python. Nó giống như hỏi rằng chẳng phải chỉ cần truyền mọi tham số hàm dưới dạng chuỗi là được sao. Có người dùng mã dựa trên chuỗi, nhưng việc đó nên hiếm, và ngôn ngữ nên cung cấp cách tránh nó
  • Một pattern thú vị là có thể sinh struct tại thời điểm biên dịch
    Tôi đã thử một thí nghiệm: tạo file JSON trong PyTorch, đọc bằng @embedFile của Zig, rồi sinh các struct có một phương thức run nhất định để triển khai mạng nơ-ron. Về lý thuyết, điều này cho phép compiler trực tiếp tối ưu hóa mạng nơ-ron. Tôi vẫn chưa chứng minh được lợi ích lớn, nhưng còn có điểm là toàn bộ network nằm trên stack nên không có cấp phát động. Tôi không chắc điều đó có tốt hay không

    • Tôi từng làm việc kiểu này bằng cách viết code generator bằng Python thay vì dùng comptime
      Tôi không chắc Zig comptime có đặc biệt nhanh hay không, và cũng không muốn mỗi lần đều chạy JSON parser để sinh struct
    • Tôi tò mò nó ảnh hưởng thế nào đến thời gian biên dịch
  • Dù thú vị, trong trường hợp này tôi lại đồng ý với kiểu bình luận “D đã làm trước rồi”

    • Đúng vậy, và nó được xử lý hygienic, cũng không phải kiểu preprocessor
  • Nếu bạn thấy comptime của Zig đáng kinh ngạc, thì Nim, với đánh giá mã tại thời điểm biên dịch và hệ thống macro AST đầy đủ, cũng rất đáng xem

    • Nim là một ngôn ngữ thú vị, nhưng tôi không nghĩ mình sẽ cân nhắc nó cho công việc “nghiêm túc”
      Nó có các vấn đề hệ sinh thái giống những ngôn ngữ ngách khác, cộng thêm các yếu tố như maintainer gây chia rẽ, những contributor cốt lõi dường như không gắn bó lâu, và nguồn tài trợ chính từ một công ty tiền mã hóa. Nếu là 10 năm trước thì những điều này có lẽ không làm tôi bận tâm, nhưng giờ thì khác
    • Zig có lợi thế là không có exception
      Nim có vẻ cũng đang cố thoát khỏi exception, nhưng exception làm “nhiễm” hàm và khiến bạn phải để tâm ngay cả khi không dùng hàm ném exception[1]. Đời quá ngắn để xử lý luồng điều khiển vô hình. [1]: https://github.com/status-im/nim-stew
  • Nhìn chung Zig là một ngôn ngữ khá tốt và làm được những việc cần làm
    Việc không đi chệch khỏi mục tiêu là rất quan trọng, nên tôi không thích lắm việc một số ngôn ngữ bị dùng ở khắp nơi chỉ vì chúng đang được dùng

  • Ước gì có thứ gì đó kết hợp năng lực metaprogramming của Zig với hệ sinh thái khổng lồ, cộng đồng và độ an toàn của Rust
    Nếu chỉ xét thiết kế ngôn ngữ thì tôi thích Zig hơn Rust rất nhiều, nhưng với tư cách một lập trình viên nghiệp dư vụng về, ít nhất hiện tại tôi không tự tin mình có thể viết thứ gì thực sự hữu ích hoặc đáng tin cậy bằng Zig

    • Là người quen với Rust, tôi đã thử Zig một chút rồi nhanh chóng bỏ cuộc
      Vì compiler không giúp tôi phát hiện những vấn đề đó tại thời điểm biên dịch. Tôi biết Zig không hứa hẹn điều đó, nhưng với tôi đây là nhược điểm quyết định, nên Zig có vẻ không phải ngôn ngữ phù hợp. Ngược lại, tôi thích khái niệm comptime hơn macro của Rust
    • Tôi mong cộng đồng Rust tránh xa Zig. Nói đùa thôi, nhưng phần lớn là thật