1 điểm bởi GN⁺ 2024-10-18 | 1 bình luận | Chia sẻ qua WhatsApp
  • Bài viết năm 1983 của Douglas Hofstadter giới thiệu Lisp, ngôn ngữ được dùng rộng rãi trong nghiên cứu AI, như một ngôn ngữ thanh nhã và linh hoạt được xây dựng trên một hạt nhân toán học nhỏ
  • Tính tương tác của Lisp thể hiện ở read-eval-print loop: khi người dùng nhập một biểu thức, trình thông dịch đọc, đánh giá, in kết quả rồi chờ đầu vào tiếp theo
  • Đối tượng Lisp được tổ chức quanh nguyên tử (atom)danh sách (list); nil có vị trí ngoại lệ vì vừa là danh sách rỗng vừa là nguyên tử
  • quote, eval, car, cdr, cons, setq, set, lambda, def, cond cho phép xử lý mã và dữ liệu bằng cùng một cấu trúc danh sách, nhờ đó có thể dần dần tạo ra các hàm mới
  • Hợp thành hàm, biểu thức điều kiện, side effect và phong cách applicative, cùng tư duy đệ quy được kết nối qua các ví dụ; hàm power ở cuối và ẩn dụ “porpuquine” gợi ý kiểu đệ quy của Lisp

AI và vị trí của Lisp trong nghiên cứu

  • Vào thời điểm năm 1983, AI là lĩnh vực nghiên cứu nhằm hiện thực hóa trên máy tính các hành vi như tính linh hoạt, lẽ thường, sự thấu hiểu, tính sáng tạo, tự nhận thức và hài hước
  • Ở Mỹ có khoảng 2.000 người chuyên tham gia vào AI, và ở nước ngoài cũng có số nhà nghiên cứu với quy mô tương tự
  • Các nhà nghiên cứu AI có nhiều bất đồng lớn về con đường tốt nhất để đi tới AI, nhưng trong lựa chọn ngôn ngữ lập trình thì hầu như cùng dùng Lisp
  • Tên Lisp bắt nguồn từ “list processing”, nhưng không phải là một từ viết tắt hoàn chỉnh
  • Sức hấp dẫn của Lisp nằm ở chỗ nó là một ngôn ngữ “crisp” và “elegant”
    • Trong khi nhiều ngôn ngữ chứa rất nhiều tính năng tùy ý, một số ngôn ngữ như Lisp và Algol được tổ chức quanh một hạt nhân tự nhiên như một nhánh của toán học
    • Hạt nhân của Lisp có “crystalline purity”; sự thuần khiết này đóng góp không chỉ cho cảm quan thẩm mỹ mà còn cho tính linh hoạt

Từ logic toán học đến ngôn ngữ xử lý danh sách

  • Gốc rễ sâu xa của Lisp nằm trong logic toán học
    • Những ý tưởng mà Thoralf Skolem, Kurt Godel, Alonzo Church để lại trong logic học vào thập niên 1920–1930 về sau được đưa vào Lisp
  • Lập trình máy tính thực sự bắt đầu trong thập niên 1940, còn các ngôn ngữ bậc cao như Lisp xuất hiện trong thập niên 1950
  • Ngôn ngữ xử lý danh sách đầu tiên không phải Lisp mà là IPL (Information Processing Language)
    • IPL được Herbert Simon, Allen Newell, J. C. Shaw phát triển vào giữa thập niên 1950
  • John McCarthy đã tạo ra Lisp, một ngôn ngữ xử lý danh sách mang tính đại số, trong giai đoạn 1956–1958 dựa trên các ý tưởng sẵn có
    • Lisp nhanh chóng lan rộng trong các nhà nghiên cứu trẻ quanh MIT Artificial Intelligence Project
    • Nó được triển khai trên IBM 704 và lan sang các nhóm AI khác
    • Nhiều dialect xuất hiện, nhưng cùng chia sẻ một hạt nhân trung tâm thanh nhã

