2 điểm bởi GN⁺ 2025-01-26 | 1 bình luận | Chia sẻ qua WhatsApp
  • Văn hóa phát triển lấy cài đặt package làm mặc định làm gia tăng dependency churn, kéo theo cập nhật, vá lỗi, kiểm toán và quản lý phụ thuộc bắc cầu, từ đó tạo ra chi phí ẩn cho năng suất
  • Tác động càng lớn ở những hệ sinh thái có đóng gói tốt như JavaScript và Rust; ví dụ một dự án Tokio mới kéo theo 28 crate, Rocket là 172 crate, còn MiniJinja CLI có 142 dependency
  • Ngay cả chức năng đã ổn định từ lâu như terminal_size cũng rơi vào nghịch lý phải gánh thêm crate và phát hành lặp lại vì thay đổi trong các thư viện trừu tượng hóa nền tảng
  • Với tính năng nhỏ, dùng ChatGPT hoặc Cursor để tạo triển khai không phụ thuộc đôi khi còn nhanh hơn việc tìm phụ thuộc và liên tục nâng cấp, đồng thời giảm gánh nặng biên dịch mã bên ngoài
  • Vẫn cần thư viện cho các mảng khó như HTTP, QUIC, đồ họa hay tokio, nhưng nên hoài nghi hơn với lựa chọn chấp nhận cả một đồ thị phụ thuộc lớn chỉ vì một hàm

Vòng chạy bộ bảo trì do phụ thuộc tăng lên tạo ra

  • Lập trình viên dễ dàng cài package vì năng suất, nhưng kết quả là bị cuốn vào dependency churn với cập nhật, vá lỗi, kiểm toán và quản lý phụ thuộc bắc cầu kéo dài không dứt
  • Vấn đề này càng nổi bật trong các hệ sinh thái có giải pháp đóng gói hoàn thiện, và JavaScript cùng Rust là những trường hợp bị ảnh hưởng mạnh nhất
  • Trường hợp của Rust:
    • Một dự án Tokio mới mang vào 28 crate
    • Một dự án Rocket mới tăng lên tới 172 crate
    • Bản thân MiniJinja có thể tồn tại chỉ với một phụ thuộc, nhưng biến thể CLI lại kéo theo 142 dependency
  • Vấn đề cốt lõi là để có một tính năng nhỏ, người ta phải build và quản lý lượng mã bên ngoài lớn hơn rất nhiều so với phần mã thực sự cần thiết

Nghịch lý của mã ổn định qua ví dụ terminal_size

  • terminal_size đúng như tên gọi, là crate dùng để xác định kích thước terminal
  • API nền tảng mà tính năng này dùng đã gần như ổn định từ buổi đầu của terminal máy tính, nhưng tùy hệ điều hành vẫn kéo theo 3~4 crate bổ sung
  • Tình huống phát sinh là để kiểm tra terminal đang là 80x25 hay 120x40, người ta phải biên dịch hàng nghìn hàm khác
  • Crate này đã có 26 lần phát hành, nhưng một triển khai tự viết được đưa vào dự án từ 10 năm trước cho cùng chức năng vẫn hoạt động mà không cần cập nhật
  • Lý do có nhiều bản phát hành không phải vì chức năng thay đổi, mà vì các thư viện trừu tượng hóa nền tảng bên dưới liên tục biến động
  • Trên UNIX có một ngoại lệ là cần phụ thuộc libc
    • Vì Rust không phơi bày các hằng số libc của nền tảng, và các hằng số đó cũng không được chuẩn hóa
    • Tuy vậy, libc là phụ thuộc phổ biến và nhẹ nên khó tránh

Bảo mật và văn hóa tái sử dụng mã làm phụ thuộc mạnh hơn

  • Cách tiếp cận kiểu “big supply chain” khiến việc sao chép một hàm vào mã nguồn hoặc tự dùng unsafe bị dè chừng, và tạo áp lực phải giao cho các lớp trừu tượng hóa nền tảng xử lý
  • Cũng có những công ty cung cấp công cụ để xử lý vấn đề phụ thuộc, đồng thời dưới danh nghĩa bảo mật khuyến khích duy trì và luôn cập nhật dependency
  • Tuy nhiên, chính nhiều dependency lại có thể là nguồn gốc chính của vấn đề bảo mật
  • Mục tiêu của mã nên là được viết sao cho đến một thời điểm nào đó đạt độ ổn định và không còn cần cập nhật nữa
  • Trong hệ sinh thái Rust, ngay cả dependency hoạt động ổn định nhưng bug tracker hơi kém sôi động cũng có thể bị RUSTSEC đánh giá thấp
  • Văn hóa review mã kiểu doanh nghiệp cũng ảnh hưởng tới mã nguồn mở
    • Kỹ sư mang vào một thư viện mới bóng bẩy có nhiều khả năng được tưởng thưởng hơn là bị trách phạt
    • Vì thế mới xuất hiện các công cụ như Dependabot, và dự án liên tục nhận PR cập nhật dependency
    • Bên trong doanh nghiệp, chỉ riêng vendoring, kiểm toán nội bộ và nâng cấp trên toàn công ty cũng đủ khiến các đội kỹ thuật luôn bận rộn

