3 điểm bởi GN⁺ 2025-11-01 | 1 bình luận | Chia sẻ qua WhatsApp
  • John Carmack chia sẻ quan điểm cá nhân về việc sử dụng biến có thể thay đổi (mutable variable)
  • Ông cho biết khi dùng Python, ông đã trở nên lơ là với nguyên tắc “gán một lần” (single assignment), và nói rằng bản thân cần phải tự cảnh giác với điều đó
  • Ông nhấn mạnh rằng nên tránh gán lại hoặc cập nhật biến, ngoại trừ các phép tính lặp trong vòng lặp
  • Khi tất cả các bước tính toán trung gian đều được giữ lại, điều đó hữu ích khi gỡ lỗi, đồng thời ngăn vấn đề vô tình dùng giá trị trước đó khi di chuyển các khối mã
  • Ông giải thích rằng trong C/C++, việc khai báo gần như mọi biến là const ngay tại thời điểm khởi tạo là một thói quen tốt
  • Cuối cùng, ông nhấn mạnh rằng ông “ước gì mutable là một từ khóa”, thể hiện mong muốn tính bất biến trở thành mặc định

1 bình luận

 
GN⁺ 2025-11-01
Ý kiến trên Hacker News
  • Sau 2 năm dùng Clojure, tôi nhận ra việc giải thích sự rõ ràng mà tính bất biến mang lại cho các lập trình viên khác thực sự rất khó
    Những người đã quen với lối tư duy tạo ra hiệu ứng thông qua thay đổi trạng thái rất khó hiểu được điều này nếu chưa tự mình trải nghiệm

    • Việc thay đổi biến tạo ra sự phụ thuộc vào thứ tự một cách ngầm định
      Ví dụ, nếu viết x = 7; x = x + 3; x = x / 2 thì đổi thứ tự vẫn không báo lỗi nhưng kết quả sẽ khác
      Ngược lại, nếu dùng biến mới như x1, x2 thì khi sai thứ tự sẽ phát sinh lỗi, qua đó làm vấn đề lộ rõ hơn
      Cuối cùng, gán một lần duy nhất (single assignment) là cách biểu đạt tường minh những phụ thuộc kiểu này
    • Tôi cũng từng có trải nghiệm tương tự với Scheme
      Dù có giải thích cho đồng nghiệp chưa từng dùng ngôn ngữ hàm rằng tư duy xoay quanh hàm dễ kiểm thử và gọn gàng đến mức nào, họ vẫn khó cảm nhận được
      Python khó viết theo phong cách hàm mà vẫn dễ đọc, còn JS thì tôi thấy lại làm tốt hơn
      Cuối cùng chỉ những lập trình viên tò mò mới thử các ngôn ngữ như Clojure
    • Nếu lấy dữ liệu bất biến và hàm thuần làm mặc định, có thể coi hàm như một hộp đen hoàn toàn
      Hàm không cần biết trạng thái bên ngoài, và bên ngoài cũng không cần biết bên trong hàm
      Không cần biết toàn bộ trạng thái của chương trình vẫn có thể kiểm thử hoặc gỡ lỗi riêng từng hàm
    • Việc đơn giản đặt tính bất biến và khả biến vào thế đối lập là cách né tránh độ phức tạp
      Thật thú vị khi cộng đồng Haskell rốt cuộc lại cố tái phát minh tính khả biến bên trong hệ thống kiểu
      Cốt lõi là kiểm soát tác dụng phụ với chi phí thấp nhất có thể
    • Clojure là ngôn ngữ có ảnh hưởng nhất mà tôi từng học
      Haskell có rào cản nhập môn cao vì hệ thống kiểu, còn F# thì quá thỏa hiệp nên rốt cuộc lại viết theo cú pháp C#
      Nhờ homoiconicity và các cấu trúc dữ liệu mạnh mẽ của Clojure, lần đầu tiên tôi thật sự hiểu rõ khái niệm “làm việc với giá trị”
      Có lẽ tôi sẽ không dùng nó trong công việc, nhưng với ai chưa có kinh nghiệm về ngôn ngữ hàm hay Lisp thì tôi rất muốn giới thiệu
  • Tôi mong biến mặc định là bất biến và mọi thứ đều là biểu thức (expression)
    Nhưng thực tế là với tư cách lập trình viên Clojure, tôi đang khổ sở vì cuộc xâm lấn của Python

    • Tôi cũng là lập trình viên Python, nhưng chỉ dùng Clojure cho dự án cá nhân
      Giờ thì tôi lại đang khổ sở vì cuộc xâm lấn của TypeScript, nên rất đồng cảm
    • Sau khi học Rust, tôi nhận ra ngôn ngữ không cần phải thuần hàm mà mọi thứ vẫn có thể là biểu thức
      Cách này thực sự hữu ích để giới hạn phạm vi thay đổi
    • Clojure lúc nào cũng nhanh hơn Python, đó cũng là một niềm an ủi
    • Bạn chỉ đơn giản là người dùng Clojure, không cần phải tự định nghĩa mình là “lập trình viên Clojure”
      Không cần cuốn vào các cuộc chiến bộ lạc giữa các ngôn ngữ
      Trong thời đại năng suất tăng vọt, những ranh giới đó trở nên vô nghĩa
      Tôi khuyên đọc bài Don’t Call Yourself a Programmer
  • Tôi cố giảm thiểu việc gán lại biến, nhưng đôi khi vẫn dùng variable shadowing
    Tôi thích kiểu result = result.process() vì nó ngắn gọn

    • Có thể ví dụ hơi trừu tượng, nhưng trong đa số trường hợp vẫn có thể đặt tên rõ ràng cho từng bước
    • Kiểu này có thể dẫn đến lỗi bảo mật
      Ví dụ nếu process()hàm kiểm tra/kiểm định, thì sẽ khó biết ở thời điểm nào giá trị đã được xử lý
      Vì vậy tốt hơn là phân biệt rõ trạng thái bằng tên gọi
    • Trong phong cách hàm, chuyện này có thể giải quyết bằng chaining hàm mà không cần biến trung gian
      Ví dụ: result = x |> foo |> bar |> baz hoặc (-> x foo bar baz)
    • result.process() là cái gì vậy, rốt cuộc là result nào và process gì?”
      Người đọc lại đoạn code sau này sẽ thấy bối rối
    • Đã là kết quả (result) rồi mà còn xử lý (process) tiếp thì về mặt logic khá kỳ cục
  • Bản thân từ “biến (variable)” cũng luôn khiến tôi thấy lấn cấn
    Đã là bất biến thì tại sao lại gọi là variable?

    • Biến không thay đổi trong lúc chạy, nhưng giữa các lần gọi thì giá trị có thể khác nhau
      Trong Rust, chỉ khi khai báo mut thì mới có thể thay đổi
      Ngược lại trong C, muốn tạo hằng số lại phải dùng preprocessor nên khá rối rắm
    • Đối số x của một hàm nhận giá trị khác nhau ở mỗi lần gọi, nên bản thân nó là giá trị thay đổi
      Dù không có gán lại thì vẫn có thể gọi là biến
    • Trong toán học, biến là ký hiệu chỉ một giá trị tùy ý chứ không phải một đối tượng cụ thể
      Lập trình đã mang nguyên khái niệm này sang
    • Rốt cuộc nó là variable vì giá trị có thể khác nhau giữa các lần chạy
      Hằng số (constant) thì có cùng một giá trị trong mọi lần chạy
      Xem thêm Variable (mathematics)
    • “Biến” ở đây không hẳn là thay đổi theo thời gian mà là thay đổi theo ngữ cảnh
  • Sẽ rất hay nếu IDE có thể hiển thị trực quan việc biến có bị thay đổi hay không
    Ví dụ chỉ cần đánh dấu nhẹ những biến đã bị thay đổi

    • Trong IntelliJ, biến bị gán lại sẽ có gạch chân, và khi di chuột lên sẽ hiện gợi ý “Reassigned local variable”
      Nếu dùng final càng nhiều càng tốt thì code sẽ dễ đọc và dễ bảo trì hơn
    • Nhưng tôi nghĩ opt-in tường minh tốt hơn là suy luận tự động
      IDE nên cảnh báo, và chỉ cho phép thay đổi khi thực sự cần thiết
      Giống như câu chuyện set vs list của Rich Hickey, nên chọn cấu trúc diễn đạt ý nghĩa một cách rõ ràng
    • Swift có thể phát hiện ở cấp độ compiler liệu biến có thực sự bị thay đổi không, và nếu không thì đề xuất đổi thành hằng số
    • Trong IDE của JetBrains đã có sẵn tính năng tìm vị trí đọc/ghi của biến
    • Nếu làm một linter cho việc này thì cái tên “mutalator” nghe khá hợp
  • Trước đây tôi từng làm một dự án áp dụng rất nghiêm ngặt tính bất biến vì thread safety
    Nhờ vậy code trở nên dễ đọc hơn, và cũng dễ lần theo thứ gì có thể thay đổi
    Từ đó tôi trở thành fan cuồng nhiệt của tính bất biến

    • Nếu vậy thì tôi rất muốn khuyên bạn thử Rust
  • Sau khi làm trong một codebase Haskell lớn rồi quay lại C, tôi lại thấy giá như bất biến là mặc định
    const là chưa đủ

    • Trong C, thật ra chỉ có thể gán lại giá trị chứ không có thay đổi trực tiếp
      Muốn thay đổi thì phải dùng con trỏ, còn C++ thì chỉ cần gọi hàm là đối số cũng có thể bị thay đổi nên khá thiếu minh bạch
    • Tôi tự hỏi liệu mặc định của Rust có cung cấp mức bất biến an toàn đủ dùng hay không
  • Tôi đồng ý với ý rằng “gần như mọi biến đều nên được khai báo là const”
    Rust xứng đáng được nhắc đến ở đây

  • Tôi hình dung ra một cú pháp mà mặc định là bất biến, và chỉ cho phép mutable bên trong một block cụ thể
    Ví dụ như block with của Python

    with mutable(x, items):
        x = 3
        items.append(4)
    

    Khi ra khỏi block thì lại trở về bất biến

    • Thực ra đây gần như chính là khái niệm mutable borrow
      Chỉ cần nhìn vào borrow checker của Rust là thấy khái niệm này phức tạp đến mức nào
    • Nếu không có borrow check, thì tham chiếu vẫn có thể còn tồn tại bên ngoài block và tiếp tục bị thay đổi
  • Có câu “State is the enemy
    Càng nhiều trạng thái thì số điều kiện phải kiểm thử càng tăng theo cấp số nhân
    Tính bất biến là cách ngăn bùng nổ trạng thái như vậy