Vui lòng giải thích một số quan điểm của Paul Graham về Lisp


146

Tôi cần một số trợ giúp để hiểu một số điểm từ What Made Lisp Dif của Paul Graham .

  1. Một khái niệm mới về các biến. Trong Lisp, tất cả các biến là con trỏ hiệu quả. Các giá trị là những gì có kiểu, không phải biến và các biến gán hoặc ràng buộc có nghĩa là sao chép con trỏ, không phải là thứ chúng trỏ đến.

  2. Một loại biểu tượng. Các biểu tượng khác với các chuỗi ở chỗ bạn có thể kiểm tra sự bằng nhau bằng cách so sánh một con trỏ.

  3. Một ký hiệu cho mã sử dụng cây biểu tượng.

  4. Toàn bộ ngôn ngữ luôn có sẵn. Không có sự phân biệt thực sự giữa thời gian đọc, thời gian biên dịch và thời gian chạy. Bạn có thể biên dịch hoặc chạy mã trong khi đọc, đọc hoặc chạy mã trong khi biên dịch và đọc hoặc biên dịch mã khi chạy.

Những điểm này có ý nghĩa gì? Chúng khác nhau như thế nào trong các ngôn ngữ như C hoặc Java? Có ngôn ngữ nào khác ngoài ngôn ngữ gia đình Lisp có bất kỳ cấu trúc nào trong số này không?


10
Tôi không chắc chắn rằng thẻ lập trình chức năng được bảo hành ở đây, vì có thể viết mã OO hoặc mã OO trong nhiều Lisps như nhau để viết mã chức năng - và trên thực tế có rất nhiều Lisp không chức năng mã xung quanh. Tôi khuyên bạn nên xóa thẻ fp và thêm clojure - hy vọng điều này có thể mang lại một số đầu vào thú vị từ Lispers dựa trên JVM.
Michał Marc:

58
Chúng tôi cũng có một paul-grahamthẻ ở đây? !!! Tuyệt vời ...
missingfaktor

@missingfaktor Có lẽ nó cần một yêu cầu ghi lại
con mèo

Câu trả lời:


98

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 ở (printlnvà 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ự 123trở thành số, 123v.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 readhoặc evalbấ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.


4
Cẩn thận: bằng cách nói "macro (trình biên dịch) thông thường", bạn gần như ngụ ý rằng các macro trình biên dịch là các macro "thông thường", khi trong Common Lisp (ít nhất), "macro trình biên dịch" là một thứ rất cụ thể và khác biệt: lispworks. com / tài liệu / lw51 / CLHS / Thân thể / Từ
Ken

Ken: Bắt tốt, cảm ơn! Tôi sẽ thay đổi điều đó thành "macro thông thường", điều mà tôi nghĩ là không thể vượt qua bất kỳ ai.
Michał Marc:

Câu trả lời tuyệt vời. Tôi đã học được nhiều hơn từ nó trong 5 phút so với tôi có hàng giờ để googling / suy nghĩ về câu hỏi. Cảm ơn.
Charlie Hoa

Chỉnh sửa: argh, hiểu nhầm một câu chạy. Đã sửa lỗi ngữ pháp (cần một "đồng đẳng" để chấp nhận chỉnh sửa của tôi).
Tatiana Racheva

Biểu thức S và XML có thể ra lệnh cho các cấu trúc tương tự nhưng XML dài hơn nhiều và do đó không phù hợp như cú pháp.
Sylwester

66

1) Một khái niệm mới về các biến. Trong Lisp, tất cả các biến là con trỏ hiệu quả. Các giá trị là những gì có kiểu, không phải biến và các biến gán hoặc ràng buộc có nghĩa là sao chép con trỏ, không phải là thứ chúng trỏ đến.

(defun print-twice (it)
  (print it)
  (print it))

"Nó" là một biến. Nó có thể được ràng buộc với bất kỳ giá trị nào. Không có hạn chế và không có loại liên quan đến biến. Nếu bạn gọi hàm, đối số không cần phải sao chép. Biến tương tự như một con trỏ. Nó có một cách để truy cập giá trị được liên kết với biến. Không cần phải dự trữ bộ nhớ. Chúng ta có thể truyền bất kỳ đối tượng dữ liệu nào khi chúng ta gọi hàm: mọi kích thước và mọi loại.

Các đối tượng dữ liệu có 'loại' và tất cả các đối tượng dữ liệu có thể được truy vấn cho 'loại' của nó.