Tính năng nhỏ có thể tự làm

  • Con đường đơn giản hơn là tự viết phần mã mình cần
  • Công sức ban đầu có thể nhiều hơn, nhưng sau khi viết xong thì không cần crate mới và cũng không phải chờ tác giả upstream sửa các edge case
  • Nếu mã bị hỏng trong chính nơi sử dụng thì có thể tự sửa, và mã đang chạy tốt không nhất thiết phải bước lên vòng chạy bộ bảo trì
  • Tính đến năm 2025, ChatGPT hay Cursor có thể nhanh chóng tạo ra triển khai không phụ thuộc cho các tính năng nhỏ phổ biến
  • Nhiều hàm nhỏ có overhead bảo trì thấp, và có thể nhẹ gánh hơn so với việc nâng cấp dependency liên tục
  • Nếu chỉ là vài dòng mã, thì không cần biên dịch hàng nghìn dòng mã của người khác chỉ để lấy một chức năng đơn lẻ

Đánh giá cao hơn các phụ thuộc thấp

  • Không phải mọi dependency đều xấu
    • Thư viện đồ họa trừu tượng hóa các driver phức tạp
    • Triển khai các giao thức như HTTP và QUIC
    • Những thư viện quan trọng như tokio, vốn không thể bỏ và cũng không có ý định bỏ
  • Tuy vậy, nếu chỉ dùng một hàm mà phải biên dịch hàng trăm hàm thì nên xem đó là tín hiệu cảnh báo
  • Nên công nhận hơn lựa chọn tự viết hàm nhỏ để tránh đồ thị phụ thuộc bắc cầu
  • Cần hoài nghi hơn với các đồ thị crate lớn, và nhìn tích cực hơn với mã đơn giản, ổn định, có thể không cần đụng tới trong nhiều năm
  • sha1-smol ban đầu, dưới tên sha1, từng trở thành crate tiêu chuẩn để tính hash SHA1, nhưng sau đó nhường tên cho rust-crypto và chịu áp lực phải phù hợp với hệ sinh thái crypto lớn hơn
    • Dùng crate sha1 mới thì sẽ kéo theo 10 dependency
    • Vì tên trong registry rất quan trọng và có yêu cầu tương thích trait, nên khó tránh được
  • MiniJinja nhấn mạnh phụ thuộc thấp trong README
$ cargo tree
minimal v0.1.0 (examples/minimal)
└── minijinja v2.6.0 (minijinja)
    └── serde v1.0.144
  • MiniJinja có một PR nhằm loại bỏ phụ thuộc cuối cùng
  • Trong những tình huống phù hợp, nên chúc mừng việc tự làm, và dành nhiều sự ghi nhận hơn cho các tác giả xây dựng thư viện mã nguồn mở với phụ thuộc thấp hoặc không có phụ thuộc

