Cara membuat bahasa homoikonik

16

Menurut artikel ini , baris kode Lisp berikut mencetak "Hello world" ke output standar.

(format t "hello, world")

Lisp, yang merupakan bahasa homoikonik , dapat memperlakukan kode sebagai data dengan cara ini:

Sekarang bayangkan kita menulis makro berikut:

(defmacro backwards (expr) (reverse expr))

mundur adalah nama makro, yang mengambil ekspresi (direpresentasikan sebagai daftar), dan membalikkannya. Inilah "Halo, dunia" lagi, kali ini menggunakan makro:

(backwards ("hello, world" t format))

Ketika kompiler Lisp melihat baris kode itu, ia melihat atom pertama dalam daftar ( backwards), dan pemberitahuan bahwa itu menamai makro. Ini melewati daftar yang tidak dievaluasi ("hello, world" t format)ke makro, yang mengatur ulang daftar (format t "hello, world"). Daftar yang dihasilkan menggantikan ekspresi makro, dan itulah yang akan dievaluasi saat dijalankan. Lingkungan Lisp akan melihat bahwa atom pertamanya ( format) adalah fungsi, dan mengevaluasinya, meneruskannya dengan argumen lainnya.

Dalam Lisp mencapai tugas ini mudah (koreksi saya jika saya salah) karena kode diimplementasikan sebagai daftar ( s-ekspresi ?).

Sekarang lihat cuplikan OCaml ini (yang bukan bahasa homoikonik):

let print () =
    let message = "Hello world" in
    print_endline message
;;

Bayangkan Anda ingin menambahkan homoiconicity ke OCaml, yang menggunakan sintaks yang jauh lebih kompleks dibandingkan dengan Lisp. Bagaimana Anda akan melakukannya? Apakah bahasa tersebut harus memiliki sintaksis yang sangat mudah untuk mencapai homoiconicity?

EDIT : dari topik ini saya menemukan cara lain untuk mencapai homoiconicity yang berbeda dari Lisp: yang diterapkan dalam bahasa io . Mungkin sebagian menjawab pertanyaan ini.

Di sini, mari kita mulai dengan blok sederhana:

Io> plus := block(a, b, a + b)
==> method(a, b, 
        a + b
    )
Io> plus call(2, 3)
==> 5

Oke, jadi bloknya berfungsi. Blok plus menambahkan dua angka.

Sekarang mari kita lakukan introspeksi pada anak kecil ini.

Io> plus argumentNames
==> list("a", "b")
Io> plus code
==> block(a, b, a +(b))
Io> plus message name
==> a
Io> plus message next
==> +(b)
Io> plus message next name
==> +

Cetakan dingin suci panas. Anda tidak hanya bisa mendapatkan nama-nama params blok. Dan Anda tidak hanya dapat memperoleh string dari kode sumber lengkap blok. Anda dapat menyelinap masuk ke kode dan menelusuri pesan di dalamnya. Dan yang paling menakjubkan: sangat mudah dan alami. Sesuai dengan pencarian Io. Cermin Ruby tidak bisa melihat semua itu.

Tapi, whoa whoa, hei sekarang, jangan menyentuh tombol itu.

Io> plus message next setName("-")
==> -(b)
Io> plus
==> method(a, b, 
        a - b
    )
Io> plus call(2, 3)
==> -1
incud
sumber
1
Anda mungkin ingin melihat bagaimana Scala melakukan macro-nya
Bergi
1
@Bergi Scala memiliki pendekatan baru untuk makro: scala.meta .
Martin Berger
Saya selalu meskipun homoiconicity berlebihan. Dalam bahasa yang cukup kuat Anda selalu dapat mendefinisikan struktur pohon yang mencerminkan struktur bahasa itu sendiri, dan fungsi utilitas dapat ditulis untuk menerjemahkan ke dan dari bahasa sumber (dan / atau bentuk yang dikompilasi) sesuai kebutuhan. Ya, ini sedikit lebih mudah di LISPs, tetapi mengingat bahwa (a) sebagian besar pekerjaan pemrograman tidak harus metaprogramming dan (b) LISP telah mengorbankan kejelasan bahasa untuk memungkinkan hal ini, saya tidak berpikir tradeoff sepadan.
Periata Breatta
@PeriataBreatta Anda benar, tetapi keuntungan utama MP adalah bahwa MP memungkinkan abstraksi tanpa penalti run-time . Dengan demikian MP menyelesaikan ketegangan antara abstraksi dan kinerja, meskipun dengan biaya peningkatan kompleksitas bahasa. Apakah itu layak? Saya akan mengatakan fakta bahwa semua PL utama memiliki ekstensi MP menunjukkan bahwa banyak programmer yang bekerja menemukan penawaran trade-off MP bermanfaat.
Martin Berger

