Điều gì làm cho macro Lisp trở nên đặc biệt?


297

Đọc các bài tiểu luận về ngôn ngữ lập trình của Paul Graham, người ta sẽ nghĩ rằng các macro Lisp là cách duy nhất để đi. Là một nhà phát triển bận rộn, làm việc trên các nền tảng khác, tôi không có đặc quyền sử dụng macro Lisp. Là một người muốn hiểu về buzz, vui lòng giải thích điều gì làm cho tính năng này trở nên mạnh mẽ như vậy.

Cũng xin liên hệ điều này với một cái gì đó tôi sẽ hiểu từ thế giới phát triển Python, Java, C # hoặc C.


2
Nhân tiện, có một bộ xử lý macro kiểu LISP cho C # được gọi là LeMP: ecsharp.net/lemp ... JavaScript cũng có một bộ gọi là Sweet.js: sweetjs.org
Qwertie

@Qwertie Sweetjs thậm chí còn làm việc những ngày này?
fredrik.hjarner

Tôi đã không sử dụng nó nhưng lần cam kết gần đây nhất là sáu tháng trước ... đủ tốt cho tôi!
Qwertie

Câu trả lời:


308

Để đưa ra câu trả lời ngắn, các macro được sử dụng để xác định các phần mở rộng cú pháp ngôn ngữ cho Lisp thông thường hoặc Ngôn ngữ cụ thể miền (DSL). Các ngôn ngữ này được nhúng ngay vào mã Lisp hiện có. Bây giờ, DSL có thể có cú pháp tương tự Lisp (như Phiên dịch Prolog của Peter Norvig cho Lisp thông thường) hoặc hoàn toàn khác nhau (ví dụ: Infix Notation Math for Clojure).

Dưới đây là một ví dụ cụ thể hơn:
Python có danh sách hiểu được tích hợp vào ngôn ngữ. Điều này đưa ra một cú pháp đơn giản cho một trường hợp phổ biến. Dòng

divisibleByTwo = [x for x in range(10) if x % 2 == 0]

mang lại một danh sách chứa tất cả các số chẵn từ 0 đến 9. Quay lại Python 1,5 ngày không có cú pháp như vậy; bạn sẽ sử dụng một cái gì đó giống như thế này:

divisibleByTwo = []
for x in range( 10 ):
   if x % 2 == 0:
      divisibleByTwo.append( x )

Cả hai đều tương đương về chức năng. Chúng ta hãy viện dẫn sự đình chỉ của chúng ta về sự hoài nghi và giả vờ Lisp có một macro vòng lặp rất hạn chế chỉ lặp đi lặp lại và không có cách nào dễ dàng để làm tương đương với việc hiểu danh sách.

Trong Lisp bạn có thể viết như sau. Tôi nên lưu ý ví dụ giả định này được chọn là giống hệt với mã Python không phải là một ví dụ hay về mã Lisp.

;; the following two functions just make equivalent of Python's range function
;; you can safely ignore them unless you are running this code
(defun range-helper (x)
  (if (= x 0)
      (list x)
      (cons x (range-helper (- x 1)))))

(defun range (x)
  (reverse (range-helper (- x 1))))

;; equivalent to the python example:
;; define a variable
(defvar divisibleByTwo nil)

;; loop from 0 upto and including 9
(loop for x in (range 10)
   ;; test for divisibility by two
   if (= (mod x 2) 0) 
   ;; append to the list
   do (setq divisibleByTwo (append divisibleByTwo (list x))))

Trước khi tôi đi xa hơn, tôi nên giải thích rõ hơn về macro là gì. Nó là một phép biến đổi được thực hiện trên mã theo mã. Đó là, một đoạn mã, được đọc bởi trình thông dịch (hoặc trình biên dịch), lấy mã làm đối số, thao tác và trả về kết quả, sau đó được chạy tại chỗ.

Tất nhiên đó là rất nhiều đánh máy và lập trình viên là lười biếng. Vì vậy, chúng tôi có thể định nghĩa DSL để thực hiện việc hiểu danh sách. Trên thực tế, chúng tôi đang sử dụng một macro (macro vòng lặp).

