Saya belajar Skema dari SICP dan saya mendapat kesan bahwa sebagian besar dari apa yang membuat Skema dan, terlebih lagi, LISP khusus adalah sistem makro. Tetapi, karena makro diperluas pada waktu kompilasi, mengapa orang tidak membuat sistem makro yang setara untuk C / Python / Java / apa pun? Misalnya, seseorang dapat mengikat python
perintah expand-macros | python
atau apa pun. Kode masih akan portabel untuk orang-orang yang tidak menggunakan sistem makro, seseorang hanya akan memperluas makro sebelum menerbitkan kode. Tapi saya tidak tahu hal seperti itu kecuali template di C ++ / Haskell, yang saya kumpulkan tidak benar-benar sama. Bagaimana dengan LISP, jika ada, membuatnya lebih mudah untuk menerapkan sistem makro?
21
Jawaban:
Banyak Lispers akan memberi tahu Anda bahwa apa yang membuat Lisp istimewa adalah homoikonisitas , yang berarti bahwa sintaksis kode diwakili menggunakan struktur data yang sama dengan data lainnya. Misalnya, inilah fungsi sederhana (menggunakan sintaks skema) untuk menghitung sisi miring dari segitiga siku-siku dengan panjang sisi yang diberikan:
Sekarang, homoiconicity mengatakan bahwa kode di atas sebenarnya direpresentasikan sebagai struktur data (khususnya, daftar daftar) dalam kode Lisp. Jadi, pertimbangkan daftar berikut dan lihat bagaimana mereka "merekatkan":
(define #2# #3#)
(hypot x y)
(sqrt #4#)
(+ #5# #6#)
(square x)
(square y)
Macro memungkinkan Anda untuk memperlakukan kode sumber hanya seperti itu: daftar barang. Masing-masing 6 "sublists" berisi baik pointer ke daftar lain, atau untuk simbol (dalam contoh ini:
define
,hypot
,x
,y
,sqrt
,+
,square
).Jadi, bagaimana kita bisa menggunakan homoiconicity untuk "memilih" sintaks dan membuat macro? Ini contoh sederhana. Mari kita implementasikan kembali
let
makro, yang akan kita panggilmy-let
. Sebagai pengingat,harus berkembang menjadi
Berikut ini implementasi dengan menggunakan skema "penggantian nama eksplisit" makro † :
The
form
parameter terikat ke bentuk yang sebenarnya, jadi misalnya kita, itu akan menjadi(my-let ((foo 1) (bar 2)) (+ foo bar))
. Jadi, mari kita bekerja melalui contoh:cadr
meraih((foo 1) (bar 2))
bagian formulir.Kemudian, kami mengambil tubuh dari formulir.
cddr
meraih((+ foo bar))
bagian formulir. (Perhatikan bahwa ini dimaksudkan untuk mengambil semua subformulir setelah penjilidan; jadi jika bentuknyamaka tubuh akan menjadi
((debug foo) (debug bar) (+ foo bar))
.)lambda
ekspresi dan panggilan yang dihasilkan menggunakan binding dan body yang telah kita kumpulkan. Backtick disebut "quasiquote", yang berarti memperlakukan segala sesuatu di dalam quasiquote sebagai datum literal, kecuali bit setelah koma ("unquote").(rename 'lambda)
sarana untuk menggunakanlambda
mengikat berlaku ketika makro ini didefinisikan , daripada apa pun yanglambda
mengikat mungkin sekitar ketika makro ini digunakan . (Ini dikenal sebagai kebersihan .)(map car bindings)
mengembalikan(foo bar)
: datum pertama di masing-masing binding.(map cadr bindings)
mengembalikan(1 2)
: datum kedua di masing-masing binding.,@
tidak "splicing", yang digunakan untuk ekspresi yang mengembalikan daftar: itu menyebabkan elemen daftar disisipkan ke dalam hasil, bukan daftar itu sendiri.(($lambda (foo bar) (+ foo bar)) 1 2)
, di mana di$lambda
sini merujuk pada yang diganti namalambda
.Mudah, bukan? ;-) (Jika tidak mudah bagi Anda, bayangkan betapa sulitnya menerapkan sistem makro untuk bahasa lain.)
Jadi, Anda dapat memiliki sistem makro untuk bahasa lain, jika Anda memiliki cara untuk dapat "memilih" kode sumber dengan cara yang tidak kikuk. Ada beberapa upaya untuk ini. Misalnya, sweet.js melakukan ini untuk JavaScript.
† Untuk Skema berpengalaman yang membaca ini, saya sengaja memilih untuk menggunakan makro penggantian nama eksplisit sebagai kompromi tengah antara
defmacro
yang digunakan oleh dialek Lisp lain, dansyntax-rules
(yang akan menjadi cara standar untuk mengimplementasikan makro semacam itu dalam Skema). Saya tidak ingin menulis dalam dialek Lisp lain, tetapi saya tidak ingin mengasingkan non-Schemers yang tidak terbiasasyntax-rules
.Untuk referensi, inilah
my-let
makro yang menggunakansyntax-rules
:Versi yang sesuai
syntax-case
terlihat sangat mirip:Perbedaan antara keduanya adalah bahwa semua yang ada di dalamnya
syntax-rules
telah#'
diterapkan secara implisit , jadi Anda hanya dapat memiliki pasangan pola / templatesyntax-rules
, karenanya sepenuhnya deklaratif. Sebaliknya, dalamsyntax-case
, bit setelah pola adalah kode aktual yang, pada akhirnya, harus mengembalikan objek sintaks (#'(...)
), tetapi dapat mengandung kode lain juga.sumber
syntax-rules
, yang murni bersifat deklaratif. Untuk makro yang rumit, saya dapat menggunakansyntax-case
, yang sebagian bersifat deklaratif dan sebagian prosedural. Dan kemudian ada penggantian nama eksplisit, yang murni prosedural. (Sebagian besar implementasi Skema akan memberikansyntax-case
ER atau ER. Saya belum pernah melihat yang menyediakan keduanya. Keduanya berkekuatan setara.)Pendapat berbeda: Homoniklikat Lisp jauh lebih tidak berguna daripada yang Anda yakini.
Untuk memahami makro sintaksis, penting untuk memahami kompiler. Tugas kompiler adalah mengubah kode yang bisa dibaca manusia menjadi kode yang dapat dieksekusi. Dari perspektif tingkat yang sangat tinggi, ini memiliki dua fase keseluruhan: parsing dan pembuatan kode .
Parsing adalah proses membaca kode, menafsirkannya sesuai dengan seperangkat aturan formal, dan mengubahnya menjadi struktur pohon, umumnya dikenal sebagai AST (Abstract Syntax Tree). Untuk semua keragaman di antara bahasa pemrograman, ini adalah satu kesamaan yang luar biasa: pada dasarnya setiap bahasa pemrograman tujuan umum diurai menjadi struktur pohon.
Pembuatan kode mengambil AST parser sebagai inputnya, dan mengubahnya menjadi kode yang dapat dieksekusi melalui penerapan aturan formal. Dari perspektif kinerja, ini adalah tugas yang jauh lebih sederhana; banyak kompiler bahasa tingkat tinggi menghabiskan 75% atau lebih dari waktu mereka untuk parsing.
Yang perlu diingat tentang Lisp adalah bahwa itu sangat, sangat tua. Di antara bahasa pemrograman, hanya FORTRAN yang lebih tua dari Lisp. Jauh di masa lalu, penguraian (bagian kompilasi yang lambat) dianggap sebagai seni yang gelap dan misterius. Makalah asli John McCarthy tentang teori Lisp (ketika itu hanya sebuah ide yang dia tidak pernah berpikir dapat benar-benar diterapkan sebagai bahasa pemrograman komputer nyata) menggambarkan sintaksis yang agak lebih kompleks dan ekspresif daripada "ekspresi-S" modern di mana-mana untuk semuanya. "notasi. Itu terjadi kemudian, ketika orang mencoba untuk benar-benar mengimplementasikannya. Karena parsing tidak dipahami dengan baik pada saat itu, mereka pada dasarnya menyadapnya dan membodohi sintaksis ke dalam struktur pohon homoikonik untuk membuat pekerjaan parser sama sekali sepele. Hasil akhirnya adalah bahwa Anda (pengembang) harus melakukan banyak pengurai Bekerja untuk itu dengan menulis AST formal langsung ke kode Anda. Homoiconicity tidak "membuat makro jadi lebih mudah" sebanyak itu membuat menulis segala sesuatu yang jauh lebih sulit!
Masalah dengan ini adalah, terutama dengan pengetikan dinamis, sangat sulit bagi ekspresi-S untuk membawa banyak informasi semantik bersamanya. Ketika semua sintaks Anda adalah jenis yang sama (daftar daftar), tidak ada banyak cara konteks yang disediakan oleh sintaks, dan sistem makro memiliki sangat sedikit untuk dikerjakan.
Teori kompiler telah berjalan jauh sejak 1960-an ketika Lisp ditemukan, dan sementara hal-hal yang dikerjakannya mengesankan untuk zamannya, mereka terlihat agak primitif sekarang. Untuk contoh sistem pemrograman metamodern, lihatlah bahasa Boo (yang sayangnya kurang dihargai). Boo diketik secara statis, berorientasi objek, dan open-source, sehingga setiap node AST memiliki tipe dengan struktur yang terdefinisi dengan baik yang dapat dibaca oleh pengembang makro. Bahasa ini memiliki sintaksis yang relatif sederhana yang terinspirasi oleh Python, dengan berbagai kata kunci yang memberikan makna semantik intrinsik pada struktur pohon yang dibangun darinya, dan metaprogramming-nya memiliki sintaks kuasiquote yang intuitif untuk menyederhanakan pembuatan node AST baru.
Inilah makro yang saya buat kemarin ketika saya menyadari saya menerapkan pola yang sama ke banyak tempat yang berbeda dalam kode GUI, di mana saya akan memanggil
BeginUpdate()
kontrol UI, melakukan pembaruan ditry
blok, dan kemudian memanggilEndUpdate()
:The
macro
perintah ini, pada kenyataannya, makro itu sendiri , yang mengambil tubuh makro sebagai masukan dan menghasilkan kelas untuk memproses makro. Itu menggunakan nama makro sebagai variabel yang berdiri untukMacroStatement
node AST yang mewakili permintaan makro. [| ... |] adalah blok kuasiquote, menghasilkan AST yang sesuai dengan kode di dalamnya, dan di dalam blok ququote, simbol $ menyediakan fasilitas "unquote", menggantikan dalam node seperti yang ditentukan.Dengan ini, dimungkinkan untuk menulis:
dan mengembangkannya ke:
Mengekspresikan makro dengan cara ini lebih sederhana dan lebih intuitif daripada dalam makro Lisp, karena pengembang tahu struktur
MacroStatement
dan tahu cara kerjaArguments
danBody
properti, dan bahwa pengetahuan yang melekat dapat digunakan untuk mengekspresikan konsep yang terlibat dalam sangat intuitif cara. Ini juga lebih aman, karena kompiler mengetahui strukturnyaMacroStatement
, dan jika Anda mencoba kode sesuatu yang tidak valid untukMacroStatement
, kompiler akan segera menangkapnya dan melaporkan kesalahan alih-alih Anda tidak tahu sampai sesuatu meledak pada Anda di runtime.Mencangkokkan makro ke Haskell, Python, Java, Scala, dll. Tidak sulit karena bahasa ini bukan homoikonik; sulit karena bahasa tidak dirancang untuk mereka, dan itu berfungsi dengan baik ketika hierarki AST bahasa Anda dirancang dari bawah ke atas untuk diperiksa dan dimanipulasi oleh sistem makro. Saat Anda bekerja dengan bahasa yang dirancang dengan metaprogramming dari awal, makro jauh lebih sederhana dan lebih mudah untuk dikerjakan!
sumber
if...
tidak terlihat seperti pemanggilan fungsi misalnya. Saya tidak tahu Boo, tetapi bayangkan Boo tidak memiliki pencocokan pola, dapatkah Anda memperkenalkannya dengan sintaksnya sendiri sebagai makro? Maksud saya adalah - setiap makro baru di Lisp terasa 100% alami, dalam bahasa lain mereka bekerja, tetapi Anda dapat melihat jahitannya.Bagaimana? Semua kode dalam SICP ditulis dalam gaya bebas makro. Tidak ada makro di SICP. Hanya dalam catatan kaki di halaman 373 makro pernah disebutkan.
Mereka belum tentu. Lisp menyediakan makro dalam penerjemah dan kompiler. Jadi mungkin tidak ada waktu kompilasi. Jika Anda memiliki juru bahasa Lisp, makro diperluas pada waktu eksekusi. Karena banyak sistem Lisp memiliki kompiler di papan, seseorang dapat menghasilkan kode dan mengompilasinya saat runtime.
Mari kita uji menggunakan SBCL, implementasi Common Lisp.
Mari kita beralih SBCL ke Penerjemah:
Sekarang kita mendefinisikan makro. Makro mencetak sesuatu ketika dipanggil untuk kode diperluas. Kode yang dihasilkan tidak dicetak.
Sekarang mari kita gunakan makro:
Lihat. Dalam kasus di atas Lisp tidak melakukan apa pun. Makro tidak diperluas pada waktu definisi.
Tetapi saat runtime, ketika kode digunakan, makro diperluas.
Sekali lagi, saat runtime, ketika kode digunakan, makro diperluas.
Perhatikan bahwa SBCL akan berkembang hanya sekali ketika menggunakan kompiler. Tetapi berbagai implementasi Lisp juga menyediakan interpreter - seperti SBCL.
Mengapa makro mudah di Lisp? Yah, itu tidak mudah. Hanya di Lisps, dan ada banyak, yang memiliki dukungan makro bawaan. Karena banyak Lisps datang dengan mesin yang luas untuk makro, sepertinya mudah. Tetapi mekanisme makro bisa sangat rumit.
sumber
eval
atauload
kode dalam bahasa Lisp apa pun, makro di dalamnya akan diproses juga. Sedangkan jika Anda menggunakan sistem preprosesor seperti yang diusulkan dalam pertanyaan Anda,eval
dan sejenisnya tidak akan mendapat manfaat dari ekspansi makro.read
dalam Lisp. Perbedaan ini penting, karenaeval
bekerja pada struktur data daftar aktual (seperti yang disebutkan dalam jawaban saya), bukan pada bentuk tekstual. Jadi Anda dapat menggunakan(eval '(+ 1 1))
dan mendapatkan kembali 2, tetapi jika Anda(eval "(+ 1 1)")
, Anda mendapatkan kembali"(+ 1 1)"
(string). Anda gunakanread
untuk mendapatkan dari"(+ 1 1)"
(string 7 karakter) ke(+ 1 1)
(daftar dengan satu simbol dan dua fixnums).read
waktu. Mereka bekerja pada waktu kompilasi dalam arti bahwa jika Anda memiliki kode seperti(and (test1) (test2))
, itu akan diperluas menjadi(if (test1) (test2) #f)
(dalam Skema) hanya sekali, ketika kode dimuat, daripada setiap kali kode dijalankan, tetapi jika Anda melakukan sesuatu seperti(eval '(and (test1) (test2)))
, yang akan mengkompilasi (dan memperluas makro) ekspresi itu dengan tepat, saat runtime.eval
berfungsi pada string teks, dan kemampuannya untuk modifikasi sintaksis jauh lebih tidak bersemangat dan / atau rumit.Homoiconicity membuatnya lebih mudah untuk mengimplementasikan macro. Gagasan bahwa kode adalah data dan data adalah kode yang memungkinkan untuk lebih atau kurang (kecuali penangkapan pengenal yang disengaja, dipecahkan oleh makro higienis ) untuk secara bebas menggantikan satu dengan yang lainnya. Lisp dan Skema menjadikan ini lebih mudah dengan sintaksis ekspresi-S yang terstruktur secara seragam dan dengan demikian mudah diubah menjadi AST yang merupakan dasar dari Makro Sintaksis .
Bahasa tanpa S-Expressions atau Homoiconicity akan mengalami kesulitan dalam mengimplementasikan Syntactic Macros meskipun masih bisa dilakukan. Proyek Kepler sedang mencoba untuk memperkenalkan mereka ke Scala misalnya.
Masalah terbesar dengan penggunaan sintaks makro selain dari non-homoiconicity, adalah masalah sintaksis yang dihasilkan secara sewenang-wenang. Mereka menawarkan fleksibilitas dan kekuatan yang luar biasa tetapi dengan harga yang kode sumber Anda mungkin tidak lagi mudah dipahami atau dipelihara.
sumber