Makra w Lispie - mały przykład

Czasem słyszę pytania o to, co takiego ciekawego i przydatnego jest w Lispie. Niech ten post będzie pierwszym, małym przykładem mocy tego języka. Wyobraźmy sobie, że pracujemy ze skanerem do kodu źródłowego. To takie małe ustrojstwo, które czyta wyrażenie typu: x = a+3; i rozpoznaje składowe: identyfikator, operator=, identyfikator, operator+, liczba. W szczególności chcemy podać do tego skanera własne słowa kluczowe. Autorzy skanera przygotowali do tego specjalną funkcję:
(add-special-identifier (&key identifier string))
Funkcja przyjmuje dwa argumenty - identyfikator, jakim chcemy nazywać nasze słowo kluczowe w kodzie oraz string ze słowem kluczowym. Przykładowo, gdybyśmy chcieli skanować kod języka Ada, moglibyśmy dodać następujące słowo kluczowe:
(add-special-identifier :identifier :begin :string "begin")
Od teraz kiedy skaner rozpozna słowo kluczowe begin w analizowanym pliku, my dostaniemy token o identyfikatorze :begin. Wszystko wygląda super; zaczynamy dodawać kolejne słowa kluczowe:
(add-special-identifier :identifier :end :string "end")
(add-special-identifier :identifier :if :string "if")
(add-special-identifier :identifier :loop :string "loop")
; ...
Programistę uczulonego na powtarzanie się może zacząć powoli denerwować, że przekazujemy dwa nazwane argumenty do funkcji, chociaż de-facto wyglądają one tak samo - identyfikator nosi taką samą nazwę jak słowo kluczowe. Pytanie, czy nie dałoby podać tego identyfikatora tylko raz? Oczywiście da się. Możemy napisać funkcję, która nas wyręczy w pisaniu. Ale w Lispie możemy też poprosić kompilator, żeby wygenerował odpowiedni kod za nas. Napiszmy makro:
(defmacro add-keyword (what)
  `(add-special-identifier :identifier ,what :string ,(string what)))
Teraz słowa kluczowe możemy dodawać tak:
(add-keyword :begin)
(add-keyword :end)
; ...
Prawda, że mniej pisania? :). Nie wchodząc w szczegóły, w powyższej definicji makra rzeczy poprzedzone znaczkiem ` zostaną wstawione bezpośrednio do kodu źródłowego, a wewnątrz nich rzeczy poprzedzone znaczkiem , zostaną obliczone w czasie kompilacji (dokładniej w tzw. fazie ekspansji makr), a w kodzie znajdzie się wartość tych obliczeń. Przykładowo, wyraźenie ,(string what) w czasie kompilacji zamieni identyfikator na odpowiadający mu string. Powyższe linijki rozwiną się więc do:
(add-special-identifier :identifier :begin:string "BEGIN")
(add-special-identifier :identifier :end :string "END")
; ...
Ładny mi bajer..., ktoś powie - Przecież preprocesor w C też coś takiego umie!. Tak, to prawda - nasze makro jedynie "rozmnożyło" ten sam argument przekazywany do funkcji w dość prosty sposób. Ale - wracając do problemu - wciąż piszemy ręcznie wywołania funkcji dodających pojedyncze słowo kluczowe. Takie wypisywanie wywołań funkcji nie jest do końca tym samym, co zadeklarowanie: chcę dodać następujące słowa kluczowe: .... Dużo lepiej byłoby móc po prostu napisać:
(add-keywords :abort :abs :abstract :accept :access :aliased :all :array :at :begin :body :case :constant :declare :delay :delta :digits :do
	      :else :elsif :end :entry :exception :exit :for :function :generic :goto :if :in :interface :is :limited :loop :mod :new
	      :not :null :of :or :others :out :overriding :package :pragma :private :procedure :protected :raise :range :record :rem :renames
	      :requeue :return :reverse :select :separate :subtype :synchronized :tagged :task :terminate :then :type :until :use :when :while :with :xor)
i za jednym zamachem zdefiniować wszystkie słowa kluczowe Ady ;). Znów, możemy napisać funkcję, która w pętli wywoła dodawanie słowa kluczowego, ale pisanie takiej pętli za każdym razem, gdy chcemy dodać nowe rzeczy nie jest tym samym, co powiedzenie: chcę dodać następujące słowa: .... Poza tym, dlaczego to kompilator nie mógłby tego wszystkiego zrobić za nas?... ... Otóż może. Oto definicja makra add-keywords:
(defmacro add-keywords (&rest keyword-list)
  `(progn
     ,@(loop for word in keyword-list collect `(add-keyword ,word))))
Bez wchodzenia w szczegóły - bo nie to jest celem tego wpisu. Powyższe makro wykona w czasie kompilacji pętlę po wszystkich swoich argumentach, która zwróci w formie listy wyrażenia (add-keyword ,word), a które - jak pamiętamy - same są makrami, i chwilę później zostaną rozwinięte w wywołania naszej skomplikowanej funkcji dodającej słowa kluczowe do skanera. Mając takie dwa makra, pojedyncze wywołanie dodające wszystkie słowa kluczowe zostanie w czasie kompilacji rozbite na odpowiednie wywołania funkcji zaprezentowanej na początku. Zamieniliśmy dużo wpisanych ręcznie pojedynczych wywołań funkcji na dwa makra i jedno wywołanie! Tego już tak łatwo w C nie zrobimy :). Ten mały i stosunkowo prosty przykład makr Lispu miał pokazać ważną cechę tego języka - ponieważ można w nim wykonać dowolny kod w czasie kompilacji i przerabiać w ten sposób sam kompilowany kod, to można łatwo rozszerzać język i dopasowywać go do rozwiązywanego problemu. Dwoma prostymi makrami stworzyliśmy narzędzie dodające hurtem słowa kluczowe. A to już doskonale odpowiada wyrażeniu: Chcę dodać te słowa: .... Dwa słowa na koniec. Windowsowej binarki do ClozeCall na razie nie będzie - mam na razie z tym małe problemy :). Ale każdy, kto ma u siebie zainstalowaną implementację Common Lisp, może sobie pograć :).