Lisp định nghĩa một vài hình thức cú pháp đặc biệt. Trích dẫn ( ') chỉ ra mã thông báo tiếp theo là một nghĩa đen. Quasiquote hoặc backtick ( `) chỉ ra mã thông báo tiếp theo là một nghĩa đen với các lần thoát. Thoát hiểm được chỉ định bởi toán tử dấu phẩy. Nghĩa đen '(1 2 3)là tương đương với Python [1, 2, 3]. Bạn có thể gán nó cho một biến khác hoặc sử dụng nó tại chỗ. Bạn có thể nghĩ `(1 2 ,x)tương đương với Python trong [1, 2, x]đó xmột biến được xác định trước đó. Ký hiệu danh sách này là một phần của phép thuật đi vào macro. Phần thứ hai là trình đọc Lisp thay thế macro một cách thông minh cho mã nhưng điều đó được minh họa rõ nhất dưới đây:

Vì vậy, chúng ta có thể định nghĩa một macro được gọi là lcomp(viết tắt của danh sách hiểu). Cú pháp của nó sẽ giống hệt như con trăn mà chúng ta đã sử dụng trong ví dụ [x for x in range(10) if x % 2 == 0] -(lcomp x for x in (range 10) if (= (% x 2) 0))

(defmacro lcomp (expression for var in list conditional conditional-test)
  ;; create a unique variable name for the result
  (let ((result (gensym)))
    ;; the arguments are really code so we can substitute them 
    ;; store nil in the unique variable name generated above
    `(let ((,result nil))
       ;; var is a variable name
       ;; list is the list literal we are suppose to iterate over
       (loop for ,var in ,list
            ;; conditional is if or unless
            ;; conditional-test is (= (mod x 2) 0) in our examples
            ,conditional ,conditional-test
            ;; and this is the action from the earlier lisp example
            ;; result = result + [x] in python
            do (setq ,result (append ,result (list ,expression))))
           ;; return the result 
       ,result)))

Bây giờ chúng ta có thể thực thi tại dòng lệnh:

CL-USER> (lcomp x for x in (range 10) if (= (mod x 2) 0))
(0 2 4 6 8)

Khá gọn gàng hả? Bây giờ nó không dừng lại ở đó. Bạn có một cơ chế, hoặc một cây cọ vẽ, nếu bạn thích. Bạn có thể có bất kỳ cú pháp nào bạn có thể muốn. Giống như withcú pháp của Python hoặc C # . Hoặc cú pháp LINQ của .NET. Cuối cùng, đây là điều thu hút mọi người đến với Lisp - sự linh hoạt tối thượng.


51
+1 để thực hiện việc hiểu danh sách trong Lisp, vì tại sao không?
ckb

9
@ckb Trên thực tế LISP đã có một macro hiểu danh sách trong thư viện chuẩn : (loop for x from 0 below 10 when (evenp x) collect x), thêm ví dụ ở đây . Nhưng thực sự, loop là "chỉ là một macro" (tôi thực sự đã triển khai lại từ đầu một thời gian trước đây )
Suzanne Dupéron

8
Tôi biết nó khá không liên quan, nhưng tôi tự hỏi về cú pháp và cách phân tích cú pháp thực sự hoạt động ... Nói rằng tôi gọi lcomp theo cách đó (thay đổi mục thirs từ "for" thành "azertyuiop"): (lcomp x azertyuiop x in ( phạm vi 10) nếu (= (% x 2) 0)) macro vẫn hoạt động như mong đợi? Hoặc là tham số "for" được sử dụng trong vòng lặp để nó phải là chuỗi "for" khi được gọi?
dader

2
@dader Vì vậy, vòng lặp thực sự là một macro khác và for là một biểu tượng đặc biệt được sử dụng trong macro đó. Tôi có thể dễ dàng xác định macro mà không cần macro vòng lặp. Tuy nhiên, nếu tôi có, bài viết sẽ dài hơn nhiều. Macro vòng lặp và tất cả cú pháp của nó được định nghĩa trong CLtL .
gte525u