1 bình luận

 
GN⁺ 2025-01-26
Các ý kiến trên Hacker News
  • Bản thân ngôn ngữ Rust thì tốt, nhưng tôi không thích hệ sinh thái phụ thuộc của Rust. Nhiều người phàn nàn rằng C++ khó thêm dependency, nhưng ngược lại tôi thấy điều đó giống như một tính năng, vì nó khiến mình phải nghĩ xem có thật sự cần hay không.
    Trong C++, tôi kiểm soát các dependency; còn trong Rust, chúng nhanh chóng vượt quá 100 và tôi đành bỏ cuộc. Nhìn từ góc độ bảo mật, thật lòng tôi không thể biết mình đang phân phối những gì.
    Hơn nữa Rust cũng không có tương thích ABI và không có văn hóa thư viện dùng chung. Vì vậy nó tạo cảm giác làm hỏng mô hình phân phối gói của hệ điều hành. Khi chọn một bản phân phối Linux, tức là ta tin tưởng những người build bản phân phối đó; còn Rust trông gần với Python hơn, nơi bất kỳ ai cũng có thể đưa thứ gì đó lên PyPI.

    • Debian/Ubuntu có thể được xem là an toàn vì các maintainer ký gói và những maintainer đó được xác minh. Cuối cùng đây là một cấu trúc dựa trên niềm tin rằng Debian/Ubuntu chỉ cho phép những người đáng tin cậy ký gói.
      Vậy Docker/Python/Rust thì sao? Tôi hoàn toàn không biết những người tạo ra Docker image, gói PyPI hay crate Rust mà tôi dùng là ai.
      Về thực chất, chúng ta đã quay lại thời kỳ trao đổi EXE và DLL bằng ZIP. Chỉ khác là giờ ta gọi nó là container và tự hào chạy nó với quyền root.
      Như tác giả nói, đôi khi cách tốt nhất là cứ gộp thẳng mã nguồn của dependency vào. Trước đây người ta gọi việc này là vendoring, và nó từng khá quan trọng trong Rails/Ruby. Lợi ích lớn là về sau không bị ảnh hưởng bởi việc chiếm đoạt gói độc hại; nếu muốn, bạn có thể tự merge các bản vá bảo mật từ upstream.
    • Tôi không hiểu những người có quan điểm này đang làm loại công việc gì mà có thể tái phát minh bánh xe trong mọi dự án chỉ vì sợ các rủi ro giả định. Phần lớn những rủi ro đó vẫn còn nguyên trong các bản tự triển khai lại nội bộ, và bug cũng vậy.
      Tôi không nhớ ra dự án nào mà hội chứng NIH đem lại hiệu quả ròng tích cực. Ngay cả những dependency không thiết yếu cũng tiết kiệm rất nhiều thời gian.
      Nếu cứ tạo lại một phần các dependency tùy chọn trong mọi dự án, tôi tự hỏi lấy đâu ra thời gian để sửa thêm bug, các điều kiện biên, nhiều phiên bản của dependency cấp dưới, và hỗ trợ nhiều nền tảng.
    • Nếu ngay từ đầu đã có trình quản lý gói, làm giảm mạnh ma sát khi thêm và phân phối package, hệ sinh thái ngôn ngữ sẽ mất đi một thứ rất hữu ích: áp lực tốt của chọn lọc tự nhiên.
      Khi điều này kết hợp với các kho mà ai cũng có thể upload và ít được tuyển chọn như PyPI, npm, Cargo, cùng với một thư viện chuẩn nhỏ, thì phải chuẩn bị tinh thần chịu đau.
    • Tôi tự hỏi liệu tính năng “một tập hợp thư viện có thể dễ dàng import” có trở thành phản tính năng chỉ vì nó khiến việc đưa thư viện hỗ trợ vào trở nên dễ dàng hay không. Điều này có vẻ giống vấn đề về lựa chọn của lập trình viên hơn là vấn đề của Rust hay JS.
      Dependency nào được đưa vào là do lập trình viên chọn. Những dependency không ai dùng sẽ biến mất, và lý do hầu hết thư viện có nhiều dependency là vì nhiều lập trình viên thích xây thứ gì đó trên nền dependency.
      Nếu nói về build system thì Cargo không bắt buộc dùng Crates.io. Nó chỉ làm việc đó dễ hơn. Bạn cũng có thể dùng dependency dựa trên đường dẫn như CMake/Vcpkg, Conan, hoặc tự tạo thư viện.
      Ngay cả khi dùng Crates.io, nếu không thích thay đổi thì chỉ cần pin phiên bản. Nó chỉ giúp lấy phiên bản mới nhất dễ hơn.
      Trong Rust, việc xây phần mềm trên nền phần mềm sẵn có rất dễ. Nếu bạn không thích phần mềm hiện có hoặc tốc độ thay đổi của nó, đừng đổ lỗi cho Cargo; hãy làm theo cách bạn muốn.
    • Vấn đề thật sự khi thêm dependency trong C++ là khó thêm theo “cách mà mọi người đều có thể build mà không gặp vấn đề”. Dù chạy được trên máy của tôi, nó có thể không chạy được trong môi trường của người dùng hoặc người đóng góp tiềm năng, và đó là vấn đề.
      Các bản phân phối vẫn build gói Rust từ source và vendor các dependency crate vào kho. Việc này đau hơn vì có nhiều dependency hơn và cập nhật thường xuyên hơn, nhưng điều đó tách biệt với thư viện dùng chung.
  • Nói rằng API để biết kích thước terminal đã ổn định 50 năm là không chính xác. Theo tôi biết, TIOCGWINSZ ioctl chưa từng được chuẩn hóa, và giữa các Unix và BSD cũng có nhiều tên khác nhau.
    Hàm tcgetwinsize() mãi đến năm 2024 mới được đưa vào POSIX, và toàn bộ chủ đề này có một lịch sử khá đáng tiếc https://news.ycombinator.com/item?id=42039401. Ý là chuyện đã như vậy ngay cả trước khi bàn đến phía Windows.

    • Ấn tượng tôi nhận được từ bài viết là bản triển khai thay thế không đơn giản vì nó tốt hơn, mà vì nó chỉ xử lý use case của chính tác giả.
      Đôi khi điều đó ổn, nhưng phần mềm có thể trở nên tệ hơn với những người có use case khác, hoặc những người chạy nó trên các hệ thống mà bản thân họ không hiểu và không dùng.
      Giá trị thật sự của thư viện nằm ở chỗ nó xử lý được việc ngay cả các vấn đề trông đơn giản cũng thực ra có nhiều phức tạp. Tôi cũng không nghĩ 3–4 dependency cho một thư viện chạy trên cả Windows/Linux là quá nhiều. Trong trường hợp tốt, người viết thư viện đó có nhiều kinh nghiệm hơn trong lĩnh vực ấy và có thể xử lý cả những vùng chưa biết mà tôi không biết.
    • Dù không được chuẩn hóa, các lời gọi như vậy cũng không thay đổi. Đặc biệt là các lời gọi Windows đã được biên dịch vào vô số binary, nên tính ổn định ABI được bảo đảm.
      ioctl rõ ràng có vấn đề, nhưng những thay đổi tạo ra nhiều release trong terminal-size hoặc các dependency của nó không liên quan đến hằng ioctl/TIOCGWINSZ hay struct winsize. Đoạn code đó không thay đổi.
    • Trong trường hợp này, crate terminal-size chỉ gọi tcgetwinsize của Rustix, rồi Rustix lại gọi tcgetwinsize của libc. Vì vậy nếu tự làm điều tương tự, bạn có thể giảm khá nhiều dependency, cái giá phải trả chỉ cỡ hỗ trợ Windows.
      API này ổn định trong 50 năm hay 25 năm chỉ là chi tiết. Dependency đó thậm chí không giả vờ xử lý sự phức tạp ấy, và khả năng hàm đó thay đổi hoặc bị gỡ bỏ trong tương lai gần cũng thấp.
    • Terminal trông rất đơn giản, nhưng đây là ví dụ hay về một thứ trở thành cơn đau đầu lớn vì thời kỳ đầu có quá nhiều nhà sản xuất và chuẩn ngành không được thiết lập. Ngay cả việc thứ gần nhất là VT100 hay VT102 cũng còn mơ hồ.
      Phần lớn người ta ghi trực tiếp vào đó, nhưng các chức năng như kích thước terminal hay raw mode thì lộn xộn và cần những thứ như ioctl. Nói thật là không hay lắm.
      Nhưng thư viện còn tệ hơn. Nếu bạn không muốn link với ncurses, thì chỉ còn biết cầu Chúa phù hộ.
  • Gần đây tôi đã hồi sinh web app startup đầu tiên mình làm năm 2006. Đó là một trang mạng xã hội tập trung vào chia sẻ media, và theo tiêu chuẩn thời đó thì là một LAMP stack khá đơn giản. Dùng PHP 5, MySQL 3.2, nhưng hầu như có đủ các tính năng mạng xã hội lúc bấy giờ
    Vì muốn tự thử các công nghệ CI/CD mới, tôi đang dùng app này như một bài học và over-engineer quy trình triển khai. Cũng có thể dùng WordPress hay app Hello World, nhưng cách này thú vị hơn nhiều
    PHP gần như toàn bộ là tự viết. Tôi cũng tự làm các thư viện cho xác thực/phân quyền, template, xử lý form, v.v.; chỉ dùng một thư viện PEAR để gửi email. Frontend là HTML thuần, gần như không có JavaScript, còn phát media thì dùng Flash. Năm 2006 đa số đều làm như vậy
    Chỉ mất khoảng một giờ để chạy lại app 19 năm tuổi này. Tôi đổi driver PHP mysql cũ sang mysqli, điều chỉnh schema và một số query cho MySQL 8. Chủ yếu chỉ là bọc bằng backtick những từ nay đã thành từ khóa dành riêng, và sửa các giá trị mặc định của cột vốn giờ nghiêm ngặt hơn. Thứ duy nhất không chạy là Flash
    Ngược lại, ở công ty hiện tại, chúng tôi vận hành hàng chục app Spring Boot viết bằng Java 8, và danh sách lỗ hổng xuất phát từ hàng chục dependency cứ chất thành từng trang. Cập nhật một cái thì phải nâng tiếp một loạt thư viện khác, và vì dependency bắc cầu nên thành ác mộng. Vì vậy chúng tôi chỉ xử lý tối thiểu các lỗ hổng nghiêm trọng nhất, và không có kế hoạch thực tế nào để nâng cấp toàn bộ
    Điều buồn cười là việc app PHP năm 2006 làm và những gì các app Spring Boot hiện nay làm cũng không khác nhau nhiều. Rốt cuộc đều là CRUD, chỉ là xung quanh được gắn thêm nhiều đồ trang trí và công cụ kiểu enterprise hơn rất nhiều

    • Nhìn vào Go và thế giới Linux họ C, tôi bị thuyết phục bởi triết lý thư viện dày. Tức là lấy một thư viện giải quyết vấn đề của cả một lĩnh vực, rồi bổ sung những phần cần thiết vào đó
      Không kéo về một dependency riêng cho từng mục trong checklist. Có thể phát sinh việc làm trùng lặp, nhưng đường nâng cấp sẽ dễ hơn nhiều
    • Phần lớn các công nghệ CI/CD mới trở thành tiêu chuẩn vì sự phức tạp sinh ra từ việc duy trì dependency bên thứ ba và môi trường chạy thay đổi liên tục. Với các ứng dụng LAMP cũ triển khai bằng scp, nhìn chung đó không phải vấn đề
    • Tôi đồng ý một phần, nhưng không muốn tốn thời gian phát minh lại bánh xe rồi nhét bug vào đó
      Ví dụ các định dạng thông điệp truyền thông theo chuẩn như FHIR hay HL7 có định nghĩa chuẩn vốn đã phức tạp, chắc bạn sẽ không muốn tự triển khai toàn bộ
      Các hàm mã hóa cũng vậy: tự viết thường là tự bắn vào chân mình, và nhiều vấn đề bảo mật nghiêm trọng được phát hiện qua nhiều năm đã chứng minh điều đó
      Đây là thời đại tôi muốn tập trung vào giải quyết vấn đề kinh doanh hơn là việc giải pháp được xây dựng đúng cách ra sao. Khi AI xuất hiện và mọi đoạn code đều có cảm giác như được nhắm mắt ghép lại với nhau, điều này càng quan trọng hơn
      Dành thời gian tự làm mọi thứ về dài hạn có thể có lợi, nhưng trước hết phải sống sót trong cạnh tranh đã. Đối thủ có thể đã chiếm thị trường từ sớm bằng thứ code nhanh và có thể vứt bỏ
    • Ở chỗ làm có chính sách phân tích tĩnh khá nghiêm ngặt và từ tháng 4 sẽ còn nghiêm hơn. Không biết bạn đã xem https://docs.openrewrite.org/ để tự động nâng cấp dependency chưa
      Gần đây chúng tôi chuyển từ Java 8, Spring Boot 2, Swagger sang Java 17, Spring Boot 3.3, OpenAPI 3, và khá painless
      Vẫn còn phải nâng thêm một số dependency trực tiếp và bắc cầu, nhưng rào cản lớn nhất đã được xử lý bằng migration
    • Điểm quan trọng ở đây là Rust về cơ bản không có vấn đề dependency bắc cầu. Nếu nâng một crate nào đó khiến dependency công khai cũng được nâng, và dependency đó lộ ra trong API nên bạn phải tương tác với nó, thì dĩ nhiên phải nâng cùng
      Nhưng miễn là không link tới thư viện C, các dependency bắc cầu không công khai hoàn toàn không ảnh hưởng. Trong cùng một cây dependency, bạn có thể có bao nhiêu phiên bản crate không tương thích SemVer tùy ý, và nếu cần cũng có thể phụ thuộc trực tiếp vào nhiều phiên bản
      Không cần kiểu nâng cấp hàng loạt quy mô lớn như Java; chỉ cần nâng đúng cái có lỗ hổng. Tôi biết C# cũng có tính năng tương tự, nhưng dài dòng hơn một chút
  • Đồng ý 100%. NodeJS đã ảnh hưởng lớn đến sự nghiệp của tôi, nhưng NPM còn gây sang chấn lớn hơn cả tuổi thơ hỗn loạn
    Cứ tưởng tượng một lập trình viên mới đang làm một app mới để khoe với bạn bè. Muốn thêm dependency mới nhưng nó không khớp với dependency khác, nên quyết định cập nhật tất cả. Ngay khoảnh khắc sau, chẳng có gì chạy và Babel bắt đầu la hét
    Bạn vẫn nghĩ rồi kiểu gì cũng giải quyết được, nhưng chẳng mấy chốc sẽ thấy các issue Git đang mở về việc ngay cả những thứ cơ bản cũng theo đúng nghĩa đen là không hoạt động. Ví dụ Expo có issue đang mở rằng project React Native mới mặc định không build được trên Android
    Một nửa thời gian có vẻ chẳng ai quan tâm, và trong trường hợp đó, lời giải cũng không nằm trong hệ sinh thái Node mà ở đâu đó trong hệ sinh thái Android. Từ trên xuống dưới đều là băng keo. Dù vậy, nếu một dự án trị giá hàng tỷ đô có thể phát hành template không chạy, thì điều đó cũng cho tôi sự tự tin rằng không có lý do gì phải bị hội chứng kẻ mạo danh chỉ vì side project của mình chỉ chạy được một nửa

    • Đây là vấn đề của Node, không phải của Rust. Các dependency không cần phải “hợp ý” nhau. Bạn có thể để mọi phiên bản cùng lúc, và không có gì bị vỡ
    • Việc Node.js và npm mất kiểm soát khoảng 15 năm trước đã là hồi chuông cảnh tỉnh đối với tôi
      Tôi bắt đầu xem khoảnh khắc đưa dependency đầu tiên vào như một thất bại cá nhân. Vì từ thời điểm đó, thay vì chỉ load JS bình thường bằng thẻ script, để dùng một package.json duy nhất thì toàn bộ việc đóng gói và tổ chức trở nên cần thiết
  • Đây là điều khiến tôi ngạc nhiên khi chuyển từ Go sang hệ sinh thái Rust. Một dự án Go trưởng thành, chẳng hạn backend web production của một công ty nào đó, có thể chỉ có 10–20 dependency tính cả dependency bắc cầu.
    Như bài viết nói, ngay cả một dự án Rust nhỏ cũng rất có khả năng nhiều hơn thế, và nếu làm tác vụ bất đồng bộ thì gần như chắc chắn.
    Tôi không rõ đây là do văn hóa hay do tính năng ngôn ngữ. Ví dụ trong Go, interface được thỏa mãn ngầm, nên không cần kéo thêm thứ gì chỉ để nói rằng mình implement nó.

    • Một lý do lớn là Go có thư viện chuẩn xuất sắc và hoàn thiện, còn Rust thì thực sự không như vậy.
      Những thứ có trong ngôn ngữ hoặc thư viện chuẩn của Go nhưng ở Rust cần dependency gồm green thread, channel, regex, HTTP client, HTTP server, thời gian, cờ dòng lệnh, logger, đọc/ghi GIF động, v.v.
      Tôi không đặc biệt thích bản thân ngôn ngữ Go, nhưng về công cụ và thư viện chuẩn thì Go dẫn đầu, và chắc chắn là tốt nhất trong số những gì tôi từng dùng.
    • Khác biệt giữa Rust và Go đặc biệt nằm ở thư viện chuẩn. Thư viện chuẩn của Rust được cố ý giữ nhỏ.
    • Thư viện chuẩn đồ sộ của Go giúp giảm số lượng dependency rất nhiều.
  • Tôi rất đồng tình với lập luận này. Việc cho phép để lộ abstraction của thư viện khác ra bên ngoài thư viện của mình có rất nhiều chi phí ẩn. Không phải là tuyệt đối không bao giờ nên làm, nhưng tại thời điểm quyết định cần cân bằng cả chi phí trong tương lai.
    Nếu package bọc bên ngoài thường xuyên đổi thiết kế hoặc đổi mục tiêu, nó sẽ tạo ra sự bất ổn. Những tính năng vốn là dạng bảo đảm đơn giản nhất trong kỹ nghệ phần mềm sẽ biến mất hoặc bị tách nhỏ.
    Nó cũng khiến những người sở hữu không phải contributor open source chuyên nghiệp nhưng hiểu rõ một lĩnh vực cụ thể khó tham gia vào hệ sinh thái hơn. Nếu “tôi đã tạo ra một cách làm X gọn gàng” biến thành nhiều tuần thảo luận, thương lượng và chính trị kiểu “vì mọi người có thể thích Z nên X phải khớp dưới Y”, thì đó là lãng phí thời gian của tất cả.
    Một điều tôi học được trong đời là thứ đơn giản nhất sẽ sống lâu nhất. Nên khen ngợi minilith và monolith thường xuyên hơn. Đây không chỉ là vấn đề riêng của Rust; tôi đã thấy ở nhiều ngôn ngữ. Cộng đồng open source thường thúc đẩy rất mạnh các package có kích thước cấp nguyên tử, và tôi hay tự hỏi liệu mục đích lớn hơn có phải là cắm cờ hoặc chuyển giao quyền sở hữu, hơn là lợi ích của cộng đồng người dùng thực sự hay không.

  • Đến năm 2025, tôi phần nào tình cờ đi đến quan điểm rằng để ChatGPT hoặc Cursor tạo các bản implement không dependency cho những hàm phổ biến thường nhanh hơn, và tôi ngày càng đồng ý với điều đó. Đặc biệt là sau khi trải qua địa ngục dependency trong một ứng dụng React lớn.
    Cũng cần nghĩ đến dependency vào SaaS hoặc dịch vụ bên thứ ba. Nhiều thứ trong số đó là pattern phổ biến và là vấn đề đã được giải, nên LLM có thể nhanh chóng tái tạo.

    • So với trước đây, tôi có xu hướng bắt đầu bằng một hàm tiện ích nội bộ nhỏ do AI implement trước khi thêm thư viện hơn rất nhiều.
      Với các vấn đề có phạm vi giới hạn, cách này khá hiệu quả, và tôi có thể nhờ AI tạo ra một implementation đầy đủ và vững chắc hơn so với tự mình viết.
      Kể cả sau này quyết định thêm thư viện, nó cũng tạo ra sự đóng gói tự nhiên. Chỉ cần đổi implementation của hàm của mình để dùng thư viện, không nhất thiết phải đụng tới mọi nơi đang sử dụng. Khi đến lúc, việc thử nhiều thư viện cũng dễ hơn.
  • Trớ trêu là chuyện này khá thú vị. Armin là tác giả ban đầu của framework web Python Flask. Cùng thời điểm đó cũng có một thư viện rất giống tên là Bottle.
    Chức năng gần như tương đương, nhưng Flask trở nên rất phổ biến, còn tôi thì luôn thích Bottle hơn. Vì nó là một file duy nhất và không có dependency, nên rất dễ chỉ việc copy vào dự án.
    Nó cũng dễ sửa đổi, và cuối cùng tôi hiểu được toàn bộ. Tôi gắn Gevent cho server và websocket, nhưng theo cách đó vẫn có thể xây dựng cả những dự án khá nặng.
    Đến giờ tôi vẫn có thôi thúc mạnh mẽ muốn dùng Bottle cho các dự án web nhỏ. Chỉ tiếc là nó không theo kịp nhiều thực hành hiện đại mà Python đã đưa vào trong nhiều năm, nên giờ trông hơi cũ.

  • Muốn tự làm thì cần năng lực kỹ thuật đủ tốt. Nếu chỉ có những kỹ sư luôn lấy thư viện từ các hệ sinh thái như NPM hay PyPI, họ sẽ gặp khó khi tự phát triển lời giải cho nhiều vấn đề. Đặc biệt nếu đó phải là lời giải bền lâu và có độ linh hoạt cần thiết.
    “Không tự lập trình mình vào ngõ cụt” cần rất nhiều luyện tập.
    Tôi cũng thường thấy có những trường hợp có thể làm tốt hơn thư viện hiện có một cách dễ dàng. Trong một dự án, tôi đã implement parser cho một biến thể Markdown có metadata ở đầu file. Nó dùng một cú pháp nhỏ, và parser chỉ chưa tới một màn hình code.
    Nhưng thư viện frontend parse cùng loại file đó lại tệ hơn tôi tưởng rất nhiều. Nó bị lỗi nếu identifier metadata có dấu gạch nối, và hóa ra nó đang dùng trực tiếp identifier metadata làm tên member của object. Thay vì dùng một object JSON đơn giản, nó đã tự giới hạn việc chọn tên một cách giả tạo và khiến mọi thứ có dấu gạch nối như something-something bị lỗi.
    Cuối cùng parser của tôi bị bỏ, với lý do là mọi người sẽ phải “bảo trì” nó. Nhưng nó đơn giản là hoạt động, và có thể dễ dàng thích ứng với thay đổi cú pháp. Nếu biết chút ít về parser thì nó cũng không có gì khó hiểu.
    Khó tin là trong team có vẻ ngoài tôi ra chẳng ai từng viết parser bằng thư viện parser generator. Còn nhiều ví dụ như vậy nữa.

  • Nếu chỉ dùng một hàm mà lại phải biên dịch hàng trăm hàm khác thì đèn cảnh báo nên bật lên. Khoảng một năm trước, tôi từng làm một dự án cập nhật các dependency bên thứ ba
    Một trong số đó là một thư viện toán học phong phú, chứa đủ loại hàm toán học. Đào sâu một chút thì hóa ra thứ chúng tôi dùng chỉ là đúng một phương thức duy nhất để tìm trung vị của một danh sách
    Tôi chỉ cho kỹ sư phụ trách trang Wikipedia, yêu cầu gỡ dependency đó và viết một phương thức đơn lẻ để thực hiện phép toán ấy
    Tuy nhiên, vấn đề thực sự không phải là bản thân việc dùng dependency bên thứ ba, mà là cần có khái niệm chỉ lấy một lát cắt hẹp của thư viện. Nếu chỉ cần một phần nhỏ của một thư viện khổng lồ, tại sao phải kéo về toàn bộ? Tôi từng nghe có người đề xuất “microframework” như một cách làm cho việc này

    • Trong Rust, thư viện có thể định nghĩa các tính năng (features) có thể bật/tắt có điều kiện, nên có một cách tích hợp sẵn để điều chỉnh phạm vi thực sự được đưa vào. Tokio là một ví dụ tốt; bạn có thể ngạc nhiên khi biết tổng số dependency trực tiếp mà Tokio bắt buộc cần chỉ là hai. Phần còn lại đều là tùy chọn https://github.com/tokio-rs/tokio/blob/ee19b0ed7371b069112b9...
      Đáng tiếc là có vẻ mọi người không xem kỹ tập tính năng mặc định mà dependency sử dụng và chủ động cắt giảm chúng. Cú pháp để kéo thêm tính năng thì đơn giản, nhưng để loại bỏ các tính năng tùy chọn được bật mặc định thì phải chỉ định no-default-features, rồi vẫn phải thêm lại từng tính năng mặc định mà mình muốn, nên phiền phức hơn
      Tệ hơn nữa, nếu muốn cho phép thư viện cắt bỏ các tính năng không cần thiết trong dependency của chính nó, bạn phải tự tạo các tính năng của mình rồi ánh xạ chúng tới tính năng của từng dependency. Ví dụ, nếu có 5 dependency và mỗi dependency có 1 dependency bắt buộc cùng 4 dependency tùy chọn, thì để người dùng kiểm soát hoàn toàn các tính năng bắc cầu, bạn phải tạo 20 tính năng ánh xạ trong thư viện của mình. Con số đó còn chưa tính các tính năng bạn tạo để quan tâm đến người dùng hạ nguồn cho chính mã của mình
      Tôi ngày càng thấy tính khả dụng quanh features, đặc biệt là ở hướng giảm bớt sự phình to không cần thiết, quá tệ đến mức nó trở thành chất xúc tác cho vấn đề thời gian biên dịch của Rust. Có vẻ chủ đề này cũng không được thảo luận rộng rãi mỗi khi được nêu ra, nên có lẽ tôi nên viết một bài blog nêu rõ quan điểm mạnh mẽ của mình, để sau này có thể trỏ tới khi hiện trạng vẫn tiếp diễn
    • Làm vậy sẽ dẫn tới những chuyện như leftpad trong hệ sinh thái JS
      Nếu hệ thống tin cậy tốt thì một mặt tôi nghĩ đó không phải là vấn đề
      Tôi cũng khá thích cách làm kiểu Shadcn, tức là nếu thích một component thì sao chép nó vào thư viện của mình. Nhưng nếu có lỗ hổng phát sinh, tôi sẽ không biết mình có bị ảnh hưởng hay không
      Với một số mã thì không sao, nhưng với mã khác thì thật sự nên dựa vào việc có càng nhiều con mắt xem xét càng tốt
    • Viết bằng mã giả cho những ai chưa biết thì đại khái như thế này
      Median(list) { let len = length(list) if len % 2 == 0 { let x = floor(len/2) return (list[x] + list[x+1]) / 2 } return list[len/2] }
      Tuy nhiên, giả định danh sách đã được sắp xếp. Tôi đã cưỡng lại cám dỗ gọi is_odd ở đây