Kiedy powinienem używać makra w moim programie, czy nie?
To pytanie jest inspirowane pouczającą odpowiedzią @tarsius. Wierzę, że wielu ma wspaniałe doświadczenia, aby dzielić się z innymi. Mam nadzieję, że to pytanie stanie się jednym z wielkich pytań subiektywnych i pomoże programistom Emacsa.
Można użyć makra do cukru syntaktycznego, ale złym pomysłem jest umieszczenie logiki w ciele makra bez względu na to, czy jest ono w samym makrze, czy w rozszerzeniu. Jeśli czujesz taką potrzebę, napisz funkcję i „makro towarzyszące” dla cukru syntaktycznego. Krótko mówiąc, jeśli masz letmakro, szukasz kłopotów.
Dlaczego?
Makra mają wiele wad:
Rozszerzają się one w czasie kompilacji i dlatego muszą być starannie napisane, aby zachować zgodność binarną w wielu wersjach. Na przykład usunięcie funkcji lub zmiennej używanej do rozwinięcia makra spowoduje uszkodzenie kodu bajtowego wszystkich zależnych pakietów, które korzystały z tego makra. Innymi słowy, jeśli zmienisz implementację makra w sposób niezgodny, wszystkie pakiety zależne muszą zostać ponownie skompilowane. Kompatybilność binarna nie zawsze jest oczywista…
Musisz zadbać o utrzymanie intuicyjnej kolejności oceny . Łatwo jest napisać makro, które zbyt często ocenia formularz, w niewłaściwym czasie, w niewłaściwym miejscu itp.
Musisz uważać na semantykę wiązania : makra dziedziczą kontekst wiązania z miejsca ekspansji , co oznacza, że nie wiesz a priori, jakie zmienne leksykalne istnieją i czy istnieje nawet powiązanie leksykalne. Makro musi działać z obydwoma wiążącymi semantykami.
W związku z tym należy zachować ostrożność podczas pisania makr, które tworzą zamknięcia . Pamiętaj, że otrzymujesz powiązania z miejsca ekspansji, a nie z definicji leksykalnej, więc uważaj na to, co zamykasz i jak tworzysz zamknięcie (pamiętaj, nie wiesz, czy powiązanie leksykalne jest aktywne w miejscu ekspansji i czy masz nawet dostępne zamknięcia).
Uwagi na temat wiązania leksykalnego a ekspansji makro są szczególnie interesujące; dzięki, lunaryorn. (Nigdy nie włączałem wiązania leksykalnego dla żadnego kodu, który napisałem, więc nie jest to konflikt, o którym kiedykolwiek myślałem.)
Phils
6
Z mojego doświadczenia w czytaniu, debugowaniu i modyfikowaniu kodu Elisp mojego i innych sugerowałem, aby unikać makr w jak największym stopniu.
Oczywistą rzeczą w makrach jest to, że nie oceniają swoich argumentów. Co oznacza, że jeśli w makrze znajduje się złożone wyrażenie, nie masz pojęcia, czy zostanie ono ocenione, czy też nie, i w jakim kontekście, chyba że przejrzysz definicję makra. To może nie być dla ciebie wielka sprawa, ponieważ znasz definicję własnego makra (obecnie, prawdopodobnie nie kilka lat później).
Ta wada nie dotyczy funkcji: wszystkie argumenty są sprawdzane przed wywołaniem funkcji. Co oznacza, że możesz zbudować ewolucyjną złożoność w nieznanej sytuacji debugowania. Możesz nawet pominąć definicje nieznanych funkcji, o ile zwracają one proste dane i zakładasz, że nie dotykają stanu globalnego.
Aby przeredagować to w inny sposób, makra stają się częścią języka. Jeśli czytasz kod z nieznanymi makrami, prawie czytasz kod w języku, którego nie znasz.
Wyjątki od powyższych zastrzeżeń:
Standardowe makra podoba push, pop, dolistzrobić naprawdę dużo dla ciebie, i są znane do innych programistów. Są w zasadzie częścią specyfikacji języka. Ale praktycznie nie można ujednolicić własnego makra. Trudno nie tylko wymyślić coś prostego i użytecznego, jak powyższe przykłady, ale jeszcze trudniej jest przekonać każdego programistę, aby się tego nauczył.
Poza tym nie mam nic przeciwko makrom save-selected-window: przechowują i przywracają stan oraz oceniają argumenty w ten sam sposób progn. Całkiem proste, w rzeczywistości upraszcza kod.
Innym przykładem makr OK mogą być rzeczy takie jak defhydralub use-package. Te makra mają w zasadzie własny mini-język. I nadal albo traktują proste dane, albo zachowują się jak progn. Ponadto są zwykle na najwyższym poziomie, więc na stosie wywołań nie ma zmiennych, na których mogłyby polegać argumenty makra, co czyni je nieco prostszym.
Moim zdaniem przykładem złego makra jest magit-section-actioni magit-section-caseMagit. Pierwszy został już usunięty, ale drugi nadal pozostaje.
Będę szczery: nie rozumiem, co oznacza „użycie składni, a nie semantyki”. Dlatego część tej odpowiedzi dotyczy odpowiedzi Lunaryona , a drugą częścią będzie moja próba odpowiedzi na pierwotne pytanie.
Kiedy słowo „semantyka” używane jest w programowaniu, odnosi się do sposobu, w jaki autor języka zdecydował się interpretować swój język. Zwykle sprowadza się to do zestawu formuł, które przekształcają reguły gramatyczne języka w niektóre inne reguły, o których czytelnik jest znany. Na przykład Plotkin w swojej książce Structural Approach to Operational Semantics używa logicznego języka pochodnego, aby wyjaśnić, na co ocenia składnia abstrakcyjna (co to znaczy uruchomić program). Innymi słowy, jeśli składnia stanowi język programowania na pewnym poziomie, to musi mieć pewną semantykę, w przeciwnym razie jest to ... no cóż, język programowania.
Ale na razie zapomnijmy o formalnych definicjach, w końcu ważne jest, aby zrozumieć intencję. Wygląda więc na to, że lunaryon zachęciłby swoich czytelników do używania makr, gdy program nie ma dodawać nowego znaczenia, a jedynie jakiś skrót. Dla mnie to brzmi dziwnie iw jaskrawym kontraście do faktycznego wykorzystania makr. Poniżej znajdują się przykłady, które wyraźnie tworzą nowe znaczenia:
defun Znajomi, którzy są istotną częścią języka, nie mogą być opisani tymi samymi terminami, które opisalibyśmy wywołania funkcji.
setfzasady opisujące działanie funkcji są nieodpowiednie do opisania efektów takiego wyrażenia (setf (aref x y) z).
with-output-to-stringzmienia również semantykę kodu wewnątrz makra. Podobnie with-current-bufferi kilka innych with-makr.
dotimes i podobnych nie można opisać w kategoriach funkcji.
ignore-errors zmienia semantykę zawijanego kodu.
W cl-libpakiecie, eieiopakiecie i kilku innych niemal wszechobecnych bibliotekach używanych w Emacs Lisp znajduje się cała masa makr , które, choć mogą wyglądać jak formy funkcyjne, mają różne interpretacje.
Zatem makra są narzędziem do wprowadzania nowej semantyki do języka. Nie mogę wymyślić alternatywnego sposobu na zrobienie tego (przynajmniej nie w Emacs Lisp).
Kiedy nie używałbym makr:
Gdy nie wprowadzają żadnych nowych konstrukcji, z nową semantyką (tj. Funkcja wykona zadanie równie dobrze).
Gdy jest to po prostu wygoda (tj. Tworzenie makra o nazwie, mvbktóre rozwija się, cl-multiple-value-bindaby go skrócić).
Kiedy oczekuję, że makro ukryje wiele kodu obsługi błędów (jak zauważono, makra są trudne do debugowania).
Gdy zdecydowanie wolę makra niż funkcje:
Gdy pojęcia specyficzne dla domeny są zasłaniane przez wywołania funkcji. Na przykład, jeśli trzeba przekazać ten sam contextargument do zestawu funkcji, które należy wywoływać kolejno, wolałbym zawinąć je w makro, w którym contextargument jest ukryty.
Kiedy ogólny kod w postaci funkcji jest zbyt szczegółowy (konstrukcje iteracji gołej w Emacsie Lisp są zbyt gadatliwe na mój gust i niepotrzebnie narażają programistę na szczegóły implementacji niskiego poziomu).
Ilustracja
Poniżej znajduje się rozwodniona wersja, która ma dać intuicję co do znaczenia semantyki w informatyce.
Załóżmy, że masz niezwykle prosty język programowania z tymi samymi regułami składni:
Teraz możemy faktycznie wykonać obliczenia przy użyciu tego języka. Oznacza to, że możemy wziąć reprezentację składniową, zrozumieć ją abstrakcyjnie (innymi słowy, przekształcić ją w składnię abstrakcyjną), a następnie użyć reguł semantycznych do manipulacji tą reprezentacją.
Kiedy mówimy o rodzinie języków Lisp, podstawowymi semantycznymi regułami oceny funkcji są z grubsza zasady rachunku lambda:
(f x) (lambda x y) x
-----, ------------, ---
fx ^x.y x
Mechanizmy cytowania i makroekspansji są również znane jako narzędzia metaprogramowania, tzn. Pozwalają mówić o programie, a nie tylko programować. Osiągają to poprzez tworzenie nowej semantyki przy użyciu „warstwy meta”. Przykładem takiego prostego makra jest:
(a . b)
-------
(cons a b)
To nie jest makro w Emacsie Lisp, ale mogło być. Wybrałem tylko dla uproszczenia. Zauważ, że żadna z reguł semantycznych zdefiniowanych powyżej nie miałaby zastosowania w tym przypadku, ponieważ najbliższy kandydat (f x)interpretuje fjako funkcję, choć aniekoniecznie jest funkcją.
„Cukier syntaktyczny” nie jest tak naprawdę terminem w informatyce. Jest to coś, co inżynierowie używają do oznaczania różnych rzeczy. Więc nie mam ostatecznej odpowiedzi. Ponieważ mówimy o semantyce, to reguła interpretacji składni postaci: (x y)jest grubsza „ocenić Y, a następnie zadzwonić X z wynikiem poprzedniej oceny”. Kiedy piszesz (defun x y), y nie jest obliczane, podobnie jak x. To, czy możesz napisać kod z równoważnym skutkiem przy użyciu innej semantyki, nie przeczy temu, że ten fragment kodu ma własną semantykę.
wvxvw
1
Odp. Twój drugi komentarz: Czy możesz odnieść się do definicji używanego makra, która mówi, co twierdzisz? Właśnie dałem wam przykład, w którym za pomocą makra wprowadzono nową regułę semantyczną. Przynajmniej w sensie CS. Nie sądzę, aby kłótnia o definicje była produktywna, a także uważam, że definicje używane w CS najlepiej pasują do tej dyskusji.
wvxvw
1
Cóż ... zdarza się, że masz własne pojęcie o tym, co znaczy słowo „semantyka”, co jest trochę ironiczne, jeśli się nad tym zastanowić :) Ale tak naprawdę te rzeczy mają bardzo precyzyjne definicje, po prostu… , zignoruj je. Książka, którą podłączyłem w odpowiedzi, jest standardowym podręcznikiem semantyki, zgodnie z nauczaniem w odpowiednim kursie CS. Jeśli po prostu przeczytasz odpowiedni artykuł Wiki: en.wikipedia.org/wiki/Semantics_%28computer_science%29 , zobaczysz, o co tak naprawdę chodzi. Przykładem jest reguła interpretacji w (defun x y)funkcji aplikacji.
wvxvw
No cóż, nie sądzę, że to mój sposób na dyskusję. Błędem było nawet rozpoczęcie próby; Usunąłem swoje komentarze, ponieważ nie ma to sensu. Przepraszam, że zmarnowałem twój czas.
Z mojego doświadczenia w czytaniu, debugowaniu i modyfikowaniu kodu Elisp mojego i innych sugerowałem, aby unikać makr w jak największym stopniu.
Oczywistą rzeczą w makrach jest to, że nie oceniają swoich argumentów. Co oznacza, że jeśli w makrze znajduje się złożone wyrażenie, nie masz pojęcia, czy zostanie ono ocenione, czy też nie, i w jakim kontekście, chyba że przejrzysz definicję makra. To może nie być dla ciebie wielka sprawa, ponieważ znasz definicję własnego makra (obecnie, prawdopodobnie nie kilka lat później).
Ta wada nie dotyczy funkcji: wszystkie argumenty są sprawdzane przed wywołaniem funkcji. Co oznacza, że możesz zbudować ewolucyjną złożoność w nieznanej sytuacji debugowania. Możesz nawet pominąć definicje nieznanych funkcji, o ile zwracają one proste dane i zakładasz, że nie dotykają stanu globalnego.
Aby przeredagować to w inny sposób, makra stają się częścią języka. Jeśli czytasz kod z nieznanymi makrami, prawie czytasz kod w języku, którego nie znasz.
Wyjątki od powyższych zastrzeżeń:
Standardowe makra podoba
push
,pop
,dolist
zrobić naprawdę dużo dla ciebie, i są znane do innych programistów. Są w zasadzie częścią specyfikacji języka. Ale praktycznie nie można ujednolicić własnego makra. Trudno nie tylko wymyślić coś prostego i użytecznego, jak powyższe przykłady, ale jeszcze trudniej jest przekonać każdego programistę, aby się tego nauczył.Poza tym nie mam nic przeciwko makrom
save-selected-window
: przechowują i przywracają stan oraz oceniają argumenty w ten sam sposóbprogn
. Całkiem proste, w rzeczywistości upraszcza kod.Innym przykładem makr OK mogą być rzeczy takie jak
defhydra
lubuse-package
. Te makra mają w zasadzie własny mini-język. I nadal albo traktują proste dane, albo zachowują się jakprogn
. Ponadto są zwykle na najwyższym poziomie, więc na stosie wywołań nie ma zmiennych, na których mogłyby polegać argumenty makra, co czyni je nieco prostszym.Moim zdaniem przykładem złego makra jest
magit-section-action
imagit-section-case
Magit. Pierwszy został już usunięty, ale drugi nadal pozostaje.źródło
Będę szczery: nie rozumiem, co oznacza „użycie składni, a nie semantyki”. Dlatego część tej odpowiedzi dotyczy odpowiedzi Lunaryona , a drugą częścią będzie moja próba odpowiedzi na pierwotne pytanie.
Kiedy słowo „semantyka” używane jest w programowaniu, odnosi się do sposobu, w jaki autor języka zdecydował się interpretować swój język. Zwykle sprowadza się to do zestawu formuł, które przekształcają reguły gramatyczne języka w niektóre inne reguły, o których czytelnik jest znany. Na przykład Plotkin w swojej książce Structural Approach to Operational Semantics używa logicznego języka pochodnego, aby wyjaśnić, na co ocenia składnia abstrakcyjna (co to znaczy uruchomić program). Innymi słowy, jeśli składnia stanowi język programowania na pewnym poziomie, to musi mieć pewną semantykę, w przeciwnym razie jest to ... no cóż, język programowania.
Ale na razie zapomnijmy o formalnych definicjach, w końcu ważne jest, aby zrozumieć intencję. Wygląda więc na to, że lunaryon zachęciłby swoich czytelników do używania makr, gdy program nie ma dodawać nowego znaczenia, a jedynie jakiś skrót. Dla mnie to brzmi dziwnie iw jaskrawym kontraście do faktycznego wykorzystania makr. Poniżej znajdują się przykłady, które wyraźnie tworzą nowe znaczenia:
defun
Znajomi, którzy są istotną częścią języka, nie mogą być opisani tymi samymi terminami, które opisalibyśmy wywołania funkcji.setf
zasady opisujące działanie funkcji są nieodpowiednie do opisania efektów takiego wyrażenia(setf (aref x y) z)
.with-output-to-string
zmienia również semantykę kodu wewnątrz makra. Podobniewith-current-buffer
i kilka innychwith-
makr.dotimes
i podobnych nie można opisać w kategoriach funkcji.ignore-errors
zmienia semantykę zawijanego kodu.W
cl-lib
pakiecie,eieio
pakiecie i kilku innych niemal wszechobecnych bibliotekach używanych w Emacs Lisp znajduje się cała masa makr , które, choć mogą wyglądać jak formy funkcyjne, mają różne interpretacje.Zatem makra są narzędziem do wprowadzania nowej semantyki do języka. Nie mogę wymyślić alternatywnego sposobu na zrobienie tego (przynajmniej nie w Emacs Lisp).
Kiedy nie używałbym makr:
Gdy nie wprowadzają żadnych nowych konstrukcji, z nową semantyką (tj. Funkcja wykona zadanie równie dobrze).
Gdy jest to po prostu wygoda (tj. Tworzenie makra o nazwie,
mvb
które rozwija się,cl-multiple-value-bind
aby go skrócić).Kiedy oczekuję, że makro ukryje wiele kodu obsługi błędów (jak zauważono, makra są trudne do debugowania).
Gdy zdecydowanie wolę makra niż funkcje:
Gdy pojęcia specyficzne dla domeny są zasłaniane przez wywołania funkcji. Na przykład, jeśli trzeba przekazać ten sam
context
argument do zestawu funkcji, które należy wywoływać kolejno, wolałbym zawinąć je w makro, w którymcontext
argument jest ukryty.Kiedy ogólny kod w postaci funkcji jest zbyt szczegółowy (konstrukcje iteracji gołej w Emacsie Lisp są zbyt gadatliwe na mój gust i niepotrzebnie narażają programistę na szczegóły implementacji niskiego poziomu).
Ilustracja
Poniżej znajduje się rozwodniona wersja, która ma dać intuicję co do znaczenia semantyki w informatyce.
Załóżmy, że masz niezwykle prosty język programowania z tymi samymi regułami składni:
z tym językiem możemy skonstruować „programów” jak
(1+0)+1
lub0
czy((0+0))
itd.Ale dopóki nie podamy reguł semantycznych, te „programy” są bez znaczenia.
Załóżmy, że teraz wyposażamy nasz język w (semantyczne) reguły:
Teraz możemy faktycznie wykonać obliczenia przy użyciu tego języka. Oznacza to, że możemy wziąć reprezentację składniową, zrozumieć ją abstrakcyjnie (innymi słowy, przekształcić ją w składnię abstrakcyjną), a następnie użyć reguł semantycznych do manipulacji tą reprezentacją.
Kiedy mówimy o rodzinie języków Lisp, podstawowymi semantycznymi regułami oceny funkcji są z grubsza zasady rachunku lambda:
Mechanizmy cytowania i makroekspansji są również znane jako narzędzia metaprogramowania, tzn. Pozwalają mówić o programie, a nie tylko programować. Osiągają to poprzez tworzenie nowej semantyki przy użyciu „warstwy meta”. Przykładem takiego prostego makra jest:
To nie jest makro w Emacsie Lisp, ale mogło być. Wybrałem tylko dla uproszczenia. Zauważ, że żadna z reguł semantycznych zdefiniowanych powyżej nie miałaby zastosowania w tym przypadku, ponieważ najbliższy kandydat
(f x)
interpretujef
jako funkcję, choća
niekoniecznie jest funkcją.źródło
(x y)
jest grubsza „ocenić Y, a następnie zadzwonić X z wynikiem poprzedniej oceny”. Kiedy piszesz(defun x y)
, y nie jest obliczane, podobnie jak x. To, czy możesz napisać kod z równoważnym skutkiem przy użyciu innej semantyki, nie przeczy temu, że ten fragment kodu ma własną semantykę.(defun x y)
funkcji aplikacji.