3
Một điều tôi bối rối với các macro của ngôn ngữ khác, đó là các macro của chúng bị giới hạn bởi cú pháp của ngôn ngữ máy chủ. Các macro Lispy có thể diễn giải cú pháp không Lispy. Tôi có nghĩa là giống như tưởng tượng việc tạo ra một cú pháp giống như cú pháp (không có paranthese) và diễn giải nó bằng cách sử dụng macro Lisp. Điều này có thể không, và những ưu / nhược điểm của việc sử dụng macro so với việc sử dụng một trình phân tích và trình phân tích cú pháp trực tiếp là gì?
CMCDragonkai

105

Bạn sẽ tìm thấy một cuộc tranh luận toàn diện xung quanh macro lisp ở đây .

Một tập hợp con thú vị của bài viết đó:

Trong hầu hết các ngôn ngữ lập trình, cú pháp là phức tạp. Các macro phải tách rời cú pháp chương trình, phân tích nó và lắp lại nó. Họ không có quyền truy cập vào trình phân tích cú pháp của chương trình, vì vậy họ phải phụ thuộc vào phỏng đoán và phỏng đoán tốt nhất. Đôi khi phân tích tỷ lệ cắt của họ là sai, và sau đó họ phá vỡ.

Nhưng Lisp thì khác. Lisp macro làm có quyền truy cập vào các phân tích cú pháp, và nó là một phân tích cú pháp thực sự đơn giản. Một macro Lisp không được trao một chuỗi, mà là một đoạn mã nguồn được chuẩn bị trước dưới dạng một danh sách, bởi vì nguồn của chương trình Lisp không phải là một chuỗi; nó là một danh sách Và các chương trình Lisp thực sự tốt trong việc tách các danh sách và đưa chúng trở lại với nhau. Họ làm điều này một cách đáng tin cậy, mỗi ngày.

Đây là một ví dụ mở rộng. Lisp có một macro, được gọi là "setf", thực hiện nhiệm vụ. Hình thức đơn giản nhất của setf là

  (setf x whatever)

trong đó đặt giá trị của ký hiệu "x" thành giá trị của biểu thức "sao cũng được".

Lisp cũng có danh sách; bạn có thể sử dụng các chức năng "xe hơi" và "cdr" để có được phần tử đầu tiên của danh sách hoặc phần còn lại của danh sách, tương ứng.

Bây giờ nếu bạn muốn thay thế phần tử đầu tiên của danh sách bằng một giá trị mới thì sao? Có một chức năng tiêu chuẩn để làm điều đó, và thật không ngờ, tên của nó thậm chí còn tệ hơn cả "xe hơi". Đó là "rplaca". Nhưng bạn không cần phải nhớ "rplaca", vì bạn có thể viết

  (setf (car somelist) whatever)

để đặt xe của somelist.

Điều thực sự xảy ra ở đây là "setf" là một macro. Tại thời điểm biên dịch, nó kiểm tra các đối số của nó và nó thấy rằng cái đầu tiên có dạng (xe SOMETHING). Nó tự nói với mình "Ồ, lập trình viên đang cố gắng thiết lập chiếc xe của một thứ gì đó. Chức năng sử dụng cho điều đó là 'rplaca'." Và nó lặng lẽ viết lại mã tại chỗ để:

  (rplaca somelist whatever)

5
setf là một minh họa đẹp về sức mạnh của macro, cảm ơn vì đã bao gồm nó.
Joel

Tôi thích phần nổi bật .. bởi vì nguồn của chương trình Lisp không phải là một chuỗi; nó là một danh sách ! Đây có phải là lý do chính tại sao macro LISP vượt trội hơn hầu hết các loại khác do dấu ngoặc đơn của nó không?
Sinh viên

@Student Tôi cho rằng như vậy: books.google.fr/... gợi ý bạn là đúng.
VonC

55

Các macro Lisp thông thường về cơ bản mở rộng "nguyên hàm cú pháp" của mã của bạn.

Ví dụ, trong C, cấu trúc chuyển đổi / trường hợp chỉ hoạt động với các loại tích phân và nếu bạn muốn sử dụng nó cho float hoặc chuỗi, bạn còn lại với các câu lệnh và so sánh rõ ràng lồng nhau. Cũng không có cách nào bạn có thể viết một macro C để thực hiện công việc cho bạn.

Nhưng, vì macro lisp (về cơ bản) là một chương trình lisp lấy đoạn mã làm đầu vào và trả về mã để thay thế "lời gọi" của macro, bạn có thể mở rộng tiết mục "nguyên thủy" của mình theo cách bạn muốn, thường kết thúc với một chương trình dễ đọc hơn.

