Tranh luận về 'Màu sắc hàm': Tham số không phải là màu

Công nghệ08 tháng 9, 2026·6 phút đọc

Bài viết phân tích khái niệm 'function color' trong lập trình, đưa ra tiêu chí rõ ràng để phân biệt thay đổi màu (color change) với thay đổi thông thường ở tham số hàm. Tác giả lập luận rằng tham số hàm, kể cả context.Context trong Go, không phải là màu vì chúng có thể được đóng gói (encapsulate) và không lan truyền bắt buộc lên toàn bộ call stack.

Tranh luận về 'Màu sắc hàm': Tham số không phải là màu

Tranh luận về "Màu sắc hàm": Tham số không phải là màu

Trong các cuộc thảo luận về "màu sắc của hàm" (function colors) trên mạng, nhiều người cho rằng tham số hàm cũng có thể được coi là một dạng "màu". Tuy nhiên, bài viết này phân tích sâu và đưa ra một tiêu chí rõ ràng để phân biệt: thay đổi màu mang tính lan truyền bắt buộc lên toàn bộ call stack, trong khi thay đổi tham số thông thường có thể được "bẻ gãy" và đóng gói tại bất kỳ tầng nào. Tác giả dùng ví dụ context.Context trong Go và monad trong Haskell để minh họa cho luận điểm của mình.

Từ khái niệm "màu" đến tranh cãi về tham số

Khái niệm "function color" trở nên nổi tiếng từ bài luận của Bob Nystrom, dùng để chỉ sự khác biệt mang tính cấu trúc giữa hàm đồng bộ và hàm bất đồng bộ (async) trong một số ngôn ngữ. Khi bạn có một hàm async, nó gần như "nhuộm màu" toàn bộ call stack phía trên, buộc mọi hàm gọi tới nó cũng phải trở thành async.

Tranh cãi nảy sinh khi một số người mở rộng khái niệm này: nếu async là một "màu", thì tại sao tham số hàm không phải? Ví dụ, trong Go, khi một hàm bắt đầu sử dụng context.Context, bạn gần như phải "thêu" nó qua tất cả các hàm con — nghe có vẻ giống một màu?

Tác giả bài viết (Jerf) phản bác quan điểm này. Ông chỉ ra một điểm logic đơn giản: nếu mọi tham số đều là màu, thì khái niệm "màu" sẽ không bao giờ được phát minh ra — vì nó sẽ trở nên quá tầm thường. Phải có một điều gì đó đặc biệt và khác biệt ở async để đáng được gọi là "màu".

Tiêu chí hình dạng đồ thị phụ thuộc

Jerf đề xuất một tiêu chí cụ thể hơn: hãy xem xét việc thay đổi một thuộc tính nào đó của một hàm nằm sâu 4 tầng trong call stack. Sự thay đổi này có thể gây ra một trong ba trường hợp:

  • Không cần thay đổi caller nào — trường hợp lý tưởng.
  • Chỉ cần thay đổi caller trực tiếp — sự thay đổi lan truyền như một "danh sách liên kết" (linked list), nhưng có thể được dừng lại.
  • Bắt buộc thay đổi mọi hàm trong stack — đây chính là "màu".

Sự khác biệt mấu chốt nằm ở chỗ: với một thay đổi thông thường, việc lan truyền lên trên có thể bị "bẻ gãy" bởi bất kỳ hàm nào ở giữa. Bất kỳ hàm nào cũng có thể "bắt" lấy sự thay đổi, đóng gói nó lại, và các hàm phía trên không cần biết gì về nó. Điều này phản ánh đúng bản chất của lập trình có cấu trúc (structured programming) — một công cụ mà con người dùng phổ biến chính vì khả năng cô lập thay đổi này.

Ví dụ với Go context.Context

Jerf dùng Go làm ví dụ điển hình. Trong thực tế, khi cần thêm context.Context vào một hàm chưa từng có, việc sửa tất cả caller "dây chuyền" lên tới main là cực kỳ hiếm. Thay vào đó, bạn có thể gọi context.Background() để tạo một context trống, không có chức năng gì nhưng vẫn hợp lệ, và truyền nó vào cho các caller không quan tâm đến context.

"Việc thêm tham số context vào một hàm KHÔNG buộc mọi hàm phía trên phải thay đổi. Bạn có thể phá vỡ chuỗi lan truyền chỉ bằng cách truyền context.Background() từ bất kỳ đâu."

Đây là điểm khác biệt rõ ràng với async: nếu bạn thêm async vào một hàm trong JavaScript, bạn không thể gọi nó từ một hàm đồng bộ mà không có sự thay đổi về bản chất. Ngay cả khi bạn "đóng gói" nó, toàn bộ hàm gọi vẫn phải trở thành async. Không có cách nào để "dừng" sự lan truyền đó bằng một cú pháp thông thường.

Khi nào thì một màu thực sự là màu?

Một thay đổi chỉ thực sự là "màu" khi nó bắt buộc mọi hàm trong cùng "hòn đảo" ngữ nghĩa phải tuân theo. Điều đó có nghĩa là: nếu một hàm ở tầng giữa có thể thỏa mãn yêu cầu và đóng gói nó, tách biệt nó khỏi các hàm phía trên, thì đó không phải là màu.

Jerf đưa ra ví dụ về monad STM trong Haskell — một ví dụ về "màu một phần" (partial color). STM cho phép tạo biến giao dịch (transactional variable), nhưng để đọc/ghi biến này, bạn phải ở trong context STM. Nếu một hàm pure ở giữa call stack cần truy cập biến TVar, nó phải được "nâng" lên thành hàm STM — và sự thay đổi này lan truyền lên đến tận nơi sử dụng STM lần đầu tiên. Không có cách nào để "bẻ gãy" chuỗi này mà không dùng unsafePerformIO — điều mà trình biên dịch sẽ ngăn chặn hoặc ít nhất là cảnh báo mạnh mẽ.

Tuy nhiên, STM có một điểm đặc biệt: nó không lan truyền lên đến đỉnh chương trình (type signature của main trong Haskell là IO (), không chứa STM). Điều này cho thấy không phải mọi monad trong Haskell đều là màu — chẳng hạn monad State không phải, vì bạn hoàn toàn có thể box nó lại và truyền qua lại giữa các hàm một cách hợp lệ về mặt ngữ nghĩa, dù có thể không phải là cách hay nhất.

Kết luận: một quang phổ, không phải nhị phân

Khái niệm "màu" cuối cùng cũng là một phổ, không phải một đường kẻ rạch ròi. Trong JavaScript, bạn vẫn có thể chạy một tác vụ async trong context đồng bộ, nhưng phải trả giá bằng toàn bộ lợi ích của async — nên thực chất nó "không thực sự chạy async". Sự khác biệt giữa màu và không phải màu vẫn tồn tại, nhưng khi nhìn kỹ hơn, ranh giới trở nên mờ nhạt.

Điểm mấu chốt: Tham số hàm, kể cả context.Context, không phải là màu. Bởi vì sự thay đổi của chúng có thể được "đóng gói" bởi bất kỳ hàm trung gian nào trong call stack. Một màu thực sự phải vượt qua mọi rào cản trung gian và áp đặt lên toàn bộ các hàm liên quan.

Đối với lập trình viên Việt Nam, dù bạn đang làm việc với Go, JavaScript, Rust hay bất kỳ ngôn ngữ nào khác, việc hiểu rõ khái niệm "function color" giúp bạn đánh giá đúng chi phí thay đổi code của mình — khi nào một sửa đổi nhỏ sẽ lan tỏa khắp hệ thống, và khi nào nó chỉ dừng lại ở một tầng nhất định, giúp bạn đưa ra quyết định thiết kế tốt hơn.

Chia sẻ:FacebookX
Nội dung tổng hợp bằng AI, mang tính tham khảo. Xem bài gốc ↗