Jawaban:

10

Anda dapat membuat bahasa apa saja menjadi homoikonik. Pada dasarnya Anda melakukan ini dengan 'mirroring' bahasa (artinya untuk setiap konstruktor bahasa Anda menambahkan representasi yang sesuai dari konstruktor itu sebagai data, pikirkan AST). Anda juga perlu menambahkan beberapa operasi tambahan seperti kutipan dan tanda kutip. Itu kurang lebih itu.

Lisp memiliki itu sejak awal karena sintaksinya yang mudah, tetapi keluarga MetaML bahasa W. Taha menunjukkan bahwa itu mungkin dilakukan untuk bahasa apa pun.

Seluruh proses diuraikan dalam Pemodelan meta-pemrograman generatif yang homogen . Pengantar yang lebih ringan untuk bahan yang sama ada di sini .

Martin Berger
sumber
1
Koreksi saya jika saya salah. "mirroring" terkait dengan bagian kedua dari pertanyaan (homoiconicity dalam io lang), kan?
incud
@ Ignus Saya tidak yakin saya sepenuhnya memahami komentar Anda. Tujuan homoiconicity adalah untuk memungkinkan perlakuan kode sebagai data. Itu berarti segala bentuk kode harus memiliki representasi sebagai data. Ada beberapa cara untuk melakukan ini (mis. Kutipan kuasi AST, menggunakan tipe untuk membedakan kode dari data seperti yang dilakukan oleh pendekatan pementasan modular ringan), tetapi semua membutuhkan penggandaan / pencerminan sintaksis bahasa dalam beberapa bentuk.
Martin Berger
Saya berasumsi @Ignus akan mendapat manfaat dari melihat MetaOCaml? Apakah menjadi "homoikonik" hanya berarti dapat dikutip? Saya berasumsi bahwa bahasa multi-tahap seperti MetaML dan MetaOCaml melangkah lebih jauh?
Steven Shaw
1
@StevenShaw MetaOCaml sangat menarik, terutama BER MetaOCaml baru milik Oleg . Namun, ini agak terbatas karena hanya menjalankan run-time meta-programming, dan hanya mewakili kode melalui kuasi-quotes yang tidak se ekspresif ASTs.
Martin Berger
7

Compiler Ocaml ditulis dalam Ocaml itu sendiri, jadi tentu saja ada cara untuk memanipulasi Ocaml AST di Ocaml.

Orang mungkin membayangkan menambahkan tipe built-in ocaml_syntaxke bahasa, dan memiliki fungsi defmacrobuilt-in, yang mengambil input tipe, katakan

f : ocaml_syntax -> ocaml_syntax

Sekarang apa jenis dari defmacro? Nah itu benar-benar tergantung pada input, karena meskipun fadalah fungsi identitas, jenis potongan kode yang dihasilkan tergantung pada potongan sintaks yang dilewatkan.

Masalah ini tidak muncul di lisp, karena bahasa diketik secara dinamis, dan tidak ada jenis yang harus dianggap berasal dari makro itu sendiri pada waktu kompilasi. Salah satu solusinya adalah memiliki

defmacro : (ocaml_syntax -> ocaml_syntax) -> 'a

yang akan memungkinkan makro untuk digunakan dalam konteks apa pun . Tapi ini tidak aman, tentu saja, itu akan memungkinkan booluntuk digunakan sebagai pengganti string, crash program pada saat run-time.

