Đánh dấu mã WIP trong Rust bằng cảnh báo
(blog.dureuill.net)- Trong quá trình phát triển Rust, nếu xử lý ngay mọi đường lỗi và vấn đề ownership, luồng triển khai rất dễ bị gián đoạn; vì vậy chiến lược tạo happy path trước rồi theo dõi phần việc còn lại bằng cảnh báo là hữu ích
- Chỉ với Rust chuẩn, cũng có thể kết hợp
unwrap,clone,todo!, cảnh báo mặc định của compiler và mẹo dùng chú thích tài liệu///để đánh dấu mã chưa hoàn thiện - Các cách hiện có có hạn chế:
unwrapvàclonekhông phát cảnh báo,todo!cũng dựa vào cảnh báo gián tiếp, còn mẹo///tùy vị trí có thể dẫn đến lỗi biên dịch hoặc tạo tài liệu ngoài ý muốn - Crate
wipdùngwip!,unwrap_wip,clone_wip,fixme!để luôn hiển thị dưới dạng cảnh báo các triển khai tạm thời,unwrap/clonetạm thời và ghi chú cần sửa sau - Meilisearch dùng
-D warningtrong CI để biến cảnh báo thành lỗi nhằm ngăn mã WIP lọt vào, và dependencywipthêm trong lúc phát triển sẽ được gỡ bỏ trong quá trình dọn lịch sử trước khi review
Vì sao cần cảnh báo khi phát triển Rust
- Rust là ngôn ngữ coi trọng tính đúng đắn, nhưng nếu xử lý ngay mọi đường lỗi trong lúc phát triển, tốc độ triển khai và sự tập trung có thể giảm
- Khi
rustcchặn biên dịch vì các lý do như trả vềResult, xử lý lỗi, triển khai edge case, thường có hai lựa chọn- Triển khai ngay phần xử lý lỗi đầy đủ rồi refactor nhiều lần cho đến trước khi PR sẵn sàng
- Để lại một dấu hiệu kiểu
TODO later, hạ vấn đề xuống mức cảnh báo và tiếp tục công việc hiện tại
- Cảnh báo không chặn phát triển, nhưng vẫn là tín hiệu cho biết cần dọn dẹp trước khi merge PR
- Meilisearch chạy compiler trong CI với cờ
-D warningđể biến mọi cảnh báo thành lỗi, tránh việc mã WIP vô tình được phát hành - Code review là thực hành cơ bản để ngăn lỗi và chia sẻ tri thức trong nhóm, nhưng nếu chỉ dựa vào trí nhớ và sự chú ý của con người thì cuối cùng vẫn có thể mắc lỗi, nên cần lan can bảo vệ bổ sung
Cách trì hoãn xử lý phần chưa hoàn thiện bằng Rust chuẩn
-
Trì hoãn xử lý lỗi bằng
unwrap- Khi dùng
unwrapvớiResulthoặcOption, bạn có thể lấy giá trị bên trong để tiếp tục triển khai hiện tại, đổi lại chương trình sẽ panic ở đường lỗi - Sau khi phần mã còn lại đã sẵn sàng, có thể chọn các
unwraptạm thời đã thêm vào để gỡ bỏ và thay bằng xử lý lỗi phù hợp - Cách này dựa trên tiền đề rằng dù Rust cung cấp cấu trúc để xử lý lỗi đúng cách, bạn không nhất thiết phải hoàn tất việc đó ngay lập tức
- Khi dùng
-
Trì hoãn vấn đề ownership bằng
clone- Vấn đề ownership ít gặp hơn xử lý lỗi, nhưng việc duy trì ownership đúng có thể đòi hỏi thay đổi thiết kế
- Khi đó, thay vì di chuyển giá trị, có thể
cloneđể tạm thời tránh vấn đề lifetime - Về sau, có thể nhận diện theo kinh nghiệm các trường hợp có thể loại bỏ clone bằng cách truyền biến phù hợp lên stack cao hơn
- Tuy nhiên, việc sửa có thể kéo theo thay đổi cấu trúc mã, nên khó trì hoãn hơn so với xử lý lỗi
-
Giữ chỗ triển khai bằng
todo!()- Macro
todo!()trong thư viện chuẩn Rust biểu thị mã chưa hoàn thiện, hữu ích như placeholder để vượt qua phân tích kiểu trong lúc prototyping - Vì
todo!()diverge bằng panic, nó có thể được dùng như biểu thức giữ chỗ ở hầu hết vị trí kiểu - Khi đang định hình interface, bạn có thể dùng được định nghĩa hàm trong khi trì hoãn triển khai thực tế
todo!()gần với tiện ích WIP dành cho người viết mã và reviewer hơn là để thông báo cho người dùng chương trình rằng tính năng chưa được triển khai- Với mục đích thông báo cho người dùng rằng chức năng chưa được triển khai, có macro riêng là
unimplemented!()
- Macro
-
Cảnh báo compiler mặc định và mẹo chú thích tài liệu
- Các cảnh báo mặc định của Rust compiler cũng hữu ích để làm lộ trạng thái chưa hoàn thiện trong quá trình phát triển
- Binding không được sử dụng
mutkhông cần thiếtResultvàOptionkhông được sử dụng
- Nếu trong lúc phát triển bạn tạm thời loại bỏ các cảnh báo này bằng tiền tố
_hoặc mẹo khác, mã chưa hoàn thiện có nguy cơ lọt qua nguyên trạng - Có những edge case hoặc logic bị hỏng không được theo dõi bằng type system nên được để lại dưới dạng chú thích
- Nếu viết chú thích trông như chú thích tài liệu
///, Rust sẽ cảnh báo về chú thích tài liệu không có đối tượng được tài liệu hóa, nhờ đó có thể theo dõi vấn đề tại vị trí đó bằng cảnh báo
- Các cảnh báo mặc định của Rust compiler cũng hữu ích để làm lộ trạng thái chưa hoàn thiện trong quá trình phát triển
Giới hạn của các cách chuẩn
- Vấn đề lớn nhất là không phải kỹ thuật nào cũng tạo được cảnh báo ổn định gắn với vấn đề cần giải quyết
todo!()thường gây cảnh báo, nhưng nhiều khi cảnh báo đến từ nguyên nhân gián tiếp như tham số hàm không được dùng hoặc mutation cần thiết chưa diễn raunwrapvàclonehoàn toàn không phát cảnh báo, nên trước PR cuối cùng phải đọc lại để gỡ bỏunwrapcũng có thể được để lại trong production một cách hợp lệ, khiến mục đích WIP tạm thời và mục đích sử dụng thông thường bị trộn lẫn- Mẹo chú thích tài liệu
///gần giống một cách hack; ở vị trí biểu thức, chú thích tài liệu bị cấm nên có thể gây lỗi biên dịch - Nếu dùng
///ở vị trí thực sự được tạo tài liệu, cảnh báo mong đợi sẽ không xuất hiện và tài liệu không mong muốn có thể còn lại trong mã phát hành
Công cụ mà crate wip cung cấp
- Crate
wiplà bộ công cụ giúp chiến lược “cảnh báo để xử lý sau” thuận tiện hơn trong quá trình phát triển Rust - Có thể thêm như một crate thông thường
cargo add wip
- Trong các file cần dùng, có thể import prelude
use wip::prelude::*;
wipdựa vào deprecation warning của Rust, nên nếu editor có tính năng gạch ngang lời gọi hàm deprecated, tắt tính năng đó có thể đỡ gây khó chịu hơn-
wip::wip!- Macro
wip!hoạt động giốngtodo!, nhưng luôn phát cảnh báo khi được dùng - Có thể dùng ở những vị trí trước đây bạn dùng
todo!để đánh dấu mã chưa hoàn thiện - Ở phiên bản đầu, nó dùng tên
todo, nhưng khi một dependency cố shadow macro củastd, Rust không ưu tiên cách này nên gây vấn đề khi dùng prelude
- Macro
-
unwrap_wipvàclone_wipunwrap_wipvàclone_wipđược triển khai dưới dạng extension trait choResultvàOption- Khi nằm trong scope, chúng hoạt động như
unwrap,clonethông thường nhưng phát cảnh báo khi sử dụng - Chúng có thể đưa ra tín hiệu rõ ràng rằng bạn không có ý định để lại
unwraphayclonetrong mã cuối cùng
-
wip::fixme!- Macro
fixme!là biến thể non-panicking củawip!và không thay thế cho biểu thức - Có thể dùng như chú thích TODO, nhưng tạo cảnh báo ổn định mà không cần mẹo
/// - Thường được dùng để để lại ghi chú cần sửa sau
- Macro
Quy trình sử dụng và lưu ý
- Crate
wipđã được dùng trong một vài PR gần đây, bao gồm Meilisearch PR #6484, một trong các PR lớn - Quy trình thông thường là thêm dependency
wipvào crate trong workspace trong lúc phát triển, rồi gỡ bỏ trong quá trình viết lại lịch sử trước review wipgiúp tập trung vào happy path mà không quên những việc cần xử lý sau- Cũng có nhược điểm
- Nếu khi thêm
fixmekhông viết đủ ngữ cảnh, về sau sẽ khó sửa - Trong PR lớn, số lượng cảnh báo do
wiptạo ra có thể tăng lên và trở thành gánh nặng
- Nếu khi thêm
- API hiện tại có thể xem trong tài liệu
wiptrên docs.rs
1 bình luận
Ý kiến trên Lobste.rs
Nói thật thì khi nhìn vào cơ sở của
wip, tôi có cảm giác tác giả đang tái tạo lại Clippy vì chưa hiểu công cụ vốn nên được dùng thế nàoCác nhà phát triển toolchain từ trước đến nay vẫn phân biệt cái gì nên là cảnh báo của
rustcvà cái gì nên là lint của Clippy, cònwipthì có vẻ như đang làm lại tính năng sẵn có dưới tên mới để ép các lint vốn thuộccargo clippysang phíacargo checkvàcargo buildVí dụ, phần mô tả
wip!về cơ bản là đánh đổi việc thêm phụ thuộc mới và cú pháp không chuẩn để#![warn(clippy::todo)]xuất hiện cả bên ngoàicargo clippy, cònunwrap_wipcũng tương tự#![warn(clippy::unwrap_used)]Với tôi,
.unwrap()là thứ dùng cho lúc dựng prototype nên tôi bật#![warn(clippy::unwrap_used)], còn.expect("description of invariant")là thứ dùng cho production, nơi đáng ra có thể dùng.unwrap()thay vì.unwrap_wip(), nên tôi không bật#![warn(clippy::expect_used)]. Ví dụ:.expect("parse regex from const")Tôi không rõ lắm về
clone_wipvàfixme!, cũng chưa từng suy nghĩ đủ sâu để trả lời rằng các nhà phát triển dự định mọi người nên xử lý những trường hợp như vậy thế nàoTuy vậy, đúng là bài viết đã thiếu phần thảo luận về các linter sẵn có như Clippy. Việc này cũng liên quan đến cách dùng cá nhân: trước đây tôi từng chạy Clippy mỗi lần lưu, nhưng ở dự án của chúng tôi nó quá chậm, có khi mất vài phút giữa các lần lưu nên tôi dừng, giờ chỉ chạy trong CI và hook trước khi push
Nếu kéo được cảnh báo sang
rustcthì sẽ nhanh hơn nhiều, và có thể xem cảnh báo inline trong lúc lặp đi lặp lại, đây chính là điểm cốt lõi của workflow nàyCấu hình Clippy cũng khá phiền, ít nhất là lần cuối tôi kiểm tra. Không có cách nào để khai báo lint bằng code một lần cho toàn bộ crate trong workspace, nên với mỗi lint muốn cấm trong workspace, chúng tôi либо phải thêm
-Dtường minh vào CI, либо cứ phải thêm thuộc tính toàn cục vào mọi crate#![warn(clippy::todo)]cần có ở mọi crate, nên với chúng tôi nó không đem lại lợi ích lớn hơn việc chỉ thêm/bớt crate trong lúc phát triển. Tôi nghĩtodo!mặc định nên bịrustchoặc Clippy cảnh báo, và nếu điều đó xảy ra thì tôi đồng ý rằng tính hữu dụng củawipsẽ giảm điTôi cũng không đồng ý rằng
unwrap_wipchỉ là#![warn(clippy::unwrap_used)], theo hai nghĩa. Thứ nhất, về mặt ý định, việc chiaunwraplà phi-production cònexpectlà production thì phổ biến nhưng không phải phổ quát. Thứ hai, codebase hiện tại có khoảng 4600 chỗ unwrap, và đúng là có thể đổi hết sangexpect, nhưng như vậy sẽ tốn quá nhiều thời gian để nhét các thông điệp tạm khắp nơi hoặc tái dựng các điều kiện tiên quyết kiểu “mutex này chưa bị poison”, “tôi muốn truyền lỗi của thao tác này cho caller”fixme!rất hữu ích, và tôi muốn nó có mặt trong standard library hay nơi nào tương tự. Thực tế đây là tính năng tôi dùng nhiều nhất trong crate nàyBài viết không đề cập hết những chi tiết này, nhưng vì crate này là thư viện chuyên biệt chứ không phải chuẩn chung nên còn có các tính năng như
wip_iter. Nó cho phép dùng các macro nhưtodotrong những hàmimpl Traitở vị trí trả về của iterator, vàwip_futurecũng tương tự. Tôi đã khá nhiều lần phải gò ép kiểu chỉ vì suy luận kiểu trả về vàtodokhông hoạt động tốt cùng nhau. Cũng có người đề xuất để nó mặc định thành lỗi biên dịch ở chế độ release, và tôi định xem xét cùng các ý tưởng cải tiến khácNếu muốn nhìn cân bằng hơn về cái giá của phụ thuộc mới và cú pháp không chuẩn, thì đây là phụ thuộc một file chỉ dùng trong quá trình phát triển và thường được gỡ đi khi xong việc
Vấn đề của việc prototype bằng
unwraplà về sau khi đưa xử lý lỗi đúng nghĩa vào thì hình dạng của kiểu trả về có thể thay đổi, và nhất là nếu các hàm đó được dùng theo kiểu hàm chức năng thì sẽ gây hiệu ứng dây chuyềnTôi khuyên nên bắt đầu với
Result, dùng kiểu lỗi nhưBox<dyn Error>, chuỗi,anyhow, v.v. Về sau vẫn có thể đổi sang enum lỗi thật sự, nhưng việc refactor mã vốn trông như không thể thất bại sang kiểu trả vềResultsẽ khó hơnNgoài ra nếu dùng LLM thì nên cẩn thận. LLM là cỗ máy khớp mẫu, nên nó không phân biệt được giữa “cố tình làm tạm cho xong và sẽ sửa sau” với “dự án này nên xử lý lỗi theo cách này”. Theo thời gian nó có thể khuếch đại các quyết định kiểu “chỉ tạm thời thôi”, nên sẽ hữu ích nếu để lại một hai chú thích ở
mainnói rõ ở đâu/khi nào xử lý lỗi lỏng tay là chấp nhận đượcCách hiểu về
todo!()vàunimplemented!()của tôi không giống với tài liệu. Dù vậy, đoạn tiếp theo mà bài viết lược bỏ cũng phần nào cân bằng lại lập trường đóTài liệu nói rằng
todo!truyền đạt ý định sẽ triển khai sau này và có thông điệp là “not yet implemented”, cònunimplemented!không hứa hẹn điều đó và có thông điệp là “not implemented”Khi
todo!được thêm ở 1.40.0, nó đơn thuần chỉ là tên ngắn hơn, ngoài cái tên ra thì không khácunimplemented!Mãi đến 1.42.0 thì từ “yet” mới bị bỏ khỏi thông điệp của
unimplemented. Nếu tôi nhớ không nhầm thì việc gán ý nghĩa này cho hai macro cũng từng gây tranh cãi đôi chút, và cả việc phát hànhtodo!như dạng viết ngắn củaunimplemented!cũng vậyNgày nay
unimplemented!()thường được khuyến nghị cho các chỗ vì lý do kỹ thuật phải có thân hàm nhưng tuyệt đối không bao giờ được gọi tới, chẳng hạn như một số triển khai trait. Ví dụ trong tài liệu đúng là theo kiểu đóTôi tự hỏi liệu workflow thêm phụ thuộc
wipvào crate trong workspace khi phát triển rồi viết lại history để gỡ nó trước khi review có thể được làm mượt hơn bằng công cụ bổ sung nào khôngViệc phải viết lại lock file trong lúc chỉnh sửa giữa chừng của PR khiến tôi hơi ngại. Nếu trong thời gian đó cây phụ thuộc cũng thay đổi ở nơi khác thì có thể phát sinh xung đột không cần thiết. Có lẽ sẽ lạm dụng được
jj fix. Dù sao thì ý tưởng dự án rất hay, và có lẽ tôi sẽ dùng nó ở đâu đóMột macro để ghi chú mà không gây panic như
wip::fixme!thực sự là ý tưởng rất hay. Mẹo dùng doc comment cũng là một cách gọn gàng đáng nhớCó vài ý tưởng thú vị mà tôi hy vọng có thể dẫn tới thay đổi ở phía
rustc.todo!()thật sự nên có cảnh báo mặc địnhSẽ còn tốt hơn nếu lập trình viên có thể tắt các cảnh báo này khi đang ở chế độ hack nhanh. Quá nhiều cảnh báo sẽ gây quá tải, và thường người ta chạy
cargotrong một ô console cao 19 dòng ở cạnh dưới IDE. Trong lúc phát triển, cảnh báo dễ che mất lỗi thật sự và những cảnh báo quan trọng hơn. Chỉ cần luôn sắp lỗi xuống cuối cùng thôi cũng đã giúp giảm vấn đề nàyNgoài ra, tên
.unwrap_wip()và.clone_wip()còn dài hơn cả tên gốc nên hơi quá dài. Tôi đề xuất các tên như.du()và.dc()như viết tắt của “dev unwrap” và “dev clone”. Chúng sẽ dễ dùng hơn khi lập trình nhanhLiệu có thể tự động thêm
.clone()và.unwrap()ở những chỗ cần không? Ví dụ bằng một cờ hay tính năng như#[wip(autoclone,autounwrap)]. Chắc là không được đâu, nhưng vẫn có thể mơMỗi khi bắt đầu một dự án mới hay prototype, tôi đều thêm các dòng sau vào
main.rs:#![allow(dead_code)]#![allow(unused_variables)]#![allow(unreachable_code)]Nó giúp giảm rất nhiều nhiễu ở giai đoạn đầu