Các macro Lisp hữu ích như thế nào?


22

Lisp thông thường cho phép bạn viết các macro thực hiện bất kỳ chuyển đổi nguồn nào bạn muốn.

Lược đồ cung cấp cho bạn một hệ thống khớp mẫu hợp vệ sinh cho phép bạn thực hiện các phép biến đổi. Làm thế nào hữu ích là macro trong thực tế? Paul Graham đã nói trong Beating the A Average rằng:

Mã nguồn của trình soạn thảo Viaweb có lẽ là khoảng 20-25% macro.

Mọi người thực sự làm gì với macro?


Tôi nghĩ rằng điều này chắc chắn rơi vào chủ quan tốt , tôi đã chỉnh sửa câu hỏi của bạn để định dạng. Đây có thể là một bản sao, nhưng tôi không thể tìm thấy.
Tim Post

1
Mọi thứ lặp đi lặp lại dường như không phù hợp với chức năng, tôi đoán vậy.

2
Bạn có thể sử dụng macro để biến Lisp thành bất kỳ ngôn ngữ nào khác, với bất kỳ cú pháp và bất kỳ ngữ nghĩa nào: bit.ly/vqqvHU
SK-logic

lập trình viên.stackexchange.com/questions / 81202 / W đáng để xem ở đây, nhưng nó không phải là một bản sao.
David Thornley

Câu trả lời:


15

Hãy xem bài đăng này của Matthias Felleisen trong danh sách thảo luận LL1 năm 2002. Ông gợi ý ba cách sử dụng chính cho macro:

  1. Ngôn ngữ con dữ liệu : Tôi có thể viết các biểu thức trông đơn giản và tạo các danh sách / mảng / bảng lồng nhau phức tạp với trích dẫn, bỏ qua, v.v. được trang điểm gọn gàng với các macro.
  2. Các cấu trúc liên kết : Tôi có thể giới thiệu các cấu trúc liên kết mới với các macro. Điều đó giúp tôi thoát khỏi lambdas và đặt những thứ gần nhau hơn thuộc về nhau.
  3. Sắp xếp lại đánh giá : Tôi có thể giới thiệu các cấu trúc trì hoãn / hoãn việc đánh giá các biểu thức khi cần thiết. Hãy nghĩ về các vòng lặp, các điều kiện mới, độ trễ / lực, v.v ... [Lưu ý: Trong Haskell hoặc bất kỳ ngôn ngữ lười biếng nào, điều này là không cần thiết.]

18

Tôi chủ yếu sử dụng các macro để thêm các cấu trúc ngôn ngữ mới tiết kiệm thời gian, nếu không sẽ yêu cầu một loạt mã soạn sẵn.

Ví dụ, gần đây tôi thấy mình muốn một mệnh lệnh for-looptương tự như C ++ / Java. Tuy nhiên, là một ngôn ngữ chức năng, Clojure đã không đi kèm với một ngôn ngữ. Vì vậy, tôi chỉ thực hiện nó như một macro:

(defmacro for-loop [[sym init check change :as params] & steps]
  `(loop [~sym ~init value# nil]
     (if ~check
       (let [new-value# (do ~@steps)]
         (recur ~change new-value#))
       value#)))

Và bây giờ tôi có thể làm:

 (for-loop [i 0 , (< i 10) , (inc i)] 
   (println i))

Và ở đó bạn có nó - một cấu trúc ngôn ngữ thời gian biên dịch đa mục đích mới trong sáu dòng mã.


13

Mọi người thực sự làm gì với macro?

Viết phần mở rộng ngôn ngữ hoặc DSL's.

Để cảm nhận điều này bằng các ngôn ngữ giống Lisp, hãy nghiên cứu Vợt , trong đó có một số biến thể ngôn ngữ: Typed Vợt, R6RS và Datalog.

Xem thêm ngôn ngữ Boo, cho phép bạn truy cập vào đường dẫn trình biên dịch cho mục đích cụ thể là tạo Ngôn ngữ dành riêng cho miền thông qua các macro.


4

Dưới đây là một số ví dụ:

Kế hoạch:

  • definecho các định nghĩa hàm. Về cơ bản nó làm cho một cách ngắn hơn để xác định một chức năng.
  • let để tạo các biến phạm vi từ vựng.

Áo choàng:

  • defn, theo tài liệu của nó:

    Giống như (tên def (fn [params *] exprs *)) hoặc (def name (fn ([params *] exprs *) +)) với bất kỳ chuỗi doc hoặc attrs nào được thêm vào siêu dữ liệu var

  • for: danh sách hiểu
  • defmacro: mỉa mai?
  • defmethod, defmulti: làm việc với nhiều phương thức
  • ns

Rất nhiều các macro này làm cho việc viết mã ở mức độ trừu tượng dễ dàng hơn nhiều. Tôi nghĩ rằng các macro là tương tự nhau, theo nhiều cách, với cú pháp trong phi Lisps.

Thư viện âm mưu Incanter cung cấp các macro cho một số thao tác dữ liệu phức tạp.


4

Macro rất hữu ích để nhúng một số mẫu.

Ví dụ, Common Lisp không định nghĩa whilevòng lặp nhưng có do thể được sử dụng để xác định vòng lặp .

Đây là một ví dụ từ On Lisp .

(defmacro while (test &body body)
  `(do ()
       ((not ,test))
     ,@body))