(type-of "abc")  -> STRING

2) Một loại ký hiệu. Các biểu tượng khác với các chuỗi ở chỗ bạn có thể kiểm tra sự bằng nhau bằng cách so sánh một con trỏ.

Biểu tượng là một đối tượng dữ liệu có tên. Thông thường tên có thể được sử dụng để tìm đối tượng:

|This is a Symbol|
this-is-also-a-symbol

(find-symbol "SIN")   ->  SIN

Vì các ký hiệu là các đối tượng dữ liệu thực, chúng ta có thể kiểm tra xem chúng có phải là cùng một đối tượng hay không:

(eq 'sin 'cos) -> NIL
(eq 'sin 'sin) -> T

Điều này cho phép chúng tôi ví dụ để viết một câu với các ký hiệu:

(defvar *sentence* '(mary called tom to tell him the price of the book))

Bây giờ chúng ta có thể đếm số lượng THE trong câu:

(count 'the *sentence*) ->  2

Trong các ký hiệu Lisp thông thường không chỉ có tên mà còn có thể có giá trị, hàm, danh sách thuộc tính và gói. Vì vậy, các biểu tượng có thể được sử dụng để đặt tên biến hoặc hàm. Danh sách tài sản thường được sử dụng để thêm dữ liệu meta vào biểu tượng.

3) Một ký hiệu cho mã sử dụng cây ký hiệu.

Lisp sử dụng các cấu trúc dữ liệu cơ bản của nó để thể hiện mã.

Danh sách (* 3 2) có thể là cả dữ liệu và mã:

(eval '(* 3 (+ 2 5))) -> 21

(length '(* 3 (+ 2 5))) -> 3

Cái cây:

CL-USER 8 > (sdraw '(* 3 (+ 2 5)))

[*|*]--->[*|*]--->[*|*]--->NIL
 |        |        |
 v        v        v
 *        3       [*|*]--->[*|*]--->[*|*]--->NIL
                   |        |        |
                   v        v        v
                   +        2        5

4) Toàn bộ ngôn ngữ luôn có sẵn. Không có sự phân biệt thực sự giữa thời gian đọc, thời gian biên dịch và thời gian chạy. Bạn có thể biên dịch hoặc chạy mã trong khi đọc, đọc hoặc chạy mã trong khi biên dịch và đọc hoặc biên dịch mã khi chạy.

Lisp cung cấp các chức năng ĐỌC để đọc dữ liệu và mã từ văn bản, LOAD để tải mã, EVAL để đánh giá mã, MÁY TÍNH để biên dịch mã và IN để ghi dữ liệu và mã thành văn bản.

Các chức năng này luôn có sẵn. Họ không đi. Họ có thể là một phần của bất kỳ chương trình. Điều đó có nghĩa là bất kỳ chương trình nào cũng có thể đọc, tải, eval hoặc in mã - luôn luôn.

Chúng khác nhau như thế nào trong các ngôn ngữ như C hoặc Java?

Những ngôn ngữ đó không cung cấp các ký hiệu, mã dưới dạng dữ liệu hoặc thời gian chạy đánh giá dữ liệu dưới dạng mã. Các đối tượng dữ liệu trong C thường được tháo ra.

Có ngôn ngữ nào khác ngoài ngôn ngữ gia đình LISP có bất kỳ cấu trúc nào trong số này không?

Nhiều ngôn ngữ có một số khả năng này.

Sự khác biệt:

Trong Lisp, các khả năng này được thiết kế thành ngôn ngữ để dễ sử dụng.


33

Đối với các điểm (1) và (2), anh ta đang nói chuyện trong lịch sử. Các biến của Java khá giống nhau, đó là lý do tại sao bạn cần gọi .equals () để so sánh các giá trị.

(3) đang nói về biểu thức S. Các chương trình Lisp được viết theo cú pháp này, cung cấp rất nhiều lợi thế so với cú pháp đặc biệt như Java và C, chẳng hạn như bắt các mẫu lặp lại trong macro theo cách sạch hơn so với các mẫu C hoặc macro C ++ và thao tác mã với cùng một danh sách lõi các hoạt động mà bạn sử dụng cho dữ liệu.

(4) lấy C làm ví dụ: ngôn ngữ thực sự là hai ngôn ngữ phụ khác nhau: những thứ như if () và while () và bộ tiền xử lý. Bạn sử dụng bộ tiền xử lý để tiết kiệm phải lặp lại chính mình mọi lúc hoặc bỏ qua mã với # if / # ifdef. Nhưng cả hai ngôn ngữ đều khá riêng biệt và bạn không thể sử dụng while () tại thời gian biên dịch như bạn có thể #if.

C ++ làm cho điều này thậm chí còn tồi tệ hơn với các mẫu. Kiểm tra một vài tài liệu tham khảo về siêu lập trình mẫu, cung cấp cách tạo mã tại thời điểm biên dịch và cực kỳ khó khăn cho những người không phải là chuyên gia quấn đầu. Ngoài ra, đây thực sự là một loạt các hack và thủ thuật sử dụng các mẫu và macro mà trình biên dịch không thể cung cấp hỗ trợ hạng nhất cho - nếu bạn mắc một lỗi cú pháp đơn giản, trình biên dịch không thể cung cấp cho bạn một thông báo lỗi rõ ràng.

Chà, với Lisp, bạn có tất cả những thứ này trong một ngôn ngữ duy nhất. Bạn sử dụng cùng một công cụ để tạo mã trong thời gian chạy khi bạn học trong ngày đầu tiên. Điều này không có nghĩa là siêu lập trình là không quan trọng, nhưng nó chắc chắn đơn giản hơn với sự hỗ trợ của trình biên dịch và ngôn ngữ hạng nhất.


7
Ngoài ra, sức mạnh này (và đơn giản) hiện đã hơn 50 năm và đủ dễ để thực hiện rằng một lập trình viên mới có thể sử dụng nó với hướng dẫn tối thiểu và tìm hiểu về các nguyên tắc cơ bản của ngôn ngữ. Bạn sẽ không nghe thấy một tuyên bố tương tự về Java, C, Python, Perl, Haskell, v.v. như một dự án tốt cho người mới bắt đầu!
Matt Curtis

9
Tôi không nghĩ các biến Java giống như các biểu tượng Lisp. Không có ký hiệu nào cho một biểu tượng trong Java và điều duy nhất bạn có thể làm với một biến là lấy ô giá trị của nó. Các chuỗi có thể được thực hiện nhưng chúng không phải là tên thông thường, do đó, thậm chí không có ý nghĩa gì khi nói về việc chúng có thể được trích dẫn, đánh giá, thông qua, v.v.
Ken

2
Trên 40 tuổi có thể chính xác hơn :), @Ken: Tôi nghĩ rằng anh ta có nghĩa là 1) các biến không nguyên thủy trong java là do refence, tương tự như lisp và 2) các chuỗi được gắn trong java tương tự như các ký hiệu trong lisp - tất nhiên, như bạn đã nói, bạn không thể trích dẫn hoặc loại bỏ các chuỗi / mã được thực hiện trong Java, vì vậy chúng vẫn khá khác nhau.

