Lời giải thích của Matt hoàn toàn tốt - và anh ấy đã chụp một bức ảnh so sánh với C và Java, điều mà tôi sẽ không làm - nhưng vì một số lý do tôi thực sự thích thảo luận về chính chủ đề này một lần, vì vậy - đây là cú đánh của tôi tại một câu trả lời
Trên các điểm (3) và (4):
Điểm (3) và (4) trong danh sách của bạn có vẻ thú vị nhất và vẫn có liên quan.
Để hiểu chúng, thật hữu ích khi có một bức tranh rõ ràng về những gì xảy ra với mã Lisp - dưới dạng một dòng các ký tự được lập trình viên nhập vào - trên đường được thực thi. Hãy sử dụng một ví dụ cụ thể:
;; a library import for completeness,
;; we won't concern ourselves with it
(require '[clojure.contrib.string :as str])
;; this is the interesting bit:
(println (str/replace-re #"\d+" "FOO" "a123b4c56"))
Đoạn mã Clojure này in ra aFOObFOOcFOO
. Lưu ý rằng Clojure được cho là không đáp ứng đầy đủ điểm thứ tư trong danh sách của bạn, vì thời gian đọc không thực sự mở đối với mã người dùng; Tôi sẽ thảo luận về ý nghĩa của việc này nếu không.
Vì vậy, giả sử chúng ta đã có mã này trong một tệp ở đâu đó và chúng tôi yêu cầu Clojure thực thi nó. Ngoài ra, hãy giả sử (vì mục đích đơn giản) rằng chúng tôi đã vượt qua quá trình nhập thư viện. Các bit thú vị bắt đầu ở (println
và kết thúc ở phía )
xa bên phải. Điều này được lexed / phân tích như mọi người mong đợi, nhưng đã có một điểm quan trọng phát sinh: kết quả không phải là một đại diện AST đặc biệt cho trình biên dịch - nó chỉ là một cấu trúc dữ liệu Clojure / Lisp thông thường , cụ thể là một danh sách lồng nhau chứa một loạt các ký hiệu, chuỗi và - trong trường hợp này - một đối tượng mẫu biểu thức chính được biên dịch tương ứng với#"\d+"
nghĩa đen (nhiều hơn về điều này dưới đây). Một số Lisps thêm các vòng xoắn nhỏ của riêng họ vào quá trình này, nhưng Paul Graham chủ yếu đề cập đến Common Lisp. Về những điểm liên quan đến câu hỏi của bạn, Clojure tương tự như CL.
Toàn bộ ngôn ngữ tại thời gian biên dịch:
Sau thời điểm này, tất cả các trình biên dịch xử lý (điều này cũng đúng với trình thông dịch Lisp; mã Clojure luôn luôn được biên dịch) là các cấu trúc dữ liệu Lisp mà các lập trình viên Lisp sử dụng để thao tác. Tại thời điểm này, một khả năng tuyệt vời trở nên rõ ràng: tại sao không cho phép các lập trình viên Lisp viết các hàm Lisp thao tác dữ liệu Lisp đại diện cho các chương trình Lisp và dữ liệu chuyển đổi đầu ra đại diện cho các chương trình được chuyển đổi, được sử dụng thay cho các bản gốc? Nói cách khác - tại sao không cho phép các lập trình viên Lisp đăng ký các chức năng của họ dưới dạng các trình biên dịch các loại, được gọi là macro trong Lisp? Và thực sự bất kỳ hệ thống Lisp phong nha đều có khả năng này.
Vì vậy, macro là các hàm Lisp thông thường hoạt động trên biểu diễn của chương trình tại thời điểm biên dịch, trước giai đoạn biên dịch cuối cùng khi mã đối tượng thực tế được phát ra. Vì không có giới hạn đối với các loại macro mã được phép chạy (cụ thể, mã mà chúng chạy thường được viết bằng cách sử dụng tự do của cơ sở macro), nên có thể nói rằng "toàn bộ ngôn ngữ có sẵn tại thời gian biên dịch ".
Toàn bộ ngôn ngữ tại thời điểm đọc:
Chúng ta hãy quay trở lại #"\d+"
regex theo nghĩa đen. Như đã đề cập ở trên, điều này được chuyển đổi thành một đối tượng mẫu được biên dịch thực tế tại thời điểm đọc, trước khi trình biên dịch nghe đề cập đầu tiên về mã mới được chuẩn bị để biên dịch. Làm thế nào điều này xảy ra?
Chà, cách Clojure hiện đang được thực hiện, bức tranh có phần khác so với những gì Paul Graham đã nghĩ, mặc dù mọi thứ đều có thể với một bản hack thông minh . Trong Common Lisp, câu chuyện sẽ nhẹ nhàng hơn về mặt khái niệm. Tuy nhiên, những điều cơ bản là tương tự nhau: Lisp Reader là một máy trạng thái, ngoài việc thực hiện chuyển đổi trạng thái và cuối cùng tuyên bố liệu nó có đạt đến "trạng thái chấp nhận" hay không, loại bỏ cấu trúc dữ liệu Lisp mà các ký tự đại diện. Do đó, các ký tự 123
trở thành số, 123
v.v. Điểm quan trọng hiện nay: máy trạng thái này có thể được sửa đổi bằng mã người dùng. (Như đã lưu ý trước đó, điều đó hoàn toàn đúng trong trường hợp của CL; đối với Clojure, một vụ hack (không được khuyến khích và không được sử dụng trong thực tế) là bắt buộc. Nhưng tôi lạc đề, đó là bài viết của PG mà tôi dự định sẽ xây dựng, vì vậy ...)
Vì vậy, nếu bạn là một lập trình viên Lisp thông thường và bạn thích ý tưởng về văn học vectơ kiểu Clojure, bạn có thể cắm vào đầu đọc một hàm để phản ứng thích hợp với một chuỗi ký tự - [
hoặc #[
có thể - và coi nó như là sự bắt đầu của một kết thúc bằng chữ vector ở kết hợp ]
. Một hàm như vậy được gọi là macro đọc và giống như macro thông thường, nó có thể thực thi bất kỳ loại mã Lisp nào, bao gồm cả mã được viết bằng ký hiệu vui nhộn được kích hoạt bởi các macro đọc đã đăng ký trước đó. Vì vậy, có toàn bộ ngôn ngữ tại thời gian đọc cho bạn.
Gói lại:
Trên thực tế, những gì đã được chứng minh cho đến nay là người ta có thể chạy các hàm Lisp thông thường vào thời gian đọc hoặc thời gian biên dịch; một bước người ta cần thực hiện từ đây để hiểu cách đọc và biên dịch có thể tự đọc, biên dịch hoặc chạy thời gian là nhận ra rằng việc đọc và biên dịch được thực hiện bởi các hàm Lisp. Bạn chỉ có thể gọi read
hoặc eval
bất cứ lúc nào để đọc dữ liệu Lisp từ các luồng ký tự hoặc biên dịch và thực thi mã Lisp, tương ứng. Đó là toàn bộ ngôn ngữ ngay tại đó, mọi lúc.
Lưu ý rằng việc Lisp thỏa mãn điểm (3) trong danh sách của bạn như thế nào là điều cần thiết cho cách thức quản lý để thỏa mãn điểm (4) - hương vị đặc biệt của macro được cung cấp bởi Lisp phụ thuộc rất nhiều vào mã được biểu thị bằng dữ liệu Lisp thông thường, đó là một cái gì đó được kích hoạt bởi (3). Ngẫu nhiên, chỉ có khía cạnh "cây-ish" của mã là thực sự quan trọng ở đây - bạn có thể hình dung được một Lisp được viết bằng XML.