Satu-satunya solusi berprinsip dalam bahasa yang diketik secara statis adalah memiliki tipe dependen di mana tipe hasil defmacroakan bergantung pada input. Hal-hal menjadi sangat rumit pada titik ini, dan saya akan mulai dengan menunjukkan Anda pada disertasi yang bagus oleh David Raymond Christiansen.

Kesimpulannya: memiliki sintaks yang rumit bukanlah masalah, karena ada banyak cara untuk mewakili sintaks di dalam bahasa, dan mungkin menggunakan meta-programing seperti quoteoperasi untuk menanamkan sintaks "sederhana" ke dalam internal ocaml_syntax.

Masalahnya adalah membuat ini diketik dengan baik, khususnya memiliki mekanisme makro run-time yang tidak memungkinkan untuk kesalahan ketik.

Memiliki waktu kompilasi mekanisme untuk macro dalam bahasa seperti Ocaml mungkin tentu saja, lihat misalnya MetaOcaml .

Juga mungkin berguna: Jane street pada program-meta di Ocaml

cody
sumber
2
MetaOCaml memiliki runtime-meta-programming, bukan compile-time meta-programming. Juga sistem mengetik MetaOCaml tidak memiliki tipe dependen. (MetaOCaml juga ditemukan tipe-tidak sehat!) Template Haskell memiliki pendekatan perantara yang menarik: setiap tahap aman-tipe, tetapi ketika memasuki tahap baru, kita harus melakukan pengecekan ulang jenis. Ini bekerja sangat baik dalam praktiknya dalam pengalaman saya, dan Anda tidak kehilangan manfaat dari keamanan tipe pada tahap akhir (run-time).
Martin Berger
@cody dimungkinkan untuk memiliki metaprogramming di OCaml juga dengan Extension Points , kan?
incud
@ Ignus Saya khawatir saya tidak tahu banyak tentang Extension Points, meskipun saya referensi di tautan ke blog Jane Street.
cody
1
Kompiler C saya ditulis dalam C, tetapi itu tidak berarti Anda dapat memanipulasi AST dalam C ...
BlueRaja - Danny Pflughoeft
2
@immibis: Jelas, tetapi jika itu yang dia maksudkan maka pernyataan itu kosong dan tidak ada hubungannya dengan pertanyaan ...
BlueRaja - Danny Pflughoeft
1

Sebagai contoh, pertimbangkan F # (berdasarkan OCaml). F # tidak sepenuhnya homoikonik, tetapi mendukung mendapatkan kode fungsi sebagai AST dalam kondisi tertentu.

Dalam F #, Anda printakan direpresentasikan sebagai Expryang dicetak sebagai:

Let (message, Value ("Hello world"), Call (None, print_endline, [message]))

Untuk menyoroti struktur dengan lebih baik, berikut ini cara alternatif untuk membuat hal yang sama Expr:

let messageVar = Var("message", typeof<string>)
let expr = Expr.Let(messageVar,
                    Expr.Value("Hello world"),
                    Expr.Call(print_endline_method, [Expr.Var(messageVar)]))
svick
sumber
Saya tidak memahaminya. Maksud Anda, F # memungkinkan Anda "membuat" AST ekspresi dan kemudian menjalankannya? Jika demikian, apa bedanya dengan bahasa yang memungkinkan Anda menggunakan eval(<string>)fungsi ini? ( Menurut banyak sumber, memiliki fungsi eval berbeda dari memiliki homoiconicity - apakah itu alasan mengapa Anda mengatakan F # tidak sepenuhnya homoiconic?)
incud
@ Ignus Anda dapat membangun AST sendiri, atau membiarkan kompiler melakukannya. Homoiconicity "memungkinkan semua kode dalam bahasa untuk diakses dan diubah sebagai data" . Di F #, Anda dapat mengakses beberapa kode sebagai data. (Misalnya, Anda harus menandai printdengan [<ReflectedDefinition>]atribut.)
svick