(let ((a 0))
  (while (< a 10)
    (princ (incf a))))

Điều này sẽ in "12345678910" và nếu bạn cố gắng xem điều gì xảy ra với macroexpand-1:

(macroexpand-1 '(while (< a 10) (princ (incf a))))

Điều này sẽ trở lại:

(DO () ((NOT (< A 10))) (PRINC (INCF A)))

Đây là một macro đơn giản, nhưng như đã nói trước đây, chúng thường được sử dụng để xác định ngôn ngữ hoặc DSL mới, nhưng từ ví dụ đơn giản này, bạn đã có thể thử tưởng tượng những gì bạn có thể làm với chúng.

Các loopvĩ mô là một ví dụ tốt về những gì các macro có thể làm.

(loop for i from 0 to 10
      if (and (= (mod i 2) 0) i)
        collect it)
=> (0 2 4 6 8 10)
(loop for i downfrom 10 to 0
      with a = 2
      collect (* a i))
=> (20 18 16 14 12 10 8 6 4 2 0)               

Lisp thông thường có một loại macro khác gọi là macro reader , có thể được sử dụng để sửa đổi cách người đọc diễn giải mã, tức là bạn có thể sử dụng chúng để sử dụng # {và #} có các dấu phân cách như # (và #).


3

Đây là một cái tôi sử dụng để gỡ lỗi (trong Clojure):

user=> (defmacro print-var [varname] `(println ~(name varname) "=" ~varname))
#'user/print-var
=> (def x (reduce * [1 2 3 4 5]))
#'user/x
=> (print-var x)
x = 120
nil

Tôi đã phải đối phó với một bảng băm cuộn bằng tay trong C ++, trong đó getphương thức lấy tham chiếu chuỗi không phải là một đối số, nghĩa là tôi không thể gọi nó bằng một nghĩa đen. Để làm cho nó dễ đối phó hơn, tôi đã viết một cái gì đó như sau:

#define LET(name, value, body)  \
    do {                        \
        string name(value);     \
        body;                   \
        assert(name == value);  \
    } while (false)

Mặc dù vấn đề như thế này khó có thể xảy ra, nhưng tôi thấy thật tuyệt vời khi bạn có thể có các macro không đánh giá các đối số của chúng hai lần, ví dụ bằng cách đưa ra một ràng buộc thực sự . (Phải thừa nhận, ở đây tôi có thể có được xung quanh nó).

Tôi cũng dùng đến cách hack đồ đạc cực kỳ xấu xí do ... while (false)để bạn có thể sử dụng nó trong phần sau của một và nếu vẫn có phần công việc khác như mong đợi. Bạn không cần điều này trong lisp, đó là chức năng của các macro hoạt động trên các cây cú pháp chứ không phải là chuỗi (hoặc chuỗi mã thông báo, tôi nghĩ, trong trường hợp của C và C ++) sau đó trải qua phân tích cú pháp.

Có một số macro phân luồng tích hợp có thể được sử dụng để sắp xếp lại mã của bạn sao cho nó đọc rõ ràng hơn ('phân luồng' như trong 'gieo mã của bạn', chứ không phải song song). Ví dụ:

(->> (range 6) (filter even?) (map inc) (reduce *))

Nó lấy biểu mẫu đầu tiên (range 6)và biến nó thành đối số cuối cùng của biểu mẫu tiếp theo, (filter even?)lần lượt được tạo thành đối số cuối cùng của biểu mẫu tiếp theo, v.v., như vậy, phần trên được viết lại thành

(reduce * (map inc (filter even? (range 6))))

Tôi nghĩ rằng người đầu tiên đọc rõ ràng hơn nhiều: "lấy những dữ liệu này, làm cái này với cái kia, rồi làm cái kia, rồi làm cái kia và chúng ta đã hoàn thành", nhưng đó là chủ quan; Một cái gì đó khách quan đúng là bạn đọc các thao tác trong chuỗi chúng được thực hiện (bỏ qua sự lười biếng).

Ngoài ra còn có một biến thể chèn biểu mẫu trước đó làm đối số đầu tiên (chứ không phải cuối cùng). Một trường hợp sử dụng là số học:

(-> 17 (- 2) (/ 3))

Đọc là "lấy 17, trừ 2 và chia cho 3".

Nói về số học, bạn có thể viết một macro không phân tích cú pháp ký hiệu, để bạn có thể nói ví dụ (infix (17 - 2) / 3)và nó sẽ nhổ ra (/ (- 17 2) 3)cái nào có nhược điểm là ít đọc và lợi thế là biểu thức không hợp lệ. Đó là phần ngôn ngữ DSL / dữ liệu.


1
Ứng dụng chức năng có ý nghĩa với tôi nhiều hơn so với luồng, nhưng chắc chắn đó là vấn đề của thói quen. Câu trả lời tốt đẹp.
coredump
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.