Để làm điều tương tự trong C, bạn sẽ phải viết một bộ xử lý trước tùy chỉnh ăn nguồn ban đầu (không hoàn toàn C) của bạn và phun ra thứ gì đó mà trình biên dịch C có thể hiểu được. Đó không phải là một cách sai lầm để đi về nó, nhưng nó không nhất thiết là dễ nhất.


41

Các macro Lisp cho phép bạn quyết định khi nào (nếu có) bất kỳ phần hoặc biểu thức nào sẽ được đánh giá. Để đưa ra một ví dụ đơn giản, hãy nghĩ về C's:

expr1 && expr2 && expr3 ...

Điều này nói là: Đánh giá expr1, và, nó phải là sự thật, đánh giá expr2, v.v.

Bây giờ hãy thử biến nó &&thành một chức năng ... đúng vậy, bạn không thể. Gọi một cái gì đó như:

and(expr1, expr2, expr3)

Sẽ đánh giá cả ba exprstrước khi đưa ra câu trả lời bất kể expr1là sai!

Với các macro lisp bạn có thể mã hóa một cái gì đó như:

(defmacro && (expr1 &rest exprs)
    `(if ,expr1                     ;` Warning: I have not tested
         (&& ,@exprs)               ;   this and might be wrong!
         nil))

bây giờ bạn có một hàm &&mà bạn có thể gọi giống như một hàm và nó sẽ không đánh giá bất kỳ dạng nào bạn chuyển đến nó trừ khi tất cả đều đúng.

Để xem làm thế nào là hữu ích, tương phản:

(&& (very-cheap-operation)
    (very-expensive-operation)
    (operation-with-serious-side-effects))

và:

and(very_cheap_operation(),
    very_expensive_operation(),
    operation_with_serious_side_effects());

Những điều khác bạn có thể làm với macro là tạo từ khóa mới và / hoặc ngôn ngữ nhỏ (xem (loop ...)ví dụ về macro), tích hợp các ngôn ngữ khác vào lisp, ví dụ: bạn có thể viết một macro cho phép bạn nói điều gì đó như:

(setvar *rows* (sql select count(*)
                      from some-table
                     where column1 = "Yes"
                       and column2 like "some%string%")

Và điều đó thậm chí không nhận được vào các macro Reader .

Hi vọng điêu nay co ich.


Tôi nghĩ rằng nó nên là: "(áp dụng &&, @ exprs); điều này và có thể sai!"
Svante

1
@svante - theo hai cách tính: đầu tiên, && là macro, không phải là hàm; chỉ áp dụng cho các chức năng. thứ hai, áp dụng lấy một danh sách các đối số để vượt qua, vì vậy bạn muốn một trong số "(funcall fn, @ exprs)", "(áp dụng fn (list, @ exprs)" hoặc "(áp dụng fn, @ exprs nil)", không "(áp dụng fn, @ exprs)".
Aaron

(and ...sẽ đánh giá các biểu thức cho đến khi một đánh giá thành sai, lưu ý rằng các hiệu ứng phụ được tạo ra bởi đánh giá sai sẽ diễn ra, chỉ các biểu thức tiếp theo sẽ bị bỏ qua.
ocodo

31

Tôi không nghĩ rằng tôi đã từng thấy các macro Lisp được giải thích tốt hơn so với đồng nghiệp này: http://www.defmacro.org/ramblings/lisp.html


3
Đặc biệt là nếu bạn có nền Java / XML.
hoàng hôn

1
Thật là một niềm vui khi đọc điều này nằm trên chiếc ghế dài của tôi vào một chiều thứ bảy! Được viết và tổ chức rất rõ ràng.
Colin

10

Hãy nghĩ về những gì bạn có thể làm trong C hoặc C ++ với các macro và mẫu. Chúng là những công cụ rất hữu ích để quản lý mã lặp đi lặp lại, nhưng chúng bị hạn chế theo những cách khá nghiêm trọng.

  • Cú pháp macro / mẫu giới hạn hạn chế sử dụng của họ. Ví dụ: bạn không thể viết một mẫu mở rộng ra một thứ khác ngoài một lớp hoặc một hàm. Macro và mẫu không thể dễ dàng duy trì dữ liệu nội bộ.
  • Cú pháp phức tạp, rất bất thường của C và C ++ gây khó khăn cho việc viết các macro rất chung chung.

Các macro Lisp và Lisp giải quyết các vấn đề này.

  • Macro Lisp được viết bằng Lisp. Bạn có toàn bộ sức mạnh của Lisp để viết macro.
  • Lisp có một cú pháp rất thường xuyên.

Nói chuyện với bất cứ ai thành thạo C ++ và hỏi họ mất bao lâu để học tất cả các mẫu giả mà họ cần để thực hiện siêu lập trình mẫu. Hoặc tất cả các mánh khóe điên rồ trong các cuốn sách (xuất sắc) như Modern C ++ Design , vẫn khó gỡ lỗi và (trong thực tế) không thể di chuyển giữa các trình biên dịch trong thế giới thực mặc dù ngôn ngữ đã được chuẩn hóa trong một thập kỷ. Tất cả điều đó tan chảy nếu ngôn ngữ bạn sử dụng cho siêu lập trình là cùng ngôn ngữ bạn sử dụng để lập trình!


11
Chà, công bằng mà nói, vấn đề với siêu lập trình mẫu C ++ không phải là ngôn ngữ lập trình siêu dữ liệu khác nhau , mà là điều đáng lo ngại - nó không được thiết kế nhiều như một chức năng mẫu đơn giản hơn nhiều.
Brooks Moses

@Brooks Chắc chắn. Các tính năng mới nổi không phải lúc nào cũng xấu. Thật không may, trong một ngôn ngữ ủy ban di chuyển chậm, thật khó để sửa chúng khi chúng tồn tại. Thật đáng tiếc khi có rất nhiều tính năng mới hữu ích hiện đại của C ++ được viết bằng ngôn ngữ mà ít người có thể đọc được và có một khoảng cách rất lớn giữa lập trình viên trung bình và "linh mục cao cấp".
Matt Curtis

2
@downvoter: nếu có gì sai với câu trả lời của tôi, vui lòng để lại nhận xét để tất cả chúng ta có thể chia sẻ kiến ​​thức.
Matt Curtis

10

Một macro lisp lấy một đoạn chương trình làm đầu vào. Đoạn chương trình này được biểu diễn một cấu trúc dữ liệu có thể được thao tác và biến đổi bất kỳ cách nào bạn muốn. Cuối cùng, macro xuất ra một đoạn chương trình khác và đoạn này là phần được thực thi trong thời gian chạy.

C # không có tiện ích macro, tuy nhiên tương đương sẽ là nếu trình biên dịch phân tích mã thành cây CodeDOM và chuyển nó sang một phương thức, chuyển đổi nó thành CodeDOM khác, sau đó được biên dịch thành IL.

Điều này có thể được sử dụng để thực hiện "đường" cú pháp như for each-statement using-clause, LINQ select-expressions và như vậy, như macro mà biến đổi vào mã bên dưới.

Nếu Java có macro, bạn có thể triển khai cú pháp Linq trong Java mà không cần Sun thay đổi ngôn ngữ cơ sở.

Dưới đây là mã giả để biết macro có kiểu chữ lisp trong C # để triển khai usingcó thể trông như thế nào:

define macro "using":
    using ($type $varname = $expression) $block
into:
    $type $varname;
    try {
       $varname = $expression;
       $block;
    } finally {
       $varname.Dispose();
    }

Bây giờ có thực sự một bộ xử lý Lisp kiểu vĩ mô cho C #, tôi sẽ chỉ ra rằng một macro cho usingsẽ trông như thế này ;)
Qwertie

9

Vì các câu trả lời hiện có đưa ra các ví dụ cụ thể tốt để giải thích các macro đạt được và cách thức, có lẽ sẽ giúp thu thập một số suy nghĩ về lý do tại sao cơ sở vĩ mô là một lợi ích đáng kể so với các ngôn ngữ khác ; đầu tiên từ những câu trả lời này, sau đó là một câu trả lời tuyệt vời từ nơi khác:

... Trong C, bạn sẽ phải viết một bộ xử lý trước tùy chỉnh [có thể đủ điều kiện là một chương trình C đủ phức tạp ] ...

- Vatine

Nói chuyện với bất cứ ai thành thạo C ++ và hỏi họ mất bao lâu để học tất cả các mẫu giả mà họ cần để thực hiện siêu lập trình mẫu [mà vẫn không mạnh bằng].

- Matt Curtis

... Trong Java, bạn phải hack theo cách của mình bằng cách dệt mã byte, mặc dù một số khung như AspectJ cho phép bạn thực hiện điều này bằng cách sử dụng một cách tiếp cận khác, về cơ bản đó là hack.

- Miguel Ping

DOLIST tương tự như foreach của Perl hoặc Python dành cho. Java đã thêm một kiểu cấu trúc vòng lặp tương tự với vòng lặp "được tăng cường" trong Java 1.5, như là một phần của JSR-201. Lưu ý những gì một macro khác biệt làm. Một lập trình viên Lisp nhận thấy một mẫu chung trong mã của họ có thể viết một macro để tạo cho mình một bản tóm tắt mức nguồn của mẫu đó. Một lập trình viên Java nhận thấy mô hình tương tự phải thuyết phục Sun rằng sự trừu tượng đặc biệt này đáng để thêm vào ngôn ngữ. Sau đó, Sun phải xuất bản một JSR và triệu tập một "nhóm chuyên gia" toàn ngành để băm nhỏ mọi thứ. Quá trình đó - theo Sun - mất trung bình 18 tháng. Sau đó, tất cả các nhà văn trình biên dịch đều phải nâng cấp trình biên dịch của họ để hỗ trợ tính năng mới. Và ngay cả khi trình biên dịch yêu thích của lập trình viên Java hỗ trợ phiên bản Java mới, có lẽ họ '' vẫn '' không thể sử dụng tính năng mới cho đến khi họ được phép phá vỡ tính tương thích nguồn với các phiên bản Java cũ hơn. Vì vậy, một sự phiền toái mà các lập trình viên Lisp thông thường có thể tự giải quyết trong vòng năm phút khiến các lập trình viên Java gặp khó khăn trong nhiều năm.

- Peter Seibel, trong "Thực tế chung Lisp"


8

Tôi không chắc mình có thể thêm một số thông tin chi tiết vào bài viết (xuất sắc) của mọi người, nhưng ...

Các macro Lisp hoạt động tuyệt vời vì bản chất cú pháp Lisp.

Lisp là một ngôn ngữ cực kỳ thường xuyên (nghĩ rằng mọi thứ là một danh sách ); các macro cho phép bạn coi dữ liệu và mã là như nhau (không cần phân tích cú pháp chuỗi hoặc các bản hack khác để sửa đổi các biểu thức lisp). Bạn kết hợp hai tính năng này và bạn có một cách rất rõ ràng để sửa đổi mã.

Chỉnh sửa: Điều tôi đã cố gắng nói là Lisp là homoiconic , có nghĩa là cấu trúc dữ liệu cho một chương trình lisp được viết bằng chính lisp.

Vì vậy, bạn kết thúc với một cách tạo trình tạo mã của riêng bạn trên ngôn ngữ bằng chính ngôn ngữ đó (ví dụ: trong Java, bạn phải hack theo cách của mình bằng cách dệt mã byte, mặc dù một số khung như AspectJ cho phép bạn làm điều này bằng cách sử dụng một cách tiếp cận khác, về cơ bản đó là một hack).

Trong thực tế, với các macro, bạn kết thúc việc xây dựng ngôn ngữ nhỏ của riêng mình trên đỉnh cao, mà không cần phải học thêm ngôn ngữ hoặc công cụ, và sử dụng toàn bộ sức mạnh của ngôn ngữ.


1
Đó là nhận xét sâu sắc, tuy nhiên, ý tưởng này cho rằng "mọi thứ là một danh sách" có thể khiến người mới sợ hãi. để hiểu một danh sách, bạn cần hiểu khuyết điểm, xe hơi, cdrs, tế bào. Chính xác hơn, Lisp được tạo ra S-expressions, không phải danh sách.
ribamar

6

Các macro Lisp đại diện cho một mô hình xảy ra trong hầu hết các dự án lập trình lớn. Cuối cùng, trong một chương trình lớn, bạn có một phần mã nhất định nơi bạn nhận ra rằng nó sẽ đơn giản hơn và ít xảy ra lỗi hơn khi bạn viết chương trình xuất mã nguồn dưới dạng văn bản mà sau đó bạn có thể chỉ cần dán vào.

Trong các đối tượng Python có hai phương thức __repr____str__. __str__chỉ đơn giản là đại diện có thể đọc được của con người. __repr__trả về một đại diện là mã Python hợp lệ, nghĩa là, một cái gì đó có thể được nhập vào trình thông dịch là Python hợp lệ. Bằng cách này, bạn có thể tạo các đoạn Python nhỏ tạo mã hợp lệ có thể được dán vào nguồn thực sự của bạn.

Trong Lisp toàn bộ quá trình này đã được hệ thống vĩ mô chính thức hóa. Chắc chắn rằng nó cho phép bạn tạo các phần mở rộng cho cú pháp và thực hiện tất cả các loại điều ưa thích, nhưng tính hữu dụng thực sự của nó được tóm tắt bởi những điều trên. Tất nhiên, nó giúp hệ thống macro Lisp cho phép bạn thao tác các "đoạn trích" này với toàn bộ sức mạnh của toàn bộ ngôn ngữ.


1
Đoạn đầu tiên của bạn rất rõ ràng với người ngoài Lisp, điều này rất quan trọng.
tự đại diện

5

Nói tóm lại, macro là các phép biến đổi của mã. Họ cho phép giới thiệu nhiều cấu trúc cú pháp mới. Ví dụ, hãy xem xét LINQ trong C #. Trong lisp, có các phần mở rộng ngôn ngữ tương tự được triển khai bởi các macro (ví dụ: cấu trúc vòng lặp tích hợp, lặp). Macro giảm đáng kể sao chép mã. Macro cho phép nhúng «ngôn ngữ nhỏ» (ví dụ: trong c # / java người ta sẽ sử dụng xml để định cấu hình, trong trường hợp không thể thực hiện được điều tương tự với macro). Macro có thể che giấu những khó khăn trong việc sử dụng thư viện.

Ví dụ, trong lisp bạn có thể viết

(iter (for (id name) in-clsql-query "select id, name from users" on-database *users-database*)
      (format t "User with ID of ~A has name ~A.~%" id name))

và điều này ẩn tất cả các công cụ cơ sở dữ liệu (giao dịch, đóng kết nối thích hợp, tìm nạp dữ liệu, v.v.)


3

Mặc dù tất cả những điều trên giải thích các macro là gì và thậm chí có các ví dụ thú vị, tôi nghĩ rằng sự khác biệt chính giữa một macro và một hàm bình thường là LISP đánh giá tất cả các tham số trước khi gọi hàm. Với một macro thì ngược lại, LISP chuyển các tham số không được đánh giá cho macro. Ví dụ: nếu bạn chuyển (+ 1 2) cho một hàm, hàm sẽ nhận giá trị 3. Nếu bạn chuyển giá trị này cho macro, nó sẽ nhận được Danh sách (+ 1 2). Điều này có thể được sử dụng để làm tất cả các loại công cụ cực kỳ hữu ích.

  • Thêm cấu trúc điều khiển mới, ví dụ vòng lặp hoặc giải cấu trúc danh sách
  • Đo thời gian cần thiết để thực thi một chức năng được truyền vào. Với một chức năng, tham số sẽ được ước tính trước khi điều khiển được truyền cho chức năng. Với macro, bạn có thể ghép mã giữa điểm bắt đầu và điểm dừng của đồng hồ bấm giờ. Dưới đây có cùng mã chính xác trong một macro và một hàm và đầu ra rất khác nhau. Lưu ý: Đây là một ví dụ có sẵn và việc triển khai đã được chọn sao cho giống hệt nhau để làm nổi bật sự khác biệt tốt hơn.

    (defmacro working-timer (b) 
      (let (
            (start (get-universal-time))
            (result (eval b))) ;; not splicing here to keep stuff simple
        ((- (get-universal-time) start))))
    
    (defun my-broken-timer (b)
      (let (
            (start (get-universal-time))
            (result (eval b)))    ;; doesn't even need eval
        ((- (get-universal-time) start))))
    
    (working-timer (sleep 10)) => 10
    
    (broken-timer (sleep 10)) => 0

Btw, Scala đã thêm macro vào ngôn ngữ. Mặc dù chúng thiếu vẻ đẹp của macro Lisp vì ngôn ngữ không phải là đồng âm, nhưng chúng chắc chắn đáng để xem xét và cuối cùng các cây cú pháp trừu tượng mà chúng cung cấp có thể dễ sử dụng hơn. Vẫn còn quá sớm để nói tôi thích hệ thống vĩ mô nào.
Joerg Schmuecker

2
"LISP chuyển các tham số không được đánh giá cho macro" cuối cùng cũng có câu trả lời rõ ràng. nhưng bạn đã quên nửa sau của điều đó: và kết quả của macro là một mã được chuyển đổi sẽ được hệ thống đánh giá toàn bộ thay cho bản gốc như thể nó ở đó ngay từ đầu (trừ khi chính nó lại gọi đến macro, mà cũng sẽ được chuyển đổi, bởi rằng vĩ mô thời gian này).
Will Ness

0

Tôi đã nhận được điều này từ cuốn sách dạy nấu ăn phổ biến nhưng tôi nghĩ nó đã giải thích tại sao các macro lisp lại tốt theo cách tốt đẹp.

"Macro là một đoạn mã Lisp thông thường hoạt động trên một đoạn mã Lisp giả định khác, dịch nó thành (một phiên bản gần hơn) Lisp thực thi. Nghe có vẻ hơi phức tạp, vì vậy hãy đưa ra một ví dụ đơn giản. Giả sử bạn muốn phiên bản setq đặt hai biến có cùng giá trị. Vì vậy, nếu bạn viết

(setq2 x y (+ z 3))

khi z=8cả x và y được đặt thành 11. (Tôi không thể nghĩ ra cách sử dụng nào cho việc này, nhưng đó chỉ là một ví dụ.)

Rõ ràng là chúng ta không thể định nghĩa setq2 là một hàm. Nếu x=50y=-5, hàm này sẽ nhận các giá trị 50, -5 và 11; nó sẽ không có kiến ​​thức về những biến được cho là được đặt. Điều chúng tôi thực sự muốn nói là, Khi bạn (hệ thống Lisp) thấy (setq2 v1 v2 e), hãy coi nó là tương đương với (progn (setq v1 e) (setq v2 e)). Trên thực tế, điều này không hoàn toàn đúng, nhưng nó sẽ làm ngay bây giờ. Một macro cho phép chúng ta thực hiện chính xác điều này, bằng cách chỉ định một chương trình để chuyển đổi mẫu đầu vào (setq2 v1 v2 e)"thành mẫu đầu ra (progn ...)".

Nếu bạn nghĩ rằng điều này là tốt đẹp, bạn có thể tiếp tục đọc ở đây: http://cl-cookbook.sourceforge.net/macros.html


1
Có thể định nghĩa setq2là một hàm nếu xyđược truyền bằng tham chiếu. Tôi không biết nếu có thể trong CL, tuy nhiên. Vì vậy, đối với những người không biết Lisps hoặc CL nói riêng thì đây không phải là ví dụ rất minh họa IMO
neoascetic

Đối số CL @neoascetic chỉ truyền theo giá trị (đó là lý do tại sao nó cần macro ở vị trí đầu tiên). một số giá trị là con trỏ mặc dù (như danh sách).
Will Ness

-5

Trong python bạn có trang trí, về cơ bản bạn có một chức năng lấy một chức năng khác làm đầu vào. Bạn có thể làm những gì bạn muốn: gọi hàm, làm một cái gì đó khác, bọc lệnh gọi hàm trong một bản phát hành tài nguyên, v.v. nhưng bạn không được nhìn trộm bên trong hàm đó. Giả sử chúng tôi muốn làm cho nó mạnh hơn, giả sử người trang trí của bạn đã nhận được mã của hàm dưới dạng danh sách thì bạn không chỉ có thể thực thi chức năng như hiện tại mà giờ đây bạn có thể thực thi các phần của nó, sắp xếp lại các dòng của hàm, v.v.

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.