Wyjaśnienie Matta jest całkowicie w porządku - i próbuje porównać z C i Javą, czego nie zrobię - ale z jakiegoś powodu naprawdę lubię omawiać ten temat od czasu do czasu, więc - oto moja szansa w odpowiedzi.
W punktach (3) i (4):
Punkty (3) i (4) na Twojej liście wydają się teraz najbardziej interesujące i nadal aktualne.
Aby je zrozumieć, warto mieć jasny obraz tego, co dzieje się z kodem Lispa - w postaci strumienia znaków wpisywanych przez programistę - na drodze do wykonania. Posłużmy się konkretnym przykładem:
;; 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"))
Ten fragment kodu Clojure zostanie wydrukowany aFOObFOOcFOO
. Zauważ, że Clojure prawdopodobnie nie spełnia w pełni czwartego punktu na twojej liście, ponieważ czas odczytu nie jest tak naprawdę otwarty na kod użytkownika; Omówię jednak, co by to znaczyło, gdyby było inaczej.
Więc załóżmy, że mamy ten kod gdzieś w pliku i prosimy Clojure o wykonanie go. Załóżmy również (dla uproszczenia), że przeszliśmy przez import biblioteki. Ciekawy fragment zaczyna się (println
i kończy )
najdalej po prawej stronie. Jest to leksowane / analizowane, jak można by się spodziewać, ale już pojawia się ważna kwestia: wynikiem nie jest jakaś specjalna reprezentacja AST specyficzna dla kompilatora - to tylko zwykła struktura danych Clojure / Lisp , a mianowicie zagnieżdżona lista zawierająca kilka symboli, stringi i - w tym przypadku - pojedynczy skompilowany obiekt wzorca wyrażenia regularnego odpowiadający plikowi#"\d+"
dosłowne (więcej na ten temat poniżej). Niektóre Lispy dodają własne małe zwroty akcji do tego procesu, ale Paul Graham miał na myśli głównie Common Lisp. W kwestiach istotnych dla twojego pytania Clojure jest podobny do CL.
Cały język w czasie kompilacji:
Po tym momencie kompilator zajmuje się wszystkimi (dotyczyłoby to również interpretera Lispa; kod Clojure jest zawsze kompilowany) to struktury danych Lispa, którymi programiści Lisp są wykorzystywani. W tym momencie staje się oczywista wspaniała możliwość: dlaczego nie pozwolić programistom Lispa na pisanie funkcji Lispa, które manipulują danymi Lispa reprezentującymi programy Lispa i wyświetlają przekształcone dane reprezentujące przekształcone programy, które mają być używane zamiast oryginałów? Innymi słowy - dlaczego nie pozwolić programistom Lisp na rejestrowanie ich funkcji jako pewnego rodzaju wtyczki kompilatora, zwane makrami w Lispie? I rzeczywiście, każdy przyzwoity system Lisp ma taką pojemność.
Tak więc makra są zwykłymi funkcjami Lispa działającymi na reprezentacji programu w czasie kompilacji, przed końcową fazą kompilacji, kiedy emitowany jest rzeczywisty kod obiektowy. Ponieważ nie ma ograniczeń co do rodzajów makr kodu, które mogą być uruchamiane (w szczególności kod, który one uruchamiają, jest często sam napisany z liberalnym wykorzystaniem funkcji makr), można powiedzieć, że „cały język jest dostępny w czasie kompilacji ”.
Cały język w czasie czytania:
Wróćmy do tego #"\d+"
dosłownego wyrażenia regularnego. Jak wspomniano powyżej, zostaje on przekształcony w rzeczywisty skompilowany obiekt wzorca w czasie odczytu, zanim kompilator usłyszy pierwszą wzmiankę o przygotowywaniu nowego kodu do kompilacji. Jak to się stało?
Cóż, sposób, w jaki Clojure jest obecnie wdrażany, jest nieco inny niż to, co miał na myśli Paul Graham, chociaż wszystko jest możliwe dzięki sprytnemu hakowaniu . W Common Lisp historia byłaby nieco czystsza pod względem koncepcyjnym. Podstawy są jednak podobne: Lisp Reader jest maszyną stanu, która oprócz wykonywania przejść między stanami i ostatecznie deklarowania, czy osiągnęła „stan akceptacji”, wypluwa struktury danych Lispa, które reprezentują znaki. W ten sposób znaki 123
stają się liczbą 123
itd. Ważna kwestia: ten automat stanów może być modyfikowany przez kod użytkownika. (Jak wspomniano wcześniej, jest to całkowicie prawdziwe w przypadku CL; w przypadku Clojure wymagany jest hack (odradzany i nie używany w praktyce). Ale dygresję, powinienem rozwinąć artykuł PG, więc ...)
Tak więc, jeśli jesteś programistą Common Lisp i przypadkiem podoba Ci się pomysł literałów wektorowych w stylu Clojure, możesz po prostu podłączyć do czytnika funkcję, aby odpowiednio reagować na jakąś sekwencję znaków - [
lub #[
być może - i traktować ją jako początek literału wektora kończący się na dopasowaniu ]
. Taka funkcja nazywana jest makrem czytnika i tak jak zwykłe makro, może wykonywać dowolny rodzaj kodu Lispa, w tym kod, który sam został napisany w funky notacji włączonej przez wcześniej zarejestrowane makra czytnika. Więc jest dla ciebie cały język w czasie czytania.
Podsumowując:
Właściwie to, co zostało dotychczas wykazane, to fakt, że można uruchamiać zwykłe funkcje Lispa w czasie odczytu lub kompilacji; Jedynym krokiem, który należy zrobić, aby zrozumieć, w jaki sposób czytanie i kompilowanie są możliwe w czasie odczytu, kompilacji lub wykonywania, jest uświadomienie sobie, że czytanie i kompilowanie są wykonywane przez funkcje Lispa. Możesz po prostu wywołać read
lub eval
w dowolnym momencie wczytać dane Lispa ze strumieni znaków lub odpowiednio skompilować i wykonać kod Lisp. To cały język, cały czas.
Zwróć uwagę, że fakt, że Lisp spełnia wymagania punktu (3) z twojej listy, jest istotny dla sposobu, w jaki udaje mu się spełnić punkt (4) - szczególny smak makr dostarczanych przez Lispa w dużym stopniu opiera się na kodzie reprezentowanym przez zwykłe dane Lisp, co jest możliwe dzięki (3). Nawiasem mówiąc, tylko aspekt „drzewiastego” kodu jest tutaj naprawdę kluczowy - można sobie wyobrazić, że Lisp napisany jest przy użyciu XML.