Czytając eseje Paula Grahama na temat języków programowania, można by pomyśleć, że makra Lisp to jedyna droga. Jako zajęty programista pracujący na innych platformach nie miałem przywileju używania makr Lisp. Jako ktoś, kto chce zrozumieć szum, wyjaśnij, dlaczego ta funkcja jest tak potężna.
Proszę również odnieść to do czegoś, co zrozumiałbym ze światów programowania Python, Java, C # lub C.
macros
lisp
homoiconicity
miętowy
źródło
źródło
Odpowiedzi:
Aby udzielić krótkiej odpowiedzi, makra są używane do definiowania rozszerzeń składni języka dla Common Lisp lub DSL (Domain Specific Languages). Te języki są osadzone bezpośrednio w istniejącym kodzie Lisp. Teraz DSL mogą mieć składnię podobną do Lisp (jak interpreter Prologa Petera Norviga dla Common Lisp) lub zupełnie inną (np. Infix Notation Math dla Clojure).
Oto bardziej konkretny przykład:
Python ma wbudowane w listę interpretacje list. Daje to prostą składnię dla typowego przypadku. Linia
zwraca listę zawierającą wszystkie liczby parzyste od 0 do 9. W Pythonie 1,5 dnia nie było takiej składni; użyłbyś czegoś takiego:
Oba są funkcjonalnie równoważne. Przywołajmy nasze zawieszenie niewiary i udawajmy, że Lisp ma bardzo ograniczone makro pętli, które po prostu wykonuje iterację i nie ma łatwego sposobu na zrobienie odpowiednika wyrażeń listowych.
W Lisp możesz napisać następujące. Powinienem zauważyć, że ten wymyślony przykład został wybrany jako identyczny z kodem Python, a nie dobrym przykładem kodu Lisp.
Zanim przejdę dalej, powinienem lepiej wyjaśnić, czym jest makro. Jest to transformacja wykonywana na kod po kodzie. To znaczy fragment kodu odczytywany przez interpretera (lub kompilatora), który przyjmuje kod jako argument, manipuluje i zwraca wynik, który jest następnie uruchamiany w miejscu.
Oczywiście to dużo pisania, a programiści są leniwi. Możemy więc zdefiniować DSL do wykonywania list. W rzeczywistości używamy już jednego makra (makro pętli).
Lisp definiuje kilka specjalnych form składni. Quote (
'
) wskazuje, że następny token jest dosłowny. Quasi-cytat lub backtick (`
) oznacza, że następny token jest dosłowny ze znakami ucieczki. Ucieczki są oznaczone przez przecinek. Dosłowność'(1 2 3)
jest odpowiednikiem języka Python[1, 2, 3]
. Możesz przypisać ją do innej zmiennej lub użyć w miejscu. Możesz myśleć o`(1 2 ,x)
tym, że jest odpowiednikiem Pythona,[1, 2, x]
gdziex
jest wcześniej zdefiniowaną zmienną. Ten zapis listy jest częścią magii, która przechodzi w makra. Druga część to czytnik Lisp, który inteligentnie zastępuje makra kodem, ale najlepiej to ilustruje poniżej:Możemy więc zdefiniować makro o nazwie
lcomp
(skrót od listy). Jego składnia będzie dokładnie taka jak w pythonie, którego użyliśmy w przykładzie[x for x in range(10) if x % 2 == 0]
-(lcomp x for x in (range 10) if (= (% x 2) 0))
Teraz możemy wykonać w wierszu poleceń:
Całkiem nieźle, co? Teraz to nie koniec. Masz mechanizm lub pędzel, jeśli chcesz. Możesz mieć dowolną składnię. Jak
with
składnia Python lub C # . Lub składnia LINQ .NET. To właśnie przyciąga ludzi do Lisp - najwyższa elastyczność.źródło
(loop for x from 0 below 10 when (evenp x) collect x)
, więcej przykładów tutaj . Ale rzeczywiście, pętla jest „tylko makrem” (zresztą jakiś czas temu ponownie jąObszerna debata na temat makro lisp znajduje się tutaj .
Interesujący podzbiór tego artykułu:
źródło
Typowe makra Lisp zasadniczo rozszerzają „prymitywy składniowe” twojego kodu.
Na przykład w C konstrukcja przełącznika / sprawy działa tylko z typami całkowymi, a jeśli chcesz użyć jej dla liczb zmiennoprzecinkowych lub łańcuchów, pozostaje ci zagnieżdżone instrukcje if i wyraźne porównania. Nie ma również możliwości napisania makra C, aby wykonać zadanie za Ciebie.
Ale ponieważ makro lisp jest (zasadniczo) programem lisp, który pobiera fragmenty kodu jako dane wejściowe i zwraca kod w celu zastąpienia „wywołania” makra, można rozszerzyć repertuar „prymitywny” tak daleko, jak chcesz, zwykle kończąc z bardziej czytelnym programem.
Aby zrobić to samo w C, musisz napisać niestandardowy procesor wstępny, który zjada twoje początkowe (niezupełnie C) źródło i wyrzuca coś, co kompilator C może zrozumieć. Nie jest to zły sposób, ale niekoniecznie najłatwiejszy.
źródło
Makra Lisp pozwalają ci zdecydować, kiedy (jeśli w ogóle) jakakolwiek część lub wyrażenie zostanie ocenione. Aby podać prosty przykład, pomyśl o C:
Mówi to: oceń
expr1
, a jeśli to prawda, oceńexpr2
itp.Teraz spróbuj przekształcić to
&&
w funkcję ... tak, nie możesz. Wywoływanie czegoś takiego:Oceni wszystkie trzy
exprs
przed udzieleniem odpowiedzi niezależnie od tego, czyexpr1
był fałszywy!Za pomocą makr lisp możesz kodować coś takiego:
teraz masz funkcję
&&
, którą możesz wywołać jak funkcję, i nie będzie oceniać żadnych przekazywanych do niej form, chyba że wszystkie są prawdziwe.Aby zobaczyć, jak to jest przydatne, kontrast:
i:
Inne rzeczy, które możesz zrobić z makrami, to tworzenie nowych słów kluczowych i / lub mini-języków (sprawdź
(loop ...)
makro na przykład), integrowanie innych języków w lisp, na przykład, możesz napisać makro, które pozwoli ci powiedzieć coś takiego:I to nawet nie wchodzi w makra programu Reader .
Mam nadzieję że to pomoże.
źródło
(and ...
będzie oceniać wyrażenia, dopóki nie zostanie ocenione na fałsz, zwróć uwagę, że wystąpią skutki uboczne wygenerowane przez fałszywą ocenę, tylko kolejne wyrażenia zostaną pominięte.Nie sądzę, żebym kiedykolwiek widział makra Lisp wyjaśniane lepiej niż ten facet: http://www.defmacro.org/ramblings/lisp.html
źródło
Pomyśl o tym, co możesz zrobić w C lub C ++ za pomocą makr i szablonów. Są to bardzo przydatne narzędzia do zarządzania powtarzalnym kodem, ale są ograniczone w dość poważny sposób.
Makra Lisp i Lisp rozwiązują te problemy.
Porozmawiaj z każdym, kto opanował C ++ i zapytaj go, ile czasu spędzili na nauce wszystkich szablonów, których potrzebują, aby wykonać metaprogramowanie szablonów. Lub wszystkie zwariowane sztuczki w (doskonałych) książkach, takich jak Modern C ++ Design , które wciąż są trudne do debugowania i (w praktyce) nieprzenośne między kompilatorami ze świata rzeczywistego, mimo że język jest standaryzowany przez dekadę. Wszystko to rozpływa się, jeśli język używany do metaprogramowania jest tym samym językiem, którego używasz do programowania!
źródło
Makro lisp pobiera fragment programu jako dane wejściowe. Ten fragment programu reprezentuje strukturę danych, którą można manipulować i przekształcać w dowolny sposób. Na koniec makro generuje kolejny fragment programu, który jest wykonywany w czasie wykonywania.
C # nie ma funkcji makr, jednak odpowiednikiem byłoby, gdyby kompilator przeanalizował kod w drzewie CodeDOM i przekazał go do metody, która przekształciła go w inną CodeDOM, która jest następnie kompilowana do IL.
Może to być wykorzystywane do realizacji „cukier” składni podobnie do
for each
-statementusing
-clause, LINQselect
-expressions i tak dalej, jako makra, które przekształca kodu bazowego.Jeśli Java ma makra, możesz zaimplementować składnię Linq w Javie, bez potrzeby zmiany języka podstawowego przez Sun.
Oto pseudo-kod dla tego, jak
using
mogłoby wyglądać makro w stylu seplenienia w języku C # do implementacji :źródło
using
będzie wyglądać tak ;)Ponieważ istniejące odpowiedzi dają dobre konkretne przykłady wyjaśniające, co osiągają makra i w jaki sposób, być może pomogłoby to zebrać niektóre przemyślenia na temat tego, dlaczego funkcja makro jest znaczącym zyskiem w stosunku do innych języków ; najpierw z tych odpowiedzi, potem świetna z innych źródeł:
- Vatine
- Matt Curtis
- Miguel Ping
- Peter Seibel, w „Practical Common Lisp”
źródło
Nie jestem pewien, czy mogę dodać trochę wglądu do wszystkich (doskonałych) postów, ale ...
Makra Lisp działają świetnie ze względu na naturę składni Lisp.
Lisp jest niezwykle regularnym językiem (myśl o wszystkim to lista ); makra pozwalają traktować dane i kod tak samo (parsowanie ciągów lub inne modyfikacje nie są potrzebne do modyfikowania wyrażeń lisp). Łączysz te dwie funkcje i masz bardzo czysty sposób na modyfikację kodu.
Edycja: Chciałem powiedzieć, że Lisp jest homoiczny , co oznacza, że struktura danych dla programu lisp jest napisana w samym lisp.
W efekcie powstaje sposób tworzenia własnego generatora kodu na języku przy użyciu samego języka z całą jego mocą (np. W Javie musisz włamać się do kodu bajtowego, chociaż niektóre frameworki, takie jak AspectJ, pozwalają ci zrób to przy użyciu innego podejścia, zasadniczo jest to hack).
W praktyce z makrami budujesz swój własny mini-język na bazie seplenienia, bez potrzeby uczenia się dodatkowych języków lub narzędzi oraz z wykorzystaniem pełnej mocy samego języka.
źródło
S-expressions
list, a nie z list.Makra Lisp reprezentują wzorzec występujący w prawie każdym dużym projekcie programistycznym. W końcu w dużym programie masz pewną sekcję kodu, w której zdajesz sobie sprawę, że napisanie programu, który generuje kod źródłowy w postaci tekstu, byłoby łatwiejsze i mniej podatne na błędy.
W Pythonie obiekty mają dwie metody
__repr__
i__str__
.__str__
jest po prostu reprezentacją czytelną dla człowieka.__repr__
zwraca reprezentację, która jest poprawnym kodem Pythona, to znaczy coś, co można wprowadzić do interpretera jako prawidłowy Python. W ten sposób możesz tworzyć małe fragmenty Pythona, które generują prawidłowy kod, który można wkleić w twoim źródle.W Lisp cały proces został sformalizowany przez system makr. Jasne, że umożliwia tworzenie rozszerzeń składni i robienie różnego rodzaju fantazyjnych rzeczy, ale ich faktyczna użyteczność została podsumowana powyżej. Oczywiście pomaga to, że system makr Lisp pozwala na manipulowanie tymi „fragmentami” z pełną mocą całego języka.
źródło
Krótko mówiąc, makra są transformacjami kodu. Pozwalają wprowadzić wiele nowych konstrukcji składni. Np. Rozważ LINQ w C #. W lisp istnieją podobne rozszerzenia języka, które są implementowane przez makra (np. Wbudowana konstrukcja pętli, iteracja). Makra znacznie zmniejszają duplikację kodu. Makra pozwalają na osadzanie «małych języków» (np. Tam, gdzie w c # / java można użyć xml do konfiguracji, w lisp to samo można osiągnąć z makrami). Makra mogą ukrywać trudności w korzystaniu z bibliotek.
Np. W seplenienie możesz pisać
i to ukrywa wszystkie elementy bazy danych (transakcje, prawidłowe zamykanie połączenia, pobieranie danych itp.), podczas gdy w C # wymaga to utworzenia SqlConnections, SqlCommands, dodania SqlParameters do SqlCommands, zapętlenia SqlDataReaders, poprawnego ich zamknięcia.
źródło
Chociaż wszystko to wyjaśnia makra, a nawet ma fajne przykłady, myślę, że kluczową różnicą między makrem a normalną funkcją jest to, że LISP najpierw ocenia wszystkie parametry przed wywołaniem funkcji. W przypadku makra jest odwrotnie, LISP przekazuje parametry bez oceny do makra. Na przykład, jeśli przekażesz (+ 1 2) do funkcji, funkcja otrzyma wartość 3. Jeśli przekażesz to do makra, otrzyma Listę (+ 1 2). Można to wykorzystać do wykonywania wszelkiego rodzaju niezwykle przydatnych rzeczy.
Zmierz czas potrzebny do wykonania przekazanej funkcji. W przypadku funkcji parametr zostanie oceniony przed przekazaniem kontroli do funkcji. Za pomocą makra możesz podzielić swój kod na początek i koniec stopera. Poniżej znajduje się dokładnie ten sam kod w makrze i funkcji, a wynik jest bardzo różny. Uwaga: Jest to wymyślony przykład, a implementacja została wybrana tak, aby była identyczna, aby lepiej podkreślić różnicę.
źródło
Mam to z książki kucharskiej Lisp, ale myślę, że to wyjaśnia, dlaczego makra lisp są dobre w dobry sposób.
„Makro jest zwykłym fragmentem kodu Lisp, który działa na innym domniemanym kodzie Lisp, tłumacząc go na wersję wykonywalną Lisp. Może to zabrzmieć nieco skomplikowanie, więc dajmy prosty przykład. Załóżmy, że chcesz wersja setq, która ustawia dwie zmienne na tę samą wartość. Więc jeśli napiszesz
gdy
z=8
zarówno x, jak i są ustawione na 11. (Nie mogę wymyślić żadnego zastosowania do tego, ale to tylko przykład.)Powinno być oczywiste, że nie możemy zdefiniować setq2 jako funkcji. Jeśli
x=50
iy=-5
, funkcja otrzyma wartości 50, -5 i 11; nie wiedziałby, jakie zmienne miały być ustawione. To, co naprawdę chcemy powiedzieć, to to, że kiedy widzisz (system Lisp)(setq2 v1 v2 e)
, potraktuj to jako równoważne(progn (setq v1 e) (setq v2 e))
. W rzeczywistości nie jest to w porządku, ale na razie wystarczy. Makro pozwala nam to zrobić dokładnie, określając program do przekształcania wzorca wejściowego(setq2 v1 v2 e)
„na wzorzec wyjściowy(progn ...)
”.Jeśli uważasz, że to było miłe, możesz dalej czytać tutaj: http://cl-cookbook.sourceforge.net/macros.html
źródło
setq2
jako funkcję, jeślix
iy
są przekazywane przez odwołanie. Nie wiem jednak, czy jest to możliwe w CL. Więc dla kogoś, kto nie zna Lispsa lub CL w szczególności, nie jest to bardzo ilustrujący przykład IMOW Pythonie masz dekoratory, w zasadzie masz funkcję, która przyjmuje inną funkcję jako dane wejściowe. Możesz robić, co tylko chcesz: wywołać funkcję, zrobić coś innego, zawinąć wywołanie funkcji w wersję pozyskiwania zasobów itp., Ale nie można zajrzeć do tej funkcji. Powiedzmy, że chcieliśmy zwiększyć jego moc, powiedzmy, że twój dekorator otrzymał kod funkcji jako listę, wtedy możesz nie tylko wykonać funkcję taką, jaka jest, ale możesz teraz wykonać jej części, zmienić kolejność wierszy funkcji itp.
źródło