Lisp tương tác và REPL

  • Khác với nhiều ngôn ngữ bậc cao khác, Lisp được trình bày như một ngôn ngữ tương tác
  • Người dùng chạy Lisp trên terminal, thấy prompt rồi nhập biểu thức
    • Ví dụ: nhập (plus 2 2) thì 4 được in ra và prompt mới xuất hiện
  • Hoạt động cốt lõi của trình thông dịch Lisp là read-eval-print loop
    • Đọc biểu thức
    • Đánh giá biểu thức
    • In giá trị thích hợp
    • Báo rằng nó đã sẵn sàng đọc biểu thức tiếp theo
  • Nhờ cấu trúc này, người dùng có thể nhập từng “điều ước” và kiểm tra kết quả ngay lập tức
  • Trong nhiều ngôn ngữ không tương tác, phải viết trước toàn bộ chương trình rồi mới chạy, nên nhiều bước phải khớp hoàn hảo với nhau, việc sửa lỗi cũng gián tiếp và tốn kém
  • Lisp cho phép phát triển và gỡ lỗi tăng dần bằng cách chạy từng biểu thức một

Polish notation và dấu ngoặc

  • Biểu thức số học của Lisp dùng Polish notation, trong đó toán tử đứng trước toán hạng
    • Ví dụ: (times (plus 6 3) (difference 6 3)) được đánh giá thành 27
  • Cách ký hiệu này do nhà logic học Ba Lan Jan Lukasiewicz tạo ra trước thời máy tính
  • Biểu thức Lisp có rất nhiều dấu ngoặc
    • Các biểu thức dài thường kết thúc bằng nhiều dấu ngoặc đóng
    • Ban đầu có thể gây áp lực, nhưng khi quen rồi thì cấu trúc logic trở nên trực quan
  • Các ví dụ được trình bày dưới dạng pretty printing, dùng thụt lề để bộc lộ cấu trúc

Nguyên tử, danh sách, nil

  • Cốt lõi của Lisp là các cấu trúc có thể thao tác; mọi chương trình hoạt động bằng cách tạo, sửa đổi và phá hủy cấu trúc
  • Có hai loại cấu trúc
    • Nguyên tử (atom): đối tượng cơ bản không thể chia nhỏ hơn
    • Danh sách (list): cấu trúc gồm nhiều phần tử theo một thứ tự nhất định
  • Mọi đối tượng Lisp đều là nguyên tử hoặc danh sách, với đúng một ngoại lệ là nil
    • nil vừa là nguyên tử vừa là danh sách
    • nil biểu thị danh sách rỗng
    • ()nil có cùng nghĩa, nhưng nil được dùng thường xuyên hơn
  • Phần tử của danh sách có thể là nguyên tử hoặc cũng có thể là danh sách
    • Ví dụ: (zonk blee strill (croak flonk)) là danh sách có bốn phần tử, trong đó phần tử cuối lại là danh sách hai phần tử
  • Bản thân câu lệnh Lisp cũng là danh sách
    • (plus 2 2) cũng là một danh sách
    • Trình thông dịch Lisp có thể thao tác danh sách và nguyên tử để tạo rồi đánh giá các “lệnh” mới

