Tolong jelaskan beberapa poin Paul Graham tentang Lisp

146

Saya butuh bantuan untuk memahami beberapa poin dari Paul Graham's What Made Lisp Different .

  1. Konsep variabel baru. Dalam Lisp, semua variabel adalah pointer efektif. Nilai adalah apa yang memiliki tipe, bukan variabel, dan penugasan atau variabel mengikat berarti menyalin pointer, bukan apa yang mereka tunjuk.

  2. Jenis simbol. Simbol berbeda dari string karena Anda dapat menguji kesetaraan dengan membandingkan sebuah pointer.

  3. Notasi untuk kode menggunakan pohon simbol.

  4. Seluruh bahasa selalu tersedia. Tidak ada perbedaan nyata antara waktu baca, waktu kompilasi, dan runtime. Anda dapat mengkompilasi atau menjalankan kode saat membaca, membaca atau menjalankan kode saat kompilasi, dan membaca atau mengkompilasi kode saat runtime.

Apa maksud poin-poin ini? Bagaimana mereka berbeda dalam bahasa seperti C atau Java? Apakah ada bahasa lain selain bahasa keluarga Lisp yang memiliki konstruksi ini sekarang?

unj2
sumber
10
Saya tidak yakin bahwa tag fungsional-pemrograman dijamin di sini, karena sama-sama mungkin untuk menulis kode imperatif atau OO dalam banyak Lisps seperti halnya menulis kode fungsional - dan pada kenyataannya ada banyak Lisp non-fungsional kode sekitar. Saya menyarankan agar Anda menghapus tag fp dan menambahkan clojure sebagai gantinya - semoga ini dapat membawa beberapa masukan menarik dari Lispers berbasis JVM.
Michał Marczyk
58
Kami juga memiliki paul-grahamtag di sini? !!! Hebat ...
missingfaktor
@missingfaktor Mungkin butuh permintaan-
cat

Jawaban:

98

Penjelasan Matt baik-baik saja - dan dia mengambil perbandingan dengan C dan Java, yang saya tidak akan lakukan - tetapi untuk beberapa alasan saya sangat menikmati membahas topik ini sesekali, jadi - inilah kesempatan saya pada sebuah jawaban.

Pada poin (3) dan (4):

Poin (3) dan (4) pada daftar Anda tampaknya paling menarik dan masih relevan sekarang.

Untuk memahaminya, penting untuk memiliki gambaran yang jelas tentang apa yang terjadi dengan kode Lisp - dalam bentuk aliran karakter yang diketikkan oleh programmer - dalam perjalanannya untuk dieksekusi. Mari kita gunakan contoh nyata:

;; a library import for completeness,
;; we won't concern ourselves with it
(require '[clojure.contrib.string :as str])

;; this is the interesting bit:
(println (str/replace-re #"\d+" "FOO" "a123b4c56"))

Potongan kode Clojure ini dicetak aFOObFOOcFOO. Perhatikan bahwa Clojure bisa dibilang tidak sepenuhnya memenuhi poin keempat dalam daftar Anda, karena waktu baca tidak benar-benar terbuka untuk kode pengguna; Saya akan membahas apa artinya jika ini sebaliknya.

Jadi, misalkan kita punya kode ini di file di suatu tempat dan kami meminta Clojure untuk mengeksekusinya. Juga, mari kita asumsikan (demi kesederhanaan) bahwa kami telah melewati impor perpustakaan. Bit yang menarik dimulai pada (printlndan berakhir di )ujung ke kanan. Ini lexed / parsed seperti yang diharapkan, tetapi sudah muncul poin penting: hasilnya bukan beberapa representasi AST khusus kompiler khusus - itu hanya struktur data Clojure / Lisp biasa , yaitu daftar bersarang yang berisi sekelompok simbol, string dan - dalam hal ini - objek pola regex terkompilasi tunggal yang sesuai dengan#"\d+"literal (lebih lanjut tentang ini di bawah). Beberapa Lisps menambahkan tikungan kecil mereka sendiri untuk proses ini, tetapi Paul Graham sebagian besar mengacu pada Common Lisp. Pada poin yang relevan dengan pertanyaan Anda, Clojure mirip dengan CL.

Seluruh bahasa pada waktu kompilasi:

Setelah titik ini, semua kompiler berurusan dengan (ini juga berlaku untuk interpreter Lisp; kode Clojure selalu dikompilasi) adalah struktur data Lisp yang digunakan para programmer Lisp untuk memanipulasi. Pada titik ini kemungkinan yang luar biasa menjadi jelas: mengapa tidak mengizinkan programmer Lisp untuk menulis fungsi Lisp yang memanipulasi data Lisp yang mewakili program Lisp dan output mentransformasikan data yang mewakili program yang diubah, untuk digunakan sebagai pengganti yang asli? Dengan kata lain - mengapa tidak mengizinkan programmer Lisp untuk mendaftarkan fungsinya sebagai plug-in compiler, yang disebut macro di Lisp? Dan memang setiap sistem Lisp yang layak memiliki kapasitas ini.

Jadi, makro adalah fungsi Lisp biasa yang beroperasi pada representasi program pada waktu kompilasi, sebelum fase kompilasi akhir ketika kode objek aktual dipancarkan. Karena tidak ada batasan pada jenis makro kode yang diizinkan untuk dijalankan (khususnya, kode yang mereka jalankan sering sendiri ditulis dengan penggunaan fasilitas makro secara liberal), dapat dikatakan bahwa "seluruh bahasa tersedia pada waktu kompilasi ".

Seluruh bahasa saat dibaca:

Mari kita kembali ke #"\d+"regex literal. Seperti disebutkan di atas, ini akan ditransformasikan ke objek pola dikompilasi yang sebenarnya pada waktu baca, sebelum kompiler mendengar penyebutan pertama dari kode baru sedang disiapkan untuk kompilasi. Bagaimana ini bisa terjadi?

Nah, cara Clojure saat ini diimplementasikan, gambarnya agak berbeda dari apa yang ada dalam pikiran Paul Graham, meskipun segala sesuatu mungkin terjadi dengan peretasan yang cerdas . Dalam Common Lisp, ceritanya akan sedikit lebih bersih secara konseptual. Dasar-dasarnya serupa: Pembaca Lisp adalah mesin negara yang, selain melakukan transisi keadaan dan akhirnya menyatakan apakah telah mencapai "negara penerima", meludahkan struktur data Lisp yang diwakili karakter. Dengan demikian karakter 123menjadi nomor 123dll. Poin penting datang sekarang: mesin negara ini dapat dimodifikasi oleh kode pengguna. (Seperti disebutkan sebelumnya, itu sepenuhnya benar dalam kasus CL; untuk Clojure, diperlukan peretasan (tidak disarankan & tidak digunakan dalam praktik). Tapi saya ngelantur, ini adalah artikel PG yang seharusnya saya uraikan, jadi ...)

Jadi, jika Anda seorang programmer Common Lisp dan Anda menyukai ide literal vektor gaya Clojure, Anda bisa langsung menyambungkan ke pembaca fungsi untuk bereaksi secara tepat terhadap beberapa urutan karakter - [atau #[mungkin - dan memperlakukannya sebagai mulai dari vektor literal yang berakhir pada pencocokan ]. Fungsi seperti ini disebut makro pembaca dan seperti makro biasa, ia dapat mengeksekusi kode Lisp apa pun, termasuk kode yang telah ditulis dengan notasi funky yang diaktifkan oleh makro pembaca yang terdaftar sebelumnya. Jadi ada seluruh bahasa saat membaca untuk Anda.

Membungkusnya:

Sebenarnya, apa yang telah ditunjukkan sejauh ini adalah bahwa seseorang dapat menjalankan fungsi Lisp reguler pada waktu baca atau waktu kompilasi; satu langkah yang perlu diambil dari sini untuk memahami bagaimana membaca dan menyusun itu sendiri dimungkinkan saat membaca, menyusun atau menjalankan waktu adalah untuk menyadari bahwa membaca dan menyusun sendiri dilakukan oleh fungsi Lisp. Anda bisa memanggil readatau evalkapan saja untuk membaca dalam data Lisp dari aliran karakter atau mengkompilasi & mengeksekusi kode Lisp, masing-masing. Itu seluruh bahasa di sana, sepanjang waktu.

Perhatikan bagaimana fakta bahwa Lisp memenuhi poin (3) dari daftar Anda sangat penting untuk cara Lisp memenuhi poin (4) - aroma makro yang disediakan oleh Lisp sangat bergantung pada kode yang diwakili oleh data Lisp biasa, yang merupakan sesuatu yang dimungkinkan oleh (3). Kebetulan, hanya aspek "tree-ish" dari kode yang benar-benar penting di sini - Anda mungkin dapat memiliki Lisp yang ditulis menggunakan XML.

Michał Marczyk
sumber
4
Hati-hati: dengan mengatakan "makro biasa (kompiler)", Anda hampir menyiratkan bahwa makro kompiler adalah makro "biasa", ketika di Common Lisp (setidaknya), "makro kompiler" adalah hal yang sangat spesifik dan berbeda: lispworks. com / dokumentasi / lw51 / CLHS / Badan / ...
Ken
Ken: Tangkapan bagus, terima kasih! Saya akan mengubahnya menjadi "makro biasa", yang saya pikir tidak mungkin membuat orang tersandung.
Michał Marczyk
Jawaban yang fantastis. Saya belajar lebih banyak dari itu dalam 5 menit daripada yang saya miliki dalam jam googling / merenungkan pertanyaan. Terima kasih.
Charlie Flowers
Sunting: argh, salah mengerti kalimat yang sedang berjalan. Dikoreksi untuk tata bahasa (perlu "rekan" untuk menerima suntingan saya).
Tatiana Racheva
Ekspresi S dan XML dapat menentukan struktur yang sama tetapi XML jauh lebih bertele-tele dan karenanya tidak cocok sebagai sintaksis.
Sylwester
66

1) Konsep variabel baru. Dalam Lisp, semua variabel adalah pointer efektif. Nilai adalah apa yang memiliki tipe, bukan variabel, dan penugasan atau variabel mengikat berarti menyalin pointer, bukan apa yang mereka tunjuk.

(defun print-twice (it)
  (print it)
  (print it))

'it' adalah variabel. Itu bisa terikat dengan nilai APA SAJA. Tidak ada batasan dan tidak ada tipe yang terkait dengan variabel. Jika Anda memanggil fungsi, argumen tidak perlu disalin. Variabelnya mirip dengan pointer. Ini memiliki cara untuk mengakses nilai yang terikat ke variabel. Tidak perlu menyimpan memori. Kita dapat melewatkan objek data apa pun ketika kita memanggil fungsi: ukuran apa pun dan jenis apa pun.

Objek data memiliki 'tipe' dan semua objek data dapat di-query untuk 'tipenya'.

(type-of "abc")  -> STRING

2) Jenis simbol. Simbol berbeda dari string karena Anda dapat menguji kesetaraan dengan membandingkan sebuah pointer.

Simbol adalah objek data dengan nama. Biasanya nama dapat digunakan untuk menemukan objek:

|This is a Symbol|
this-is-also-a-symbol

(find-symbol "SIN")   ->  SIN

Karena simbol adalah objek data nyata, kita dapat menguji apakah mereka adalah objek yang sama:

(eq 'sin 'cos) -> NIL
(eq 'sin 'sin) -> T

Sebagai contoh, ini memungkinkan kita untuk menulis kalimat dengan simbol:

(defvar *sentence* '(mary called tom to tell him the price of the book))

Sekarang kita dapat menghitung jumlah THE dalam kalimat:

(count 'the *sentence*) ->  2

Dalam Common Lisp simbol tidak hanya memiliki nama, tetapi mereka juga dapat memiliki nilai, fungsi, daftar properti, dan paket. Jadi simbol dapat digunakan untuk memberi nama variabel atau fungsi. Daftar properti biasanya digunakan untuk menambahkan meta-data ke simbol.

3) Notasi untuk kode menggunakan pohon simbol.

Lisp menggunakan struktur data dasar untuk mewakili kode.

Daftar (* 3 2) dapat berupa data dan kode:

(eval '(* 3 (+ 2 5))) -> 21

(length '(* 3 (+ 2 5))) -> 3

Pohon:

CL-USER 8 > (sdraw '(* 3 (+ 2 5)))

[*|*]--->[*|*]--->[*|*]--->NIL
 |        |        |
 v        v        v
 *        3       [*|*]--->[*|*]--->[*|*]--->NIL
                   |        |        |
                   v        v        v
                   +        2        5

4) Seluruh bahasa selalu tersedia. Tidak ada perbedaan nyata antara waktu baca, waktu kompilasi, dan runtime. Anda dapat mengkompilasi atau menjalankan kode saat membaca, membaca atau menjalankan kode saat kompilasi, dan membaca atau mengkompilasi kode saat runtime.

Lisp menyediakan fungsi BACA untuk membaca data dan kode dari teks, LOAD untuk memuat kode, EVAL untuk mengevaluasi kode, COMPILE untuk mengkompilasi kode dan PRINT untuk menulis data dan kode ke teks.

Fungsi-fungsi ini selalu tersedia. Mereka tidak pergi. Mereka dapat menjadi bagian dari program apa pun. Itu berarti setiap program dapat membaca, memuat, mengevaluasi atau mencetak kode - selalu.

Bagaimana mereka berbeda dalam bahasa seperti C atau Java?

Bahasa-bahasa itu tidak memberikan simbol, kode sebagai data, atau evaluasi runtime data sebagai kode. Objek data dalam C biasanya tidak diketik.

Apakah ada bahasa lain selain bahasa keluarga LISP yang memiliki konstruksi ini sekarang?

Banyak bahasa memiliki beberapa kemampuan ini.

Perbedaan:

Dalam Lisp, kemampuan ini dirancang ke dalam bahasa sehingga mudah digunakan.

Rainer Joswig
sumber
33

Untuk poin (1) dan (2), dia berbicara secara historis. Variabel Java hampir sama, itulah sebabnya Anda perlu memanggil .equals () untuk membandingkan nilai.

(3) berbicara tentang ekspresi-S. Program Lisp ditulis dalam sintaks ini, yang memberikan banyak keuntungan dibandingkan sintaks ad-hoc seperti Java dan C, seperti menangkap pola berulang dalam makro dalam cara yang jauh lebih bersih daripada makro C atau templat C ++, dan memanipulasi kode dengan daftar inti yang sama operasi yang Anda gunakan untuk data.

(4) mengambil C sebagai contoh: bahasanya benar-benar dua bahasa yang berbeda: hal-hal seperti if () dan while (), dan preprocessor. Anda menggunakan preprocessor untuk menghemat keharusan mengulangi diri sendiri sepanjang waktu, atau untuk melewatkan kode dengan # if / # ifdef. Namun kedua bahasa tersebut cukup terpisah, dan Anda tidak dapat menggunakan while () pada waktu kompilasi seperti #if.

C ++ membuat ini lebih buruk dengan template. Lihat beberapa referensi tentang metaprogramming template, yang menyediakan cara menghasilkan kode pada waktu kompilasi, dan sangat sulit bagi non-pakar untuk membungkus kepala mereka. Selain itu, ini benar-benar kumpulan peretasan dan trik menggunakan templat dan makro yang tidak dapat disediakan oleh kompiler kelas satu - jika Anda membuat kesalahan sintaksis sederhana, kompiler tidak dapat memberikan Anda pesan kesalahan yang jelas.

Nah, dengan Lisp, Anda memiliki semua ini dalam satu bahasa. Anda menggunakan hal yang sama untuk menghasilkan kode pada saat dijalankan seperti yang Anda pelajari di hari pertama. Ini bukan untuk menyarankan metaprogramming itu sepele, tetapi tentu saja lebih mudah dengan bahasa kelas satu dan dukungan kompiler.

Matt Curtis
sumber
7
Oh juga, kekuatan ini (dan kesederhanaan) sekarang sudah berusia lebih dari 50 tahun, dan cukup mudah untuk diterapkan sehingga seorang programmer pemula dapat mengeluarkannya dengan bimbingan minimal, dan belajar tentang dasar-dasar bahasa. Anda tidak akan mendengar klaim serupa tentang Java, C, Python, Perl, Haskell, dll. Sebagai proyek pemula yang baik!
Matt Curtis
9
Saya tidak berpikir variabel Java seperti simbol Lisp sama sekali. Tidak ada notasi untuk simbol di Jawa, dan satu-satunya hal yang dapat Anda lakukan dengan variabel adalah mendapatkan sel nilainya. String dapat diinternir tetapi mereka biasanya bukan nama, sehingga bahkan tidak masuk akal untuk berbicara tentang apakah mereka dapat dikutip, dievaluasi, disahkan, dll.
Ken
2
Lebih dari 40 tahun mungkin lebih akurat :), @ Ken: Saya pikir maksudnya 1) variabel non-primitif di java adalah dengan refence, yang mirip dengan lisp dan 2) string yang diinternir di java mirip dengan simbol di lisp - tentu saja, seperti yang Anda katakan, Anda tidak dapat mengutip atau mengevaluasi string / kode yang diinternir di Jawa, jadi mereka masih sangat berbeda.
3
@Dan - Tidak yakin kapan implementasi pertama disatukan, tetapi makalah McCarthy awal tentang perhitungan simbolik diterbitkan pada tahun 1960.
Inaimathi
Java memang memiliki dukungan parsial / tidak teratur untuk "simbol" dalam bentuk Foo.class / foo.getClass () - yaitu objek tipe-of-a-type Class <Foo> agak analog - seperti nilai enum, untuk gelar. Tapi bayangan simbol Lisp sangat minim.
BRPocock
-3

Poin (1) dan (2) juga cocok dengan Python. Dengan mengambil contoh sederhana "a = str (82.4)" penerjemah pertama-tama membuat objek floating point dengan nilai 82,4. Kemudian ia memanggil konstruktor string yang kemudian mengembalikan string dengan nilai '82 .4 '. Tanda 'a' di sisi kiri hanyalah label untuk objek string itu. Objek floating point asli adalah sampah yang dikumpulkan karena tidak ada referensi lagi untuk itu.

Dalam Skema semuanya diperlakukan sebagai objek dengan cara yang sama. Saya tidak yakin tentang Common Lisp. Saya akan mencoba untuk menghindari berpikir dalam hal konsep C / C ++. Mereka memperlambat langkah saya ketika saya mencoba untuk mendapatkan kesederhanaan indah dari Lisps.

CyberFonic
sumber