- Trong Zig 0.15, giao diện IO mới (
std.Io.Reader, std.Io.Writer) đã được giới thiệu
- Mục tiêu là cải thiện độ phức tạp và các vấn đề hiệu năng của cách làm IO cũ, nhưng lại phát sinh sự bối rối trong cách sử dụng thực tế
- Liên quan đến việc dùng
tls.Client và buffer, cách truyền tham số thiếu nhất quán càng làm tăng sự khó hiểu
- Ngay cả khi triển khai ví dụ sử dụng cơ bản cũng có nhiều yêu cầu phức tạp như chỉ định kích thước buffer, các trường tùy chọn
- Do thiếu tài liệu chính thức, ví dụ mã nguồn và hàm tiện ích, nên không trực quan với người mới bắt đầu
Giao diện IO mới được đưa vào Zig 0.15 và bối cảnh của nó
- Trong Zig 0.15, các kiểu IO mới là
std.Io.Reader và std.Io.Writer đã được đưa vào
- Giao diện IO trước đây gây ra độ phức tạp do vấn đề hiệu năng, trộn lẫn kiểu và lạm dụng
anytype
- Mục tiêu chính của cấu trúc IO mới là phân tách kiểu rõ ràng giữa các giao diện và cải thiện hiệu năng
Các vấn đề thực tế khi dùng tls.Client và giao diện IO
- Trong quá trình cập nhật thư viện smtp cũ, sự nhầm lẫn xuất hiện ở cách dùng hàm
tls.Client.init
- Theo tài liệu, hàm init nhận con trỏ tới Reader và Writer cùng một tập tùy chọn làm đối số
net.Stream của Zig trả về Stream.Reader/Stream.Writer thông qua các phương thức reader() và writer() tương ứng
- Tuy nhiên
Stream.Reader/Stream.Writer và std.Io.Reader/std.Io.Writer không phải chính xác là cùng một kiểu, nên cần chuyển đổi
- Với Reader phải gọi phương thức
interface(), còn Writer phải dùng trường &interface, nên thiếu tính nhất quán
Vấn đề cấu hình buffer và các trường tùy chọn
stream.writer, stream.reader mỗi bên đều nhận buffer làm đối số
- Buffer được nhấn mạnh là yếu tố bắt buộc trong giao diện IO mới
- Khi gọi
tls.Client.init, bắt buộc phải có bốn trường tùy chọn như ca_bundle, host, write_buffer, read_buffer
- Quy tắc tách biệt giữa giá trị truyền qua tham số tùy chọn và giá trị truyền trực tiếp qua đối số tạo cảm giác không rõ ràng
var tls_client = try std.crypto.tls.Client.init(
reader.interface(),
&writer.interface,
.{
.ca = .{.bundle = bundle},
.host = .{ .explicit = "www.openmymind.net" } ,
.read_buffer = &read_buf2,
.write_buffer = &write_buf2,
},
)
- Trên thực tế, nếu không truyền đúng con trỏ buffer thì chương trình có thể không hoạt động đúng, bị treo hoặc crash, cùng nhiều vấn đề khác
Vấn đề về tính trực quan khi dùng Reader
- Dù trường
reader của tls.Client bản thân là một "luồng đã giải mã", nhưng trên thực tế std.Io.Reader lại không có phương thức read thông thường
- Thay vào đó chỉ có các phương thức kém trực quan hơn như
peek, takeByteSigned, readSliceShort
- API gần nhất với cách sử dụng quen thuộc là đọc dữ liệu vào buffer thông qua phương thức
stream
var buf: [1024]u8 = undefined;
var w: std.Io.Writer = .fixed(&buf);
const n = try tls_client.reader.stream(&w, .limited(buf.len));
Ví dụ mã đầy đủ và các vấn đề thực chiến
- Ngay cả khi cố tạo một ví dụ tối thiểu chạy được đầy đủ, vẫn có rất nhiều điểm phải chú ý như tùy chọn, kích thước buffer, chuyển đổi kiểu
- Thiếu test/tài liệu/ví dụ làm tăng độ khó học và rào cản tiếp cận
- Nếu chưa hiểu rõ tính nhất quán trong ngôn ngữ Zig hoặc thiết kế nền tảng của nó, sẽ có nhiều điểm tạo cảm giác kỳ lạ
- Ngay trong thư viện chuẩn, cách làm này cũng chưa được dùng nhiều nên thiếu tài liệu tham khảo thực tế
Kinh nghiệm và kết luận
- Quá trình migration bản thân đã không hề dễ, do các thay đổi như đổi tên
std.fmt.printInt, thay đổi thiết kế API, v.v.
- Đã liên tục gặp nhiều khó khăn như cách
reader.interface(), &writer.interface, cách truyền tùy chọn và việc cần nhiều buffer
- Với người chưa quen các giao thức mạng/bảo mật như TLS, việc nắm bắt các yêu cầu này lại càng khó hơn
- Tổng thể mà nói, so với trước đây vẫn còn nhiều điểm chưa hoàn thiện về độ rõ ràng, tài liệu hóa và tính tiện dụng
1 bình luận
Ý kiến trên Hacker News
Xin nói rõ là tôi là tác giả. Cuối cùng tôi đã làm cho nó chạy đúng. Cả encrypted writer lẫn stream writer đều cần quá trình flush, đồng thời phía đọc cũng có vấn đề. Streaming thì có hoạt động, nhưng vì
Writer.Fixedkhông triển khaisendFile, nên ở lần đọc đầu tiên nó luôn trả về 0. Sau lần gọi đầu, nội bộ chuyển từ chế độ streaming sang chế độ đọc, rồi đột nhiên mọi thứ bắt đầu hoạt động (liên kết mã liên quan: Zig File.zig #L1318). Hiện tôi đang cố bật lại tính năng nén cho thư viện websocketLàm tôi nhớ đến meme YouTube "Đừng quên flush" (video YouTube)
Không biết nguyên tắc ít gây ngạc nhiên nhất (principle of least surprise) đã đi đâu mất rồi
Thật đáng kinh ngạc khi từ giao diện trước đó lại đi đến tình trạng hiện tại. Mức độ “ngạc nhiên” là rất lớn
Tôi không phải PM của Zig, nhưng giải pháp đầu tiên hiển nhiên cho vấn đề mà OP gặp phải là tài liệu tốt hơn và nhiều ví dụ sử dụng hơn (nhiều bao nhiêu cũng không sao). Đây cũng có thể là dịp tốt để, trong lúc làm việc đó, nhìn lại xem có đang bắt người dùng làm quá nhiều việc hay không. Nếu mục tiêu theo đuổi là hiệu năng tuyệt đối hoặc tránh đưa vào các abstraction gây suy giảm hiệu năng, thì có vẻ mục tiêu đó đã đạt được, nhưng DX (trải nghiệm nhà phát triển) thì như bay vào vũ trụ
Có vẻ bạn chưa hiểu rõ văn hóa cộng đồng Zig. Chỉ cần phàn nàn về việc thiếu tài liệu là phải sẵn sàng nhận một loạt bình luận kiểu "tự đọc code stdlib đi". Phần lớn API đều khó dùng như trong bài này, và ngay cả các tác vụ cơ bản như HTTP hay hệ thống tệp cũng thực sự khó nếu bạn chưa quen. Vì thế chỉ những người thật sự giỏi mới trụ lại được
Viết tài liệu tốn chi phí và thời gian. Khoảng thời gian đó có thể dùng để cải thiện những phần khác của Zig. Nếu là code đang trong quá trình hoàn thiện, trì hoãn tài liệu cho đến khi nó thật sự ổn định cũng là một lựa chọn hợp lý. Tất nhiên tài liệu hóa là tốt, nhưng khi phải ưu tiên giữa tính năng mới, sửa lỗi quan trọng hoặc làm tài liệu, thì không phải lúc nào cũng có thể có tất cả
Có vẻ zig quá tập trung vào việc chỉ ra không nên làm gì. Tôi mong nó phát triển theo hướng tập hợp, sắp xếp và chỉ dẫn rõ ràng nhiều cách làm cùng các ví dụ sử dụng. Việc thiếu tài liệu cho giao diện này là một ví dụ tiêu biểu
Viết tài liệu hay ví dụ tốt cần rất nhiều công sức. Nhìn vào mức độ thay đổi đang diễn ra trong zig hiện nay thì dù có viết tài liệu trước khi mọi thứ thật sự ổn định, nó cũng nhanh chóng trở nên vô dụng
Tôi không phải nhà phát triển Zig, nhưng tôi nghĩ một trong những lý do tài liệu của Zig quá sơ sài là vì ngôn ngữ này còn trẻ và vẫn đang tiếp tục tiến hóa. Tôi hiểu việc khó bỏ thời gian và năng lượng vào thứ mà bạn biết là sớm muộn gì cũng sẽ sai trong tương lai
Bản thân ngôn ngữ Zig thì thật sự ổn, nhưng thư viện chuẩn vẫn còn rất dang dở, liên tục thay đổi, thiếu sót nhiều, và có phần thì quá trừu tượng còn phần khác lại quá low-level. Hiện tại tôi nghĩ dùng trực tiếp OS API còn tốt hơn dùng thư viện chuẩn. Nếu không sẵn sàng làm beta tester thì tôi khuyên nên tránh thư viện chuẩn
Thực tế tôi cũng chủ yếu dùng OS API khi làm việc với zig.
cImportshoạt động tốt nên cả khi ngại tự tạo định nghĩa zig cũng vẫn dễ dùngTheo tôi, Zig có xu hướng cố làm quá nhiều thứ cùng lúc, đến mức còn chưa đạt nổi ngưỡng chất lượng tối thiểu theo quan điểm của tôi. Cách nó buộc người dùng chấp nhận các thay đổi đột ngột và những thử nghiệm cho thấy chỉ có thể dựa vào việc đủ nhiều người đầu tư để tin vào ảo tưởng rằng "trước 1.0 thì hỏng cũng không sao, sau này sẽ tốt lên" (kết luận: có lẽ ngày đó sẽ không bao giờ đến). Tôi không nghĩ việc bắt người khác gánh các thử nghiệm của mình là điều nên làm. Dù có thông báo trước là không ổn định, dù có nói là đừng phụ thuộc vào nó, thì với người đang bị rug pull (mọi thứ đột ngột thay đổi hoàn toàn), đó vẫn là vấn đề. Tôi không rõ zig rốt cuộc là gì. Matklad gọi nó là machine level language (phỏng vấn liên quan: lobste.rs - phỏng vấn Matklad), còn trang chính thức lại nói đó là ngôn ngữ general-purpose robust, optimal, reusable. Hai điều này mâu thuẫn với nhau. Và có rất nhiều bài toán không hề cần quản lý bộ nhớ thủ công, nên zig không bao giờ là ngôn ngữ đa dụng thực sự. Rốt cuộc toàn bộ sự hỗn loạn này lộ ra ở tính bất ổn và thư viện chuẩn phình to của zig. Tự nhận là đơn giản và đa dụng nhưng lại có thư viện lớn đến mức này là mâu thuẫn. Async cũng được hứa hẹn như thể là lời giải vạn năng, dù đó không phải tính năng có thể được triển khai hiệu quả một cách phổ quát trên mọi nền tảng. Trước đây họ còn quảng bá rằng đã giải quyết được vấn đề function coloring, nhưng nỗ lực đó giờ đã bị bỏ. Lập luận rằng hãy lại tin là lần này họ làm được nghe thật kỳ lạ. Thực ra để triển khai compiler trên mọi nền tảng, chỉ cần các lệnh assembly cơ bản để chạy được là đủ, còn luajit thì thậm chí triển khai parser hoàn toàn bằng assembly thuần và vẫn chạy tốt ở khắp nơi. Tôi lập trình chủ yếu bằng lua và gần như chưa bao giờ gặp bug trong interpreter. Tôi cũng không nghĩ ra được vấn đề nào zig giải quyết tốt hơn luajit. Nếu có thứ gì chỉ zig mới giải quyết được, thì cứ embed phần đó vào code lua rồi nối bằng FFI là được. Phần lớn code không thực sự cần mức tối ưu low-level đến vậy. Mang zig vào chỉ khiến mọi thứ đau đầu hơn. Gần đây kỳ vọng phóng đại về zig đã đạt đến mức chênh lệch với thực tế kiểu như AI. Muốn tin vào zig thì phải tin vào niềm hy vọng hão huyền rằng một ngày nào đó nó sẽ có những năng lực mà hiện giờ nó không có. Trên thực tế cũng chẳng có kế hoạch thực thi nào cả, chỉ kiểu "đợi thêm chút nữa thôi"
Tôi không hiểu tại sao thư viện hay giao diện lại yêu cầu cấp phát bộ đệm bằng kiểu của tôi. Nếu tôi là người parse thì đâu cần thư viện, còn nếu dùng thư viện thì điều đó còn có thể phá vỡ interchange. Giao diện đặc trưng của Go là vì một số interface mở rộng
writerinterface (xemhijackerinterface), hoặc vì đối tượng request được tái sử dụng rất đa dạng qua nhiều middleware. Tóm lại, request thì không cần phải mở rộng, nhưng response thì có thể biến đổi thành nhiều dạng như websocket, wrapper TCP, v.v.Việc thư viện yêu cầu cấp phát buffer bên ngoài không khiến tôi thấy lạ. Nó mang lại tính linh hoạt để đổi lấy nhiều thao tác thủ công hơn. Ví dụ, nếu bạn đã có sẵn buffer pool thì có thể muốn tái sử dụng nó. Nếu kiểu dữ liệu tự cấp phát nội bộ thì điều đó là không thể. Hoặc trong môi trường phải cấp phát trước toàn bộ tài nguyên thì về sau sẽ không thể cấp phát thêm. Điểm bất lợi là chỉ khoảng 10% người dùng thực sự cần mức linh hoạt này, còn 90% chỉ muốn cấp phát buffer rồi truyền vào, nhưng tất cả mọi người đều phải làm việc phức tạp hơn. Cách tốt nhất là vừa cho mức linh hoạt cao, vừa xử lý dễ dàng trong trường hợp đơn giản. Ví dụ, cho phép truyền buffer độ dài 0 (hoặc
nullcủa Zig) để kiểu tự cấp phát, và đồng thời cung cấp thêm constructor tạo đơn giản không cần buffer. Tất nhiên kiểu này hoàn toàn là cực hình cho khâu tài liệu hóaĐiều này giống với sự khác biệt về quy ước mà mỗi ngôn ngữ lựa chọn (kiểu radian/độ). Bất kỳ IO nào cũng có thể chuyển đổi tự do. Ở bên này thì gọi là mock, ở ngôn ngữ khác có thể đặt là
unsafeFoo. Andrew Kelley đã tự mình tái phát hiện các pattern mà cộng đồng Haskell bàn suốt 30 năm trong live stream. Vậy nên tương lai là Zig. Anh ấy là người ngộ ra trướcÝ nghĩa của external buffer là hàm có thể bỏ qua việc cấp phát buffer
Tôi sẽ không nâng side project zig của mình lên 0.15.x. Tôi tôn trọng việc Andrew chọn phát hành và trao Io mới cho những người dùng sớm, nhưng mới chỉ vài ngày kể từ đợt thay đổi lớn với readers/writers. Với những người làm thư viện chuẩn thì đây là điều tốt, nhưng với người dùng zig như một thú vui như tôi thì có vẻ khôn ngoan hơn là chờ đến khi nó ổn định sau 0.16.0
Nếu tên ngôn ngữ là Zig, đôi khi chẳng phải cũng nên Zag một chút sao, nghĩ vậy thấy buồn cười
Loris Cro, thành viên nòng cốt của Zig, cũng đã nói trong một cuộc phỏng vấn gần đây rằng anh ấy sẽ trì hoãn cập nhật zig cho dự án của mình cho đến khi dư chấn từ thay đổi IO lắng xuống. Tuy nhiên triển vọng về sau là tích cực. Cả Andrew lẫn Loris đều xem đây có lẽ là thay đổi lớn cuối cùng, nên có thể 1.0 sẽ không còn xa. Tác động của stack-less coroutine được (tái) đưa vào hiện là biến số lớn nhất
Sau khi đọc bài viết về giao diện IO mới, tôi đã chọn tránh zig. Cảm giác như trực giác của tôi đã đúng. Dù lý do có khác, kết quả cuối cùng vẫn là mức độ phức tạp giống sự dài dòng của thời kỳ trước C++11. Mẫu hình quen thuộc lại lặp lại: một ngôn ngữ mới cố thay thế cái cũ rồi cuối cùng lại trở nên phức tạp ngang ngửa
Tôi nghĩ nhận xét của OP rằng việc phải gọi method
interface()để chuyểnStream.Readerthànhstd.Io.Reader, trong khi để lấystd.io.WritertừStream.Writerlại cần địa chỉ của field&interface, là một sự thiếu nhất quán mà cộng đồng Go hẳn sẽ bác bỏ ngay từ đầu. Go có xu hướng chỉ quyết định ngay cả những thay đổi nhỏ sau khi đã phân tích cực kỳ sâu. Ví dụ issue Go mà tôi thích nhất: Go github issue #45624. Thảo luận 4 năm rồi mới chốt. Có thể chậm, nhưng họ xem xét kỹ tính nhất quán, suy nghĩ thiết kế và cả việc dùng trong code thực tế. Chậm, nhưng tôi nghĩ đó là tốc độ cần thiết. Và những quyết định như vậy rốt cuộc có chất lượng rất caoRust cũng vậy. Có rất nhiều tính năng hữu ích chỉ có trên nightly rust mà chưa có trên stable (ví dụ: generator). Khá sốt ruột, nhưng các tính năng được đưa vào stable đều được kiểm chứng cực kỳ kỹ. Tôi là người thiếu kiên nhẫn, nhưng tôi nghĩ cách tiếp cận của đội Rust là đúng đắn
Trước Go 1.0 thì mọi thứ không chậm như vậy. Các thay đổi lớn, dù không hẳn mang tính nền tảng hơn, vẫn diễn ra thường xuyên (bỏ dấu chấm phẩy, thay đổi kiểu lỗi, v.v.), và còn có cả công cụ tự động chuyển đổi. Từ 1.0 trở đi họ hứa về tính ổn định nên mới thành cách làm như hiện nay
Zig là ngôn ngữ đầu tiên tôi nghĩ tới khi làm việc low-level. Việc có thể dùng Zig như cross-compiler cho C/C++ là cực kỳ tuyệt
Phần lớn vấn đề ở đây có vẻ đơn giản là xuất phát từ việc thiếu tài liệu hoặc tài liệu kém
output.write("hello")trở nên dễ dùng, nhưng thực tế lại gây bối rối vì thiếu giải thích cách sử dụng. Tôi cũng nghi ngờ liệu có cần phải biểu diễn một hệ thống kiểu phức tạp như vậy ngay trong thư viện chuẩn hay không. Toàn bộ Zig vốn gồm những method rõ ràng, gọn và dễ đọc, còn hệ thống IO mới thì đi rất xa khỏi điều đó và thiếu trực giác(Hệ thống mới của zig) có vấn đề ở chỗ nó trộn một khái niệm vốn chỉ dùng để phân chia ranh giới thực thi vào toàn bộ runtime engine, trong khi lại không chỉ ra rõ phải nối hai phía đó với nhau như thế nào