Giá trị, quote, eval

  • Nguyên tử có thể có giá trị
    • Nguyên tử số có giá trị vĩnh viễn là chính nó
    • Giá trị của nil là nil
    • t cũng là một nguyên tử đặc biệt có giá trị vĩnh viễn là chính nó
  • setq gán giá trị cho nguyên tử
    • (setq pie 4) làm cho giá trị của pie thành 4
    • (setq pie (plus 2 2)) cũng làm cho giá trị của pie thành 4
  • Giá trị của nguyên tử không chỉ có thể là số mà có thể là bất kỳ đối tượng Lisp nào
    • Có thể là nguyên tử, cũng có thể là danh sách
  • quote chặn việc đánh giá và khiến danh sách hoặc nguyên tử được xử lý nguyên trạng như dữ liệu
    • (setq pie (plus 2 2)) đánh giá biểu thức bên trong rồi lưu 4 vào pie
    • (setq pi '(plus 2 2)) lưu chính danh sách (plus 2 2) vào pi
  • eval đánh giá lại biểu thức được lưu dưới dạng giá trị
    • Nếu giá trị của pi(plus 2 2), thì (eval pi) trả về 4
    • Trình thông dịch thường chỉ đánh giá một bước, nhưng dùng eval có thể yêu cầu đánh giá bổ sung

car, cdr, cons

  • Mọi danh sách không phải nil đều có ít nhất một phần tử, và phần tử đầu được gọi là car
    • car của (eval pi)eval
    • car của (plus 2 2)plus
  • car không nhất thiết phải là tên hàm
    • car của ((1)(2 2) (3 3 3))(1)
  • Danh sách còn lại sau khi bỏ car là cdr
    • cdr của (a b c d)(b c d)
    • Tiếp tục giảm thành (c d), (d), rồi nil
  • nil không có car cũng không có cdr; nếu cố lấy car hoặc cdr của nil thì phải xảy ra lỗi
  • cons tạo danh sách mới bằng cách gắn một car mới vào trước danh sách hiện có
    • Khi x(cake cookie), (cons 'pie x) tạo ra (pie cake cookie)
    • Lúc này x hiện có không bị thay đổi
  • Kết quả thay đổi tùy theo có quote hay không
    • Nếu giá trị của pie là 4, (cons pie x) trả về (4 cake cookie)
    • (cons 'pie x) gắn chính tên nguyên tử pie vào đầu

Khác biệt giữa setq và set

  • Trình thông dịch Lisp không phải lúc nào cũng đánh giá đối số, và setq là ngoại lệ tiêu biểu
  • Đối số thứ nhất của setq không được đánh giá mà được xử lý nguyên trạng như tên biến
    • Chữ q trong setq nghĩa là quote, và đối số thứ nhất được xử lý như thể đã được quote
  • set tương tự setq nhưng cũng đánh giá đối số thứ nhất
    • Khi giá trị của x là k, (set x 7) không đổi x mà đổi giá trị của k thành 7
  • Ví dụ đưa ra thứ tự sau
    • (setq a 'b)
    • (setq b 'c)
    • (setq c 'a)
    • (set a c)
    • (set c b)
  • Sau khi thực thi, giá trị của a, b, c lần lượt đều trở thành a
  • set không được dùng thường xuyên, và kiểu nhầm lẫn này cũng không hay xảy ra

Định nghĩa hàm và lambda

  • Sức mạnh lớn của lập trình nằm ở khả năng định nghĩa các phép toán phức hợp mới từ những phép toán sẵn có, rồi lặp lại việc đó để tích lũy một repertoire phép toán phức tạp hơn
  • Trong Lisp, ta định nghĩa hàm mới bằng các hàm đã biết
  • rac là ví dụ về hàm trả về phần tử cuối của danh sách
    • Muốn lấy phần tử cuối, có thể đảo ngược danh sách rồi lấy car
    • Định nghĩa: (def rac (lambda (lyst) (car (reverse lyst))))
  • (lyst) sau lambda biểu thị tham số của hàm, tức biến giả
  • Sau khi định nghĩa bằng def, rac trở thành một hàm có thể dùng như car
    • (rac '(your brains)) trả về brains
  • Hàm readers-digest-condensed-version tạo một danh sách ngắn chỉ chứa phần tử đầu và phần tử cuối từ một danh sách dài
    • Có ví dụ: nếu coi toàn bộ Finnegans Wake của James Joyce là một danh sách từ, ta thu được (riverrun the)
  • Phép ngược tên rejoyce là một hàm tưởng tượng tạo ra một tiểu thuyết có phần đầu và phần cuối được xác định bằng hai từ; việc viết hàm này được để lại làm bài tập cho độc giả

Giá trị trả về, side effect, phong cách applicative

  • Một số người cho rằng với Lisp và các ngôn ngữ liên quan, hai mục tiêu sau là đáng mong muốn và khả thi
    • Mọi câu lệnh đều trả về một giá trị
    • Tác dụng của câu lệnh chỉ xảy ra thông qua giá trị trả về
  • Các dialect Lisp được đề cập trong bài đáp ứng điều kiện thứ nhất, nhưng không nhất thiết đáp ứng cả điều kiện thứ hai
  • (reverse x) không thay đổi bản thân x, mà tạo và trả về một danh sách mới giống x nhưng có thứ tự đảo ngược
    • Cùng cách như (plus 2 2) không làm thay đổi giá trị của 2
  • cons cũng không thay đổi danh sách hiện có mà trả về một danh sách mới
  • Ngược lại, setq là ví dụ về lệnh để lại side effect bằng cách thay đổi binding của biến
    • Side effect ngoài thay đổi binding biến còn có thể bao gồm cả nhập/xuất
  • Những người ủng hộ lập trình applicative chỉ ưa cách các hàm tính toán và truyền giá trị mà không có side effect
  • Trong phong cách này, binding được phép là lambda binding, chỉ tạm thời phát sinh trong lúc gọi hàm
    • Khi việc tính toán của hàm kết thúc, binding của biến giả biến mất
  • Người kể trong bài xem phong cách applicative là thanh nhã, nhưng cho rằng nó không thực tế khi xây dựng các chương trình lớn theo phong cách AI
  • Trong lập trình applicative nghiêm ngặt, ngay cả def cũng trở thành một trường hợp cực đoan không được phép, vì nó lưu định nghĩa hàm vĩnh viễn vào bộ nhớ

Biểu thức điều kiện cond và phán đoán

  • Để Lisp làm được những việc thú vị hơn, nó phải có khả năng ra quyết định tùy theo điều xảy ra trong quá trình trung gian, và lúc này cần biểu thức điều kiện
  • Ví dụ: (cond ((eq x 1) 'land) ((eq x 2) 'sea))
    • Nếu x là 1 thì land
    • Nếu x là 2 thì sea
    • Các trường hợp khác trả về nil
  • eq là hàm Lisp trả về t nếu giá trị của hai đối số bằng nhau, và trả về nil nếu khác nhau
  • Câu lệnh cond bắt đầu bằng tên hàm cond và có nhiều mệnh đề
    • Mỗi mệnh đề là một danh sách 2 phần tử gồm điều kiện và kết quả
    • Nó kiểm tra các điều kiện theo thứ tự; khi tìm thấy điều kiện đầu tiên trả về giá trị không phải nil, nó đánh giá kết quả tương ứng và trả về làm giá trị của toàn bộ cond
    • Các mệnh đề sau đó không được kiểm tra
  • Nếu đặt một mệnh đề catch-all có điều kiện là t ở cuối, có thể trả về kết quả mặc định thay vì nil
    • Ví dụ có một cond trả về air nếu cả điều kiện land và sea đều thất bại

Lũy thừa và cấu trúc đệ quy

  • Cuối bài xuất hiện các định nghĩa hàm có mẫu rất rõ
    • squarek * k
    • cubek * square(k)
    • 4th-powerk * cube(k)
    • Cứ tiếp tục theo cùng cách như vậy
  • Câu hỏi đặt ra là liệu có thể định nghĩa một hàm hai tham số power bao quát toàn bộ mẫu này trong một lần hay không
    • (power 9 3) là 729
    • (power 7 4) phải trả về 2.401
  • Các công cụ cần thiết đều đã được trình bày trong phần nội dung, và điều còn lại đòi hỏi sự nhanh trí của độc giả
  • Câu chuyện về Glazunkian porpuquine ở cuối xử lý cấu trúc đệ quy như một ngụ ngôn
    • Gai của porpuquine là các porpuquine nhỏ hơn
    • Ở Outer Glazunkia luôn có 9 chiếc gai, ở Inner Glazunkia luôn có 7 chiếc gai
    • Porpuquine 0 inch không có gai, qua đó chặn sự thoái lui vô hạn
  • “buying power” hay “power” của con vật này liên quan đến số mũi của các porpuquine 0 inch nhỏ bên trong nó, và bài kết thúc bằng ẩn dụ nối tiếp với bài toán power đã nêu trước đó

Bối cảnh tái đăng nguyên văn

  • Phần đầu Gist cho biết đây là bài giới thiệu Lisp của Douglas Hofstadter mà người đăng gặp trong một số cũ của Scientific American vào giữa thập niên 1980
  • Các ví dụ vẫn có thể chạy trong Emacs nếu cài một số alias
    • plus+
    • quotient/
    • times*
    • difference-
  • Phần cuối cho biết nếu thích phần hướng dẫn Lisp độc đáo của Hofstadter, có thể đọc thêm các bài tương tự trong sách Metamagical Themas của ông

1 bình luận

 
GN⁺ 2024-10-18
Bình luận trên Hacker News
  • Tôi đã bị rối bởi tên hàm "oval""snot" trong ví dụ, rồi vài giây sau mới nhận ra đó lần lượt là lỗi đánh máy của "eval""snoc"
    Mã gốc lẽ ra phải được đọc là (cond ((eq (eval pi) pie) (eval (snoc pie pi))) (t (eval (snoc (rac pi) pi))))
    Tôi đã tải bài Scientific American gốc từ https://www.jstor.org/stable/24968822 để kiểm tra, và quả thật oval/snot là lỗi đánh máy của eval/snoc

    • Đính chính cho phần đính chính: "oval""snot" đúng là lỗi đánh máy của "eval""snoc"
      snoc có vẻ là cons viết ngược, còn raccar viết ngược
    • Cũng có thể đây là lỗi OCR
  • Bài này một lần nữa cho thấy nguyên nhân cốt lõi khiến Lisp không được phổ biến là cách người ta giải thích về Lisp
    Những bài viết kiểu này về Lisp khi tôi còn nhỏ thực sự không dạy cho tôi bất cứ thứ gì có thể dùng được, cũng không cho thấy việc X nào đó sẽ trở nên dễ hơn bao nhiêu so với assembly/C/Pascal
    Nếu là tôi bây giờ, tôi sẽ cho thấy kiểu như: “Một trình kiểm tra chính tả mất 7 tháng để viết bằng assembly? Với Lisp trên một máy vi tính dùng bộ nhớ chuyển trang ngân hàng thì đó gần như là việc vặt, và thậm chí còn có thể làm cho garbage collection hoạt động được trên một CPU tệ hại”
    Quá nhiều bài viết và giáo trình về Lisp chỉ lặp đi lặp lại chuyện danh sách, đệ quy và AI, mà không chỉ ra cách làm những việc hữu ích, khiến tôi cảm thấy đã lãng phí thời gian như thể đang lập trình bằng nhíp, hạt gạo và hồ dán

    • Common Lisp: A Gentle Introduction to Symbolic Computation có thể hữu ích trong bối cảnh này: https://www.cs.cmu.edu/~dst/LispBook/
    • Cũng có Practical Common Lisp: https://gigamonkeys.com/book/
    • Các bài viết tiếp theo của Hofstadter có những ví dụ thú vị hơn
      Trong số đầu tiên của tạp chí Byte mà tôi đọc khi còn nhỏ có mã viết phép vi phân ký hiệu và rút gọn đại số bằng Lisp; dù khó theo kịp, tôi vẫn cảm nhận được có gì đó rất cuốn hút
      Chắc chắn nó sẽ không dễ hơn nếu viết bằng Basic, và về sau nhìn lại thì đoạn mã đó cũng không hẳn xuất sắc, nhưng phải đến khi gặp XLisp trên PC cuối thập niên 80 cùng với SICP tôi mới thực sự bị Lisp mê hoặc
  • Tôi thực sự rất thích văn phong của Hofstadter. Ông ấy nắm bắt rất tốt cảm giác khi khám phá ra Lisp
    Tôi là một đứa trẻ học lập trình trong thập niên 80, và đến đầu thời trung học phổ thông rồi đại học thì đã biết sơ qua BASIC, Fortran, Pascal, COBOL; chúng có khác nhau, nhưng về cơ bản vẫn có điểm chung
    Thế rồi môn khoa học máy tính đầu tiên ở UC Berkeley lại dùng Scheme, một phương ngữ của Lisp, và đó là một cú sốc hoàn toàn
    Như Hofstadter nói, nó cho cảm giác gần với toán học nhất, gợi nhớ rất nhiều đến các lớp lý thuyết toán, và là ngôn ngữ đầu tiên tôi thấy thực sự đẹp
    Tôi đặc biệt thích câu trích dẫn: “Lisp và Algol được tạo ra quanh một hạt nhân tự nhiên như thể thuộc về một nhánh của toán học. Ở trung tâm của Lisp có một sự thuần khiết kết tinh, không chỉ chạm đến cảm quan thẩm mỹ mà còn khiến nó linh hoạt hơn rất nhiều so với hầu hết các ngôn ngữ khác”

    • Không biết bạn đã dùng Haskell chưa. Với tôi, nó còn gần với toán học hơn nhiều vì nó được tạo thành từ các định nghĩa chứ không phải thủ tục, và bề ngoài cũng trông giống toán học
    • Có chút nguy cơ đi chệch khỏi bài gốc, nhưng tôi nghĩ gọi đây là “toán học” có khi lại làm mờ vấn đề
      Ngay cả từ góc nhìn của một người từng muốn trở thành nhà tô pô đại số, Scheme gần hơn với việc biểu đạt một cách thanh nhã và tối giản phong cách lập trình đã lan rộng trong các ngôn ngữ dùng kiểu động và garbage collection
      Nó cho cảm giác như một “lý thuyết”, theo nghĩa là nó có vẻ hoàn chỉnh, và nếu nghĩ ra cách giải quyết vấn đề trong Scheme rồi chuyển sang ngôn ngữ kiểu động khác thì vẫn có thể đi đến một lời giải thanh nhã
      So với Lisp truyền thống thời đó, Scheme là một ngôn ngữ đã được đơn giản hóa và sắp xếp lại ở các mặt như lexical scope, không gian tên thống nhất cho hàm và biến
  • Năm 1983 là thời điểm khoảng 20 năm sau khi lĩnh vực này bắt đầu, vậy mà người ta đã nói về AI có thể lập trình máy tính để có tính linh hoạt, lẽ thường, trực giác, sáng tạo, tự nhận thức, hài hước
    Khá thú vị khi danh sách này nghe như đúng là danh sách những thứ mà LLM thực sự làm không tốt
    Dù vậy, ít nhất cũng đã có một chút tiến bộ khác 0 theo hướng đó

    • Nếu so với những người bình thường tôi gặp, thì LLM có vẻ đã thể hiện lẽ thường, trực giác, sáng tạo, tự nhận thức, hài hước ít nhất ngang mức người trung bình
      Có lẽ còn hơn thế, nhưng nói ra vậy chính tôi cũng thấy hơi kỳ
    • Nhóm nghiên cứu của Hofstadter có một lịch sử dài trong việc cố gắng xử lý những vấn đề này
      Dù phần lớn vẫn chưa dẫn đến kết quả cụ thể, đây vẫn là những thứ rất đáng đọc
  • Hai bài đồng hành được nhắc đến cùng bài này nằm trong các chương 17~19 của cuốn Metamagical Themas của Hofstadter, cùng với các bài khác từ chính loạt đăng trên Scientific American
    [0]: https://www.goodreads.com/book/show/181239.Metamagical_Thema...
    Tên sách cũng là tiêu đề của loạt bài, được tạo thành từ một phép đảo chữ của “Mathematical Games”, chuyên mục mà Martin Gardner từng viết trên Scientific American, sau đó Hofstadter tiếp quản

    • Tôi cực kỳ thích cuốn này. Rất khuyến nghị cho những ai yêu thích tác phẩm kinh điển của Hofstadter là Gödel, Escher, Bach
    • Cuốn sách này đẹp cả khi đọc lẫn khi ngắm, và có rất nhiều minh họa thú vị
  • Bài viết nói rằng áp dụng car hoặc cdr lên nil thì đáng lẽ phải gây lỗi như chia cho 0, nhưng trong Lisp hiện đại nhiều khi không còn như vậy nữa
    Trong Lisp nguyên bản do John McCarthy định nghĩa, CARCDR không được định nghĩa cho NIL: <https://dl.acm.org/doi/pdf/10.1145/367177.367199>
    Nhưng Common Lisp và Emacs Lisp định nghĩa (car nil)(cdr nil)nil: <https://www.lispworks.com/documentation/HyperSpec/Body/f_car...>, <https://www.gnu.org/software/emacs/manual/html_node/elisp/Li...>

    • Tò mò không biết Maclisp thì thế nào, nên tôi đã telnet vào ITS công khai của Lars Brinkhoff để kiểm tra
      Trong LISP 2156, (status lispversion) trả về /2156, còn (car nil)(cdr nil) đều trả về NIL
    • Thật đáng ngạc nhiên khi trong sự ra đời của một ngôn ngữ được thiết kế theo tinh thần toán học, những chi tiết triển khai rất cụ thể như carcdr lại nằm ở vị trí cốt lõi
      Phép toán cơ bản và nổi tiếng nhất của “List Processor” thực ra không hoạt động trên danh sách, mà được tạo ra để tác động lên cons, tức một phần tử biểu diễn máy cụ thể mà Lisp dùng để tạo cấu trúc dữ liệu
      cons cũng không phải lúc nào cũng được diễn giải là danh sách, và danh sách rất quan trọng trong trường hợp cơ sở của hàm đệ quy trên danh sách thì lại không được biểu diễn bằng cons
      Sau 60 năm, phần lớn chương trình Lisp vẫn đầy các phép toán cons, nên có lẽ tên chính xác hơn phải là “Cons Processor”
      Điều này gợi nhớ rằng Lisp ra đời trong thời kỳ ngôn ngữ và triển khai phải gắn chặt với nhau, nên thành tựu xây dựng ngôn ngữ máy tính trên nền tảng logic toán học lại càng thấy ấn tượng hơn
    • Scheme thì không như vậy. Lấy CAR hoặc CDR của nil sẽ gây lỗi
    • Đáng tiếc là trong Scheme, car/cdr của nil là lỗi, nên với người mới như tôi thì mã nguồn có cảm giác khá thiếu công thái học
      Dù vậy, tôi vẫn thích Guile Scheme hơn Common Lisp nên cũng thấy hơi tiếc
    • Tôi tự hỏi có thuật ngữ nào để gọi lựa chọn thiết kế ngôn ngữ mà khi áp dụng gì đó lên nil thì không báo lỗi mà lại trả về nil hay không
      Nó làm tôi nghĩ đến SQL, và cá nhân tôi xem đây là một lựa chọn không tốt, nên nếu chưa có tên nào khác thì tôi muốn gọi nó là “bleeding nils/NULLs”
      Đặc biệt còn tệ hơn nếu nil trong so sánh boolean lại không tương đương với false
      Trong Ruby và Elixir, nil được xử lý như false, còn Elixir tách riêng and chỉ nhận boolean thuần với && coi nilfalse
      Kiểu thiết kế này lúc đầu có thể làm mã trông gọn gàng hơn, nhưng một nil bị xử lý sai vốn là lỗi thật sự có thể chỉ lộ ra ở một phần hoàn toàn khác sau nhiều tầng call stack, khiến việc debug khó hơn rất nhiều
  • Tôi vốn đã biết những thông tin về Lisp trong bài này, nhưng đọc vẫn thấy rất thú vị. Hofstadter có lối hành văn thật cuốn hút
    Đặc biệt thú vị là trò đùa ông tạo ra rejoyce, phép toán ngược của readers-digest-condensed-version, sao cho khi chạy (rejoyce 'Stately 'Yes) thì một nàng tiên Lisp sẽ tạo ra toàn bộ Ulysses như thể do James Joyce viết ngay từ đầu
    Mất khá nhiều thời gian, nhưng rốt cuộc chúng ta cũng đã chạm đến điểm đó; và dù AI của năm 2024 không hoàn toàn giống điều ông tưởng tượng vào năm 1983, việc tái tạo văn bản từ một seed ngắn quả thật là công việc mà AI ngày nay làm khá hợp

  • Lisp vẫn được xem là một trong những ngôn ngữ chủ chốt có thể chứa đựng khái niệm vòng lặp kỳ lạ mà Hofstadter bàn đến
    Lisp không phải là ngôn ngữ duy nhất có tính homoiconic, nhưng trong số các ngôn ngữ mà người ta thực sự biết cách dùng, nó thuộc nhóm lớn nhất mà eval không nhận chuỗi cần phải được phân tích cú pháp
    Tôi không thích việc mọi người đồng nhất Lisp với toàn bộ lập trình hàm
    Không phải tôi ghét lập trình hàm, nhưng tính biểu tượng của Lisp thú vị hơn nhiều, và việc có thể tạo ra mã được vận hành bằng các đoạn (go tag) khiến cả lập trình kiểu GOTO cũng trở nên cực kỳ dễ dàng, điều đó hấp dẫn mãi không thôi

    • Còn có một ngôn ngữ homoiconic khác, mang tính hàm đúng nghĩa và từng được chấp nhận trong dòng chính một thời gian ngắn vào những năm 2000: XSLT
      Khả năng metaprogramming kiểu dùng XSLT và XML để sinh ra XSLT thay vì tự viết mã lặp đi lặp lại đã từng được dùng khá rộng rãi
      Tuy nhiên, cú pháp của nó còn là vấn đề lớn hơn cả cú pháp Lisp
      Không dễ để tạo ra một ngôn ngữ có cú pháp thuận tiện cho việc dùng hằng ngày mà đồng thời cũng không quá khó để xử lý như một cây cú pháp trừu tượng, và Lisp là một trong số rất ít ví dụ thành công tương đối
    • Đặc biệt, nếu đi đến lập trình dựa trên continuation của Scheme thì ranh giới giữa lập trình hàm và phi hàm gần như mờ đi đến mức không còn nhiều ý nghĩa
    • Tôi tò mò không biết ở mức sâu, điều mà một lập trình viên năm 2024 thực sự nhận được từ hỗ trợ homoiconic/biểu tượng trong Lisp — thứ mà Python với functools khó làm được — là gì: https://docs.python.org/3/library/functools.html
      Tôi đang xây dựng AGI biểu tượng mà không dùng Lisp, nên muốn nghe những gợi ý từ các chuyên gia
      Theo hiểu biết của tôi, phía Python có các hàm áp dụng hàm khác lên iterable như filter(), map(), reduce(), các wrapper định tuyến lời gọi như @singledispatch, các hàm hỗ trợ điều khiển luồng hoặc tiện ích hiệu năng như @cachepartial(), cùng các tính năng bọc hàm tùy ý như wraps()
      Phần lớn những thứ này trông giống như các tiện ích để gọi hàm theo những cách khác thường, và dường như chưa đạt đến mức được ca ngợi trong Lisp, nơi “tự phản chiếu” được xem như mối quan tâm hạng nhất
      Tôi muốn biết chính xác Lisp thực sự cung cấp điều gì mà các tính năng trên không có
    • Cũng bởi vì nhiều người chỉ tiếp xúc với Lisp thông qua giới học thuật, và các học giả dạy Lisp thì lại không mấy quan tâm đến việc thực sự phát triển thứ gì đó bằng Lisp
      Họ dùng Lisp như một phương tiện để truyền đạt khái niệm, và các khái niệm đó thường xoay quanh đệ quy hàm
      Scheme và văn hóa xung quanh nó cũng góp phần vào điều này
      Dù Scheme không chỉ là ngôn ngữ hàm, nó nhấn mạnh lập trình thuần túy hơn các họ Lisp trước đó, ngôn ngữ lõi cung cấp cấu trúc đệ quy đuôi thay cho cấu trúc lặp, và yêu cầu các implementation tối ưu hóa lời gọi đuôi
  • Ngay cả trong Common Lisp, cũng có thể định nghĩa defalias bằng macro để dùng giống như defalias của Emacs: https://stackoverflow.com/questions/24252539/defining-aliase...

  • Tôi rất thích bài viết về Lisp của Hofstadter đăng trên Metamagical Themas, và đã chuyển đổi, sắp xếp lại phần mã trong bài cuối của loạt đó cho phù hợp với Clojure để dùng cho nhóm học tập ở chỗ làm
    [1] http://johnj.com/posts/oodles/

    • Tôi cũng tò mò không biết có bản chuyển ngữ nào sang một Lisp hiện đại hay không