Potrzebuję pomocy w zrozumieniu niektórych punktów z książki Paula Grahama What Made Lisp Different .
Nowa koncepcja zmiennych. W Lispie wszystkie zmienne są efektywnymi wskaźnikami. Wartości mają typy, a nie zmienne, a przypisywanie lub wiązanie zmiennych oznacza kopiowanie wskaźników, a nie to, na co one wskazują.
Typ symbolu. Symbole różnią się od łańcuchów tym, że równość można przetestować, porównując wskaźnik.
Notacja kodu wykorzystująca drzewa symboli.
Cały język zawsze dostępny. Nie ma rzeczywistego rozróżnienia między czasem odczytu, czasem kompilacji i czasem wykonania. Możesz kompilować lub uruchamiać kod podczas czytania, odczytywania lub uruchamiania kodu podczas kompilacji oraz czytać lub kompilować kod w czasie wykonywania.
Co te punkty oznaczają? Czym się różnią w językach takich jak C czy Java? Czy jakiekolwiek języki inne niż języki rodziny Lisp mają teraz którąś z tych konstrukcji?
paul-graham
tag? !!! Świetnie ...Odpowiedzi:
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:
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
lubeval
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.
źródło
1) Nowa koncepcja zmiennych. W Lispie wszystkie zmienne są efektywnymi wskaźnikami. Wartości mają typy, a nie zmienne, a przypisywanie lub wiązanie zmiennych oznacza kopiowanie wskaźników, a nie to, na co one wskazują.
„to” jest zmienną. Może być powiązany z DOWOLNĄ wartością. Nie ma żadnych ograniczeń ani typu związanego ze zmienną. Jeśli wywołasz funkcję, argument nie musi być kopiowany. Zmienna jest podobna do wskaźnika. Ma sposób na dostęp do wartości, która jest powiązana ze zmienną. Nie ma potrzeby rezerwowania pamięci. Gdy wywołujemy funkcję, możemy przekazać dowolny obiekt danych: dowolny rozmiar i dowolny typ.
Obiekty danych mają „typ” i wszystkie obiekty danych mogą być odpytywane o jego „typ”.
2) Typ symbolu. Symbole różnią się od łańcuchów tym, że równość można przetestować, porównując wskaźnik.
Symbol to obiekt danych z nazwą. Zazwyczaj do znalezienia obiektu można użyć nazwy:
Ponieważ symbole są rzeczywistymi obiektami danych, możemy sprawdzić, czy są tym samym obiektem:
To pozwala nam na przykład napisać zdanie z symbolami:
Teraz możemy policzyć liczbę THE w zdaniu:
W Common Lisp symbole mają nie tylko nazwę, ale mogą również mieć wartość, funkcję, listę właściwości i pakiet. Tak więc symbole mogą być używane do nazywania zmiennych lub funkcji. Lista właściwości jest zwykle używana do dodawania metadanych do symboli.
3) Notacja kodu wykorzystująca drzewa symboli.
Lisp używa swoich podstawowych struktur danych do reprezentowania kodu.
Lista (* 3 2) może zawierać zarówno dane, jak i kod:
Drzewo:
4) Zawsze dostępny cały język. Nie ma rzeczywistego rozróżnienia między czasem odczytu, czasem kompilacji i czasem wykonania. Możesz kompilować lub uruchamiać kod podczas czytania, odczytywania lub uruchamiania kodu podczas kompilacji oraz czytać lub kompilować kod w czasie wykonywania.
Lisp zapewnia funkcje READ do odczytu danych i kodu z tekstu, LOAD do załadowania kodu, EVAL do oceny kodu, COMPILE do kompilacji kodu i PRINT do zapisania danych i kodu do tekstu.
Te funkcje są zawsze dostępne. Nie odchodzą. Mogą być częścią dowolnego programu. Oznacza to, że każdy program może czytać, ładować, oceniać lub drukować kod - zawsze.
Czym się różnią w językach takich jak C czy Java?
Te języki nie zapewniają symboli, kodu jako danych ani oceny danych jako kodu w czasie wykonywania. Obiekty danych w C zwykle nie mają typu.
Czy jakiekolwiek języki inne niż języki rodziny LISP mają teraz którąś z tych konstrukcji?
Wiele języków ma niektóre z tych możliwości.
Różnica:
W Lispie te możliwości są zaprojektowane w języku tak, aby były łatwe w użyciu.
źródło
W przypadku punktów (1) i (2) mówi on historycznie. Zmienne Java są prawie takie same, dlatego aby porównać wartości, należy wywołać .equals ().
(3) mówi o wyrażeniach S. Programy Lisp są napisane w tej składni, która zapewnia wiele zalet w porównaniu ze składnią ad-hoc, taką jak Java i C, na przykład przechwytywanie powtarzających się wzorców w makrach w znacznie czystszy sposób niż makra C lub szablony C ++ oraz manipulowanie kodem za pomocą tej samej podstawowej listy operacje, których używasz do danych.
(4) biorąc na przykład C: język to tak naprawdę dwa różne języki podrzędne: rzeczy takie jak if () i while () oraz preprocesor. Używasz preprocesora, aby zaoszczędzić na konieczności ciągłego powtarzania się lub pomijania kodu za pomocą # if / # ifdef. Ale oba języki są dość oddzielne i nie możesz używać while () w czasie kompilacji, tak jak możesz #if.
C ++ dodatkowo pogarsza to z szablonami. Zapoznaj się z kilkoma referencjami na temat metaprogramowania szablonów, które zapewnia sposób generowania kodu w czasie kompilacji i jest niezwykle trudne do zrozumienia dla osób nie będących ekspertami. Ponadto jest to naprawdę kilka trików i sztuczek wykorzystujących szablony i makra, dla których kompilator nie może zapewnić obsługi pierwszej klasy - jeśli popełnisz prosty błąd składniowy, kompilator nie będzie w stanie podać jasnego komunikatu o błędzie.
Cóż, dzięki Lisp masz to wszystko w jednym języku. Używasz tych samych rzeczy do generowania kodu w czasie wykonywania, jak uczysz się pierwszego dnia. Nie oznacza to, że metaprogramowanie jest trywialne, ale z pewnością jest prostsze dzięki pierwszorzędnemu językowi i obsłudze kompilatorów.
źródło
Punkty (1) i (2) również pasowałyby do Pythona. Biorąc prosty przykład "a = str (82.4)" interpreter najpierw tworzy obiekt zmiennoprzecinkowy o wartości 82.4. Następnie wywołuje konstruktor łańcuchowy, który następnie zwraca łańcuch o wartości „82 .4”. „A” po lewej stronie jest jedynie etykietą tego obiektu typu string. Oryginalny obiekt zmiennoprzecinkowy został wyrzucony jako śmieci, ponieważ nie ma już do niego odwołań.
W Scheme wszystko jest traktowane jako przedmiot w podobny sposób. Nie jestem pewien co do Common Lisp. Starałbym się unikać myślenia w kategoriach C / C ++. Zwalniali mnie mocno, kiedy próbowałem zrozumieć piękną prostotę Lispsa.
źródło