Proszę wyjaśnij niektóre punkty Paula Grahama na temat Lispa

146

Potrzebuję pomocy w zrozumieniu niektórych punktów z książki Paula Grahama What Made Lisp Different .

  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ą.

  2. Typ symbolu. Symbole różnią się od łańcuchów tym, że równość można przetestować, porównując wskaźnik.

  3. Notacja kodu wykorzystująca drzewa symboli.

  4. 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?

unj2
źródło
10
Nie jestem pewien, czy znacznik programowania funkcjonalnego jest tutaj objęty gwarancją, ponieważ w wielu Lispsach jest równie możliwe pisanie kodu imperatywnego lub OO, jak pisanie kodu funkcjonalnego - i tak naprawdę istnieje wiele niefunkcjonalnych Lispów kod wokół. Sugerowałbym, abyś usunął znacznik fp i zamiast tego dodał clojure - mam nadzieję, że może to przynieść interesujące dane wejściowe od Lisperów opartych na JVM.
Michał Marczyk
58
Mamy tu też paul-grahamtag? !!! Świetnie ...
missingfaktor
@missingfaktor Może potrzebna jest prośba
kot

Odpowiedzi:

98

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ę (printlni 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 123stają się liczbą 123itd. 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ć readlub evalw 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.

Michał Marczyk
źródło
4
Uwaga: mówiąc „zwykłe (kompilatorowe) makro”, jesteś bliski zasugerowania, że ​​makra kompilatora są „zwykłymi” makrami, podczas gdy w Common Lisp (przynajmniej) „makro kompilatora” jest bardzo specyficzną i inną rzeczą: lispworks. pl / dokumentacja / lw51 / CLHS / Body /…
Ken
Ken: Dobry chwyt, dzięki! Zmienię to na „zwykłe makro”, co moim zdaniem nikogo nie potknie.
Michał Marczyk
Fantastyczna odpowiedź. Dowiedziałem się z niego więcej w 5 minut niż w ciągu godzin szukania w Google / rozważania pytania. Dzięki.
Charlie Flowers
Edycja: argh, źle zrozumiałem zdanie poprzedzające. Poprawione ze względu na gramatykę (potrzebuję „peera”, aby zaakceptować moją zmianę).
Tatiana Racheva
Wyrażenia S i XML mogą dyktować te same struktury, ale XML jest znacznie bardziej rozwlekły i dlatego nie nadaje się jako składnia.
Sylwester
66

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ą.

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

„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”.

(type-of "abc")  -> STRING

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:

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

(find-symbol "SIN")   ->  SIN

Ponieważ symbole są rzeczywistymi obiektami danych, możemy sprawdzić, czy są tym samym obiektem:

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

To pozwala nam na przykład napisać zdanie z symbolami:

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

Teraz możemy policzyć liczbę THE w zdaniu:

(count 'the *sentence*) ->  2

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:

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

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

Drzewo:

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

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

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.

Rainer Joswig
źródło
33

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.

Matt Curtis
źródło
7
Och, ta moc (i prostota) ma teraz ponad 50 lat i jest na tyle łatwa do zaimplementowania, że ​​początkujący programista może sobie z tym poradzić przy minimalnych wskazówkach i nauczyć się podstaw języka. Nie usłyszałbyś podobnego twierdzenia o Javie, C, Pythonie, Perlu, Haskellu itp. Jako projekt dobrego dla początkujących!
Matt Curtis
9
Nie sądzę, żeby zmienne Java były w ogóle jak symbole Lispa. W Javie nie ma notacji dla symbolu, a jedyną rzeczą, jaką można zrobić ze zmienną, jest pobranie jej komórki wartości. Ciągi znaków mogą być internowane, ale nie są to zazwyczaj nazwy, więc nie ma sensu rozmawiać o tym, czy można je cytować, oceniać, przekazywać itp.
Ken
2
Więcej niż 40 lat może być dokładniejsze :), @Ken: Myślę, że ma na myśli, że 1) zmienne nieprymitywne w java są przez refence, co jest podobne do seplenienia i 2) wewnętrzne ciągi znaków w java są podobne do symboli w lisp - oczywiście, jak powiedziałeś, nie możesz cytować ani oceniać wbudowanych ciągów / kodu w Javie, więc nadal są one zupełnie inne.
3
@Dan - Nie jestem pewien, kiedy powstała pierwsza implementacja, ale pierwszy artykuł McCarthy'ego na temat obliczeń symbolicznych został opublikowany w 1960 roku.
Inaimathi
Java ma częściową / nieregularną obsługę „symboli” w postaci Foo.class / foo.getClass () - tj. Obiekt klasy <Foo> typu typu jest nieco analogiczny - podobnie jak wartości wyliczenia, do stopień. Ale bardzo minimalne cienie symbolu Lispa.
BRPocock
-3

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.

CyberFonic
źródło