Seberapa bergunakah Lisp macro?

22

Common Lisp memungkinkan Anda untuk menulis makro yang melakukan transformasi sumber apa pun yang Anda inginkan.

Skema memberi Anda sistem pencocokan pola higienis yang memungkinkan Anda melakukan transformasi juga. Seberapa berguna makro dalam praktik? Paul Graham mengatakan dalam Beating the Averages bahwa:

Kode sumber editor Viaweb mungkin sekitar 20-25% makro.

Hal-hal macam apa yang akhirnya orang lakukan dengan makro?

compman
sumber
Saya pikir ini pasti jatuh ke dalam subyektif yang baik , saya telah mengedit pertanyaan Anda untuk memformat. Ini bisa jadi duplikat, tapi saya tidak bisa menemukannya.
Tim Post
1
Semuanya berulang yang tampaknya tidak cocok dengan suatu fungsi, saya kira.
2
Anda dapat menggunakan macro untuk mengubah Lisp ke setiap bahasa lain, dengan sintaks dan semantik setiap: bit.ly/vqqvHU
SK-logika
programmers.stackexchange.com/questions/81202/… layak dilihat di sini, tetapi itu bukan duplikat.
David Thornley

Jawaban:

15

Lihatlah posting ini oleh Matthias Felleisen ke daftar diskusi LL1 pada tahun 2002. Dia menyarankan tiga kegunaan utama untuk makro:

  1. Subbahasa data : Saya dapat menulis ekspresi yang tampak sederhana dan membuat daftar / susunan / tabel bersarang yang kompleks dengan kutipan, tanda kutip, dll. Dengan berpakaian rapi dengan makro.
  2. Binding constructs : Saya dapat memperkenalkan konstruksi binding baru dengan macro. Itu membantu saya menyingkirkan lambdas dan menempatkan hal-hal yang lebih dekat bersama milik bersama.
  3. Penataan ulang evaluasi : Saya dapat memperkenalkan konstruksi yang menunda / menunda evaluasi ekspresi sesuai kebutuhan. Pikirkan loop, kondisional baru, delay / force, dll. [Catatan: Di Haskell atau bahasa malas apa pun, ini tidak perlu.]
John Clements
sumber
18

Saya kebanyakan menggunakan makro untuk menambahkan konstruksi bahasa baru yang menghemat waktu, yang kalau tidak akan memerlukan banyak kode boilerplate.

Sebagai contoh, saya baru-baru ini mendapati diri saya ingin imperatif for-loopmirip dengan C ++ / Java. Namun, sebagai bahasa fungsional, Clojure tidak datang dengan satu di luar kotak. Jadi saya hanya mengimplementasikannya sebagai makro:

(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#)))

Dan sekarang saya bisa melakukan:

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

Dan begitulah - konstruksi bahasa kompilasi waktu-tujuan baru baru dalam enam baris kode.

mikera
sumber
13

Hal-hal macam apa yang akhirnya orang lakukan dengan makro?

Ekstensi bahasa tulisan atau DSL.

Untuk merasakan hal ini dalam bahasa mirip Lisp, pelajarilah Racket , yang memiliki beberapa varian bahasa: Typed Racket, R6RS, dan Datalog.

Lihat juga bahasa Boo, yang memberi Anda akses ke pipa penyusun untuk tujuan khusus membuat Bahasa Khusus Domain melalui makro.

Robert Harvey
sumber
4

Berikut ini beberapa contohnya:

Skema:

  • defineuntuk definisi fungsi. Pada dasarnya itu membuat cara yang lebih pendek untuk mendefinisikan suatu fungsi.
  • let untuk membuat variabel dengan cakupan leksikal.

Clojure:

  • defn, menurut dokumennya:

    Sama seperti (nama def (fn [params *] exprs *)) atau (nama def (fn ([params *] exprs *) +)) dengan setiap string atau attr yang ditambahkan ke var metadata

  • for: daftar pemahaman
  • defmacro: ironis?
  • defmethod, defmulti: bekerja dengan multi-metode
  • ns

Banyak makro ini membuatnya lebih mudah untuk menulis kode pada tingkat yang lebih abstrak. Saya menganggap makro mirip, dalam banyak hal, dengan sintaks dalam non-Lisps.

Incanter plotting library menyediakan makro untuk beberapa manipulasi data yang kompleks.


sumber
4

Makro berguna untuk menyematkan beberapa pola.

Sebagai contoh, Common Lisp tidak mendefinisikan whileloop tetapi memiliki do yang dapat digunakan untuk mendefinisikannya.

Ini adalah contoh dari On Lisp .

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

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

Ini akan mencetak "12345678910", dan jika Anda mencoba melihat apa yang terjadi dengan macroexpand-1:

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

Ini akan mengembalikan:

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

Ini adalah makro sederhana, tetapi seperti yang dikatakan sebelumnya, mereka biasanya digunakan untuk mendefinisikan bahasa atau DSL baru, tetapi dari contoh sederhana ini Anda sudah dapat mencoba membayangkan apa yang dapat Anda lakukan dengan mereka.

The loopmakro adalah contoh yang baik dari apa yang macro bisa lakukan.

(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)               

Common Lisp memiliki jenis makro lain yang disebut pembaca makro yang dapat digunakan untuk memodifikasi cara pembaca menginterpretasikan kode, yaitu Anda dapat menggunakannya untuk menggunakan # {dan #} memiliki pembatas seperti # (dan #).

Daimrod
sumber
3

Inilah yang saya gunakan untuk debugging (di 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

Saya harus berurusan dengan tabel hash linting tangan di C + +, di mana getmetode mengambil referensi string non-const sebagai argumen, yang berarti saya tidak bisa menyebutnya dengan literal. Untuk membuatnya lebih mudah untuk ditangani, saya menulis sesuatu seperti berikut:

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

Sementara sesuatu seperti masalah ini tidak mungkin muncul di lisp, saya merasa sangat baik bahwa Anda dapat memiliki makro yang tidak mengevaluasi argumen mereka dua kali, misalnya dengan memperkenalkan pengikatan nyata . (Diakui, di sini saya bisa mengatasinya).

Saya juga menggunakan cara membungkus barang yang jelek dan jelek do ... while (false)sehingga Anda bisa menggunakannya di bagian waktu dan jika masih ada bagian lain seperti yang diharapkan. Anda tidak memerlukan ini dalam lisp, yang merupakan fungsi dari makro yang beroperasi pada pohon sintaks daripada string (atau urutan token, saya pikir, dalam kasus C dan C ++) yang kemudian menjalani parsing.

Ada beberapa makro threading bawaan yang dapat digunakan untuk mengatur ulang kode Anda sehingga terbaca lebih bersih ('threading' seperti dalam 'menabur kode Anda bersama-sama', bukan paralelisme). Sebagai contoh:

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

Ia mengambil bentuk pertama (range 6),, dan menjadikannya argumen terakhir dari bentuk berikutnya (filter even?),, yang pada gilirannya dijadikan argumen terakhir dari bentuk berikutnya dan seterusnya, sehingga di atas akan ditulis ulang menjadi

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

Saya pikir yang pertama berbunyi lebih jelas: "ambil data ini, lakukan ini, lalu lakukan itu, lalu lakukan yang lain dan kita selesai", tapi itu subjektif; sesuatu yang secara objektif benar adalah bahwa Anda membaca operasi dalam urutan yang mereka lakukan (mengabaikan kemalasan).

Ada juga varian yang menyisipkan bentuk sebelumnya sebagai argumen pertama (bukan terakhir). Satu kasus penggunaan adalah aritmatika:

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

Dibaca sebagai "ambil 17, kurangi 2 dan bagi 3".

Berbicara tentang aritmatika, Anda dapat menulis makro yang tidak menguraikan notasi parsing, sehingga Anda bisa mengatakan misalnya (infix (17 - 2) / 3)dan itu akan memuntahkan (/ (- 17 2) 3)yang memiliki kelemahan yaitu kurang dapat dibaca dan keuntungan menjadi ekspresi pelat yang valid. Itu bagian sublingu DSL / data.

Jonas Kölker
sumber
1
Aplikasi fungsi jauh lebih masuk akal bagi saya daripada threading, tapi itu sudah pasti kebiasaan. Jawaban bagus.
coredump