3
@Dan - Không chắc chắn khi thực hiện lần đầu tiên được thực hiện cùng nhau, nhưng bài báo McCarthy ban đầu về tính toán tượng trưng đã được xuất bản vào năm 1960.
Inaimathi

Java có hỗ trợ một phần / không thường xuyên cho các biểu tượng của NỀN TẢNG dưới dạng Foo. Class / foo.getClass () - tức là một đối tượng Class <Foo> loại tương tự một chút - như là các giá trị enum, một mức độ. Nhưng bóng tối thiểu của một biểu tượng Lisp.
BRPocock

-3

Điểm (1) và (2) cũng sẽ phù hợp với Python. Lấy một ví dụ đơn giản "a = str (82.4)", trình thông dịch trước tiên tạo ra một đối tượng dấu phẩy động có giá trị 82.4. Sau đó, nó gọi một hàm tạo chuỗi, sau đó trả về một chuỗi có giá trị '82 .4 '. 'A' ở phía bên trái chỉ là nhãn cho đối tượng chuỗi đó. Đối tượng dấu phẩy động ban đầu là rác được thu thập vì không có thêm tài liệu tham khảo nào về nó.

Trong Đề án, mọi thứ được coi là một đối tượng theo cách tương tự. Tôi không chắc chắn về Lisp thông thường. Tôi sẽ cố gắng tránh suy nghĩ về các khái niệm C / C ++. Họ làm tôi chậm lại hàng đống khi tôi đang cố gắng xoay quanh sự đơn giản đẹp đẽ của Lisps.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.