Đánh giá rằng Zig comptime rất xuất sắc
(scottredig.com)- 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: typevàanytypethay 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ườnga,b,ccủa structMyStructđược đặt cạnh nhau để so sánhinline 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,ccủastructđược trả về đều dùng kiểuT
- Ví dụ:
- 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, ...)vàquadratic(i64, ...)
- Ví dụ:
- 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đến100 - Với bội số của
3và5thì infizzbuzz, bội số của3thì infizz, bội số của5thì inbuzz, còn lại in số
- Lặp từ
- Khi chạy chính hàm
fizzBuzzđó bên trong khốicomptime, có thể tính sẵn toàn bộ đầu rastd.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]u8vớ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,typechỉ 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
- Các giá trị
- 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ằng1,2,3sumđược cập nhật theo0 → 1 → 3 → 6- Cuối cùng được đơn giản hóa đến dạng
std.debug.print(..., .{6})
- Vòng lặp
- Ví dụ cộng các trường của struct cũng có thể được đọc theo cách tương tự
inline forchạ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ànhmy_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ư
ifphụ 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ã
writeSumFndùngwriter.printđể xuất mã nguồn của hàmsumFields- 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 forvà@fieldđể tạo mã truy cập trường
- Lấy tên trường bằ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
@embedFilerồ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)
- Sinh văn bản phải nối chuỗi như
- 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.printsử 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
ArrayListlà một container generic tương đối đơn giản nhưng đầy đủ tính năng- Hàm
maincủ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
- Hàm định dạng mà
1 bình luận
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
Tuy vậy, trong những tình huống khác, khi thấy
comptimetrong 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ùngcomptime, 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.EnumArraysẽ tốt hơnVề 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àyTôi xem
comptimekiể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úccomptime; 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ữacomptimevà 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ì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ã
comptimeTừ 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ở raD đã 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ònenum 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 đượcTrướ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 DSLNế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?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ốtcomptimesquare(2)là lời gọi runtime, còncomptime square(3)là lời gọi tại thời điểm biên dịch. Lời gọicomptimesẽ 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àoZig 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
addnhưng lại thực hiện phép nhân.iostreamshayboost::spiritcủ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ơnTrong 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 moduleVec3, bạn có thể viếtVec3.(a + b + c + d)hoặclet 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ớia.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ủa Python và PHPCá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ự
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 |+| bthì 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ínhNế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/#Vector và https://clang.llvm.org/docs/LanguageExtensions.html#vectors-...Tôi không rõ đã có đề xuất như vậy chưa
fieldNamestrông rất giốngfieldPairscủa Nim, và là một cấu trúc cực kỳ tiện lợiNó 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 khaiserde, và cũng không thể tự tạo struct có cùng các field theo cách lập trình đượcKhá đáng tiếc. Những thứ như
serdesẽ dễ triển khai hơn rất nhiều nếu có reflection tại thời điểm biên dịchThỏ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
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
“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/1875824288487571873Muố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
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
@embedFilecủa Zig, rồi sinh các struct có một phương thứcrunnhấ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ôngcomptimeTôi không chắc Zig
comptimecó đặc biệt nhanh hay không, và cũng không muốn mỗi lần đều chạy JSON parser để sinh structDù 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”
Nếu bạn thấy
comptimecủ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 xemNó 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
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
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
comptimehơn macro của Rust