Vì sao Lisp khó đọc? Góc nhìn từ khoa học nhận thức

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

Lisp nổi tiếng với cú pháp đầy dấu ngoặc đơn và bị nhiều lập trình viên coi là khó đọc. Một bài phân tích dựa trên tâm lý học nhận thức chỉ ra bốn nguyên nhân cụ thể, đồng thời đề xuất các nguyên tắc thiết kế cú pháp cho ngôn ngữ lập trình mới.

Lisp là một trong những ngôn ngữ lập trình lâu đời nhất còn được sử dụng rộng rãi, nhưng nó cũng nổi tiếng vì cú pháp đầy dấu ngoặc đơn khiến nhiều người cảm thấy khó đọc hơn so với Python, Java hay C. Nhiều nỗ lực tạo ra ký pháp mới cho Lisp đã được thực hiện, nhưng vẫn chưa có lời giải thích thỏa đáng cho vấn đề này.

Một bài phân tích mới đây tiếp cận câu hỏi trên từ góc độ khoa học nhận thức, cho rằng lý do không chỉ nằm ở thói quen, mà còn ở cách bộ não con người xử lý thông tin thị giác và ghi nhớ ngữ cảnh.

Vấn đề không chỉ là sự quen thuộc

Lập trình viên Lisp thường lập luận rằng khó đọc chỉ là vấn đề làm quen: cú pháp Lisp ít phổ biến hơn cú pháp kiểu C, nên lập trình viên chưa quen. Tuy nhiên, tác giả bài viết cho rằng còn nhiều yếu tố sâu xa hơn.

Xét ví dụ hàm tính giai thừa đệ quy viết bằng Scheme và JavaScript:

(define (factorial n)
  (if (= n 1)
      n
      (* n (factorial (- n 1)))))
function factorial(n) {
  if (n == 1) {
    return n;
  } else {
    return (n * factorial(n - 1));
  }
}

Sự khác biệt về ký pháp trung tố (n == 1), từ khóa (else) hay các loại dấu phân cách khác nhau ({}) thường được nhắc đến nhiều nhất. Nhưng theo tác giả, tác động của chúng bị đánh giá quá cao. Vấn đề cốt lõi nằm ở cách bố trí dấu ngoặc trong lệnh gọi hàm thông thường.

Bộ não người có "tăng tốc phần cứng" riêng

Để hiểu vì sao vị trí dấu ngoặc lại quan trọng, cần nhìn vào cách nhận thức của con người. Hiệu ứng Stroop cho thấy bộ não xử lý thông tin không đồng đều — khi màu chữ và nghĩa của từ không khớp nhau, việc đọc trở nên chậm hơn rõ rệt.

Tương tự, trong một tập dữ liệu có điểm ngoại lai, nếu các điểm được tô màu hoặc định dạng khác biệt, mắt người có thể tìm ra chúng gần như tức thì thay vì phải quét tuần tự. Tác giả gọi đây là những dạng "tăng tốc phần cứng" bẩm sinh của hệ nhận thức.

Thiết kế tốt nên tận dụng những hiệu ứng này để giúp giao diện dễ tìm kiếm hơn, chẳng hạn qua hình dạng biểu tượng hoặc màu nhấn khác nhau.

Các hiệu ứng nhận thức này không hoàn toàn khách quan và cần sự sáng tạo khi áp dụng, nhưng chúng đóng vai trò như nguyên tắc thiết kế hữu ích.

Quy luật gần kề và khoảng cách đối số

Tâm lý học Gestalt mô tả nhiều cách con người có xu hướng nhóm các đối tượng lại với nhau, trong đó quy luật gần kề (law of proximity) là rõ ràng nhất: những gì gần nhau thì được coi là có liên quan.

Cú pháp kiểu C tận dụng quy luật này tốt hơn Lisp. Một lệnh gọi hàm chỉ có một đối số không cần dấu cách, nên toàn bộ lệnh gọi được nhìn như một khối thống nhất. Khi có nhiều đối số, dấu phẩy và khoảng trắng tạo ra sự phân tách rõ ràng giữa các đối số.

Ngược lại, trong Lisp, mọi hàm và đối số đều được ngăn cách bằng cùng một ký tự khoảng trắng. Khoảng cách không truyền tải thông tin gì về cách nhóm.

Khi làm mờ hai đoạn mã tương đương, đoạn kiểu C tạo thành ba khối mờ gợi ý cấu trúc tổng thể, trong khi đoạn Lisp tách thành bảy khối riêng biệt không cho thấy bất kỳ dấu hiệu nhóm nào.

Dấu ngoặc đóng/mở quá xa nhau

Hai quy luật Gestalt khác là quy luật khép kín (closure) và quy luật đối xứng (symmetry). Hai dấu ngoặc đơn tạo thành một hình ellipse hoàn chỉnh khi được nhìn cùng lúc, nhưng điều này đòi hỏi chúng phải nằm gần nhau trong tầm nhìn.

Thực tế, vùng võng mạc có độ phân giải cao nhất chỉ chiếm khoảng hai độ góc thị giác — tương đương đường kính khoảng 2,5 cm khi ngồi cách màn hình 70 cm. Ngoài vùng đó, mọi thứ đều mờ và bộ não phải "đoán" nội dung.

Vì Lisp đặt tên hàm ngay sau dấu ngoặc mở, khoảng cách giữa hai dấu ngoặc trở nên lớn hơn nhiều. Vấn đề càng nghiêm trọng khi Lisp thường dùng tên hàm dài như make-string-output-stream hay call-with-current-continuation.

So sánh hai cách viết sau:

(long-function-name argument)
long-function-name(argument)

Trong đoạn kiểu C, dấu ngoặc đóng nằm trong tầm nhìn khi mắt đang ở dấu ngoặc mở, nên có thể khớp cặp ngay lập tức. Với Lisp, lập trình viên phải ghi nhớ dấu ngoặc mở cho đến khi mắt di chuyển tới dấu ngoặc đóng.

Điều này cũng giải thích vì sao nhiều ngôn ngữ hiện đại chuộng ký pháp "trailing lambda" — ví dụ trong Kotlin:

repeat(5, { body })    // dạng thông thường
repeat(5) { body }     // dạng trailing lambda

Cách viết thứ hai đưa các dấu phân cách lại gần nhau hơn, và theo kinh nghiệm tác giả, nó được dùng phổ biến hơn hẳn.

Bộ nhớ làm việc và "ngăn xếp tinh thần"

Bộ nhớ làm việc của con người chỉ lưu được khoảng ba đến năm mục cùng lúc. Khi đọc một biểu thức lồng nhau sâu, ta phải ghi nhớ ngữ cảnh của từng tầng — không khác gì ngăn xếp trong bộ phân tích cú pháp.

Đây chính là điều khiến các biểu thức lồng sâu trở nên khó đọc: vượt quá bốn tầng lồng, ta buộc phải tốn thêm nỗ lực để theo dõi.

Cú pháp kiểu C có một lợi thế quan trọng ở đây. Với các lệnh gọi lồng nhau kiểu f(a)(b)(c), cấu trúc lồng được thể hiện qua tính kết hợp trái của cú pháp, không cần đến các dấu ngoặc lồng nhau. Điều này cũng xuất hiện phổ biến trong truy cập mảng: f[a][b][c].

So sánh hai cách viết tương đương:

(((((((function aap) noot) mies) wim) zus) jet) teun)
function(aap)(noot)(mies)(wim)(zus)(jet)(teun)

Rõ ràng dạng thứ hai dễ phân tích hơn hẳn, và việc "giải phóng" ngăn xếp tinh thần ở phần dễ đọc còn giúp xử lý phần còn lại của biểu thức nhanh hơn.

Thứ tự đọc không khớp thứ tự thực thi

Trường hợp ngược lại — lồng về bên phải — lại nảy sinh vấn đề mới. Xét biểu thức (h (g (f a))): biểu thức trong cùng (f a) được thực thi trước tiên nhưng lại được đọc sau cùng.

Khi thứ tự đọc không khớp thứ tự thực thi, lập trình viên buộc phải hoặc ghi nhớ các lệnh gọi bên ngoài, hoặc đọc ngược từ trong ra. Cách thứ nhất thất bại khi vượt quá giới hạn bộ nhớ, cách thứ hai khiến việc hiểu mã phải qua hai lượt: một lượt nắm cấu trúc, một lượt đọc theo đúng thứ tự thực thi.

Vấn đề này không tệ hơn ở kiểu C nếu so sánh các chương trình tương đương, nhưng lập trình viên C hiếm khi viết các cấu trúc lồng phải sâu như vậy, bởi phong cách lập trình mệnh lệnh tự nhiên khiến văn bản đi theo thứ tự thực thi.

Điều này cũng giải thích vì sao nhiều ngôn ngữ chính thống gần đây bổ sung cú pháp gọi phương thức (method chaining), và vì sao Clojure phổ biến các macro threading:

(->> (file->string "input.txt")
     (string-split "\n")
     (map string->number)
     (filter (fn [it] (> it 0)))
     (apply +))

Những yếu tố thường bị đánh giá quá cao

Tác giả cho rằng một số khác biệt cú pháp thường được nhắc đến lại ít quan trọng hơn tưởng tượng:

  • Thiếu ký pháp trung tố: chỉ thực sự tạo khác biệt trong các chương trình chủ yếu là số học đơn giản. Với lập trình hàm — nơi Lisp thiên về — con số xuất hiện tương đối ít.
  • Cú pháp riêng cho các thành phần nguyên thủy: việc if trong Python dễ đọc hơn Lisp không hẳn do if được đối xử đặc biệt, mà do cách đối xử đặc biệt đó vô tình dùng cú pháp dễ chịu hơn.
  • Điều thực sự quan trọng là các ngôn ngữ không phải Lisp có nhiều dạng ký pháp cho lệnh gọi hàm khác nhau. Chính sự đa dạng này tạo ra các "mốc thị giác" giúp mắt định vị trong mã nguồn.

Kết luận và bài học thiết kế

Tổng kết lại, tác giả nêu bốn lý do khiến Lisp có vẻ khó đọc hơn cú pháp kiểu C:

  • Ký pháp tiền tố đẩy các dấu ngoặc ra xa nhau
  • Thói quen định dạng không giúp theo dõi dấu ngoặc
  • Ký pháp tiền tố dẫn đến nhiều cấu trúc lồng trái hơn
  • Lisp thiếu hoặc không khuyến khích các tính năng giúp thứ tự đọc khớp thứ tự thực thi

Từ đó, tác giả đưa ra lời khuyên cho việc thiết kế ký pháp mới:

Sắp xếp ký pháp sao cho các token liên quan (như dấu phân cách) nằm gần nhau nhất có thể; dùng hình dạng mã để truyền tải cách nhóm; tận dụng tính kết hợp để tránh lồng dấu phân cách; thiết kế cú pháp và ngữ nghĩa sao cho thứ tự đọc khớp thứ tự thực thi.

Đối với lập trình viên Việt Nam đang cân nhắc học Lisp hoặc Clojure, phân tích này gợi ý rằng cảm giác khó đọc ban đầu là có cơ sở nhận thức chứ không chỉ là định kiến. Tuy nhiên, cú pháp đơn giản của Lisp cũng có lợi thế riêng: dễ học hơn cho người mới, và một số thuật toán có thể diễn đạt thanh thoát hơn hẳn trong phong cách hàm. Đánh đổi một dạng dễ đọc lấy những lợi ích về meta-programming là lựa chọn hoàn toàn hợp lý.

Bài viết gốc cũng tiết lộ tác giả đang phát triển một ngôn ngữ họ Lisp của riêng mình, với sự kết hợp cú pháp và ngữ nghĩa mà ông cho là chưa từng có tiền lệ.

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