Aktywne Wpisy
tormentorer +5
Jestem takim nieszczególnie żarliwym Katolikiem, ale coś mnie trafia, jak czytam że w Polsce,kraju, który wyrósł i uformował swoją tożsamość na barkach chrystianizacji, jakiś watażka zabrania wieszania krzyży w urzędach publicznych - niech by ktoś spróbował wywinąć taki numer w takich Niemczech, dajmy na to Bawarii - w super zlaicyzowanym społeczeństwie, to pogoniliby dziada bez mrugnięcia okiem. Dlatego apeluję - nie głosujmy na takie indywidua- i pamiętajmy o swoich korzeniach. 8 gwiazdek,
Marek_Tempe +8
Lepiej z mądrym zgubić, niż z głupim znaleźć.
Odcinek 6. Makra - pierwsza krew.
Przepraszam za opóźnienie, teraz trochę rzadziej będą kolejne odcinki - tematy są trudniejsze do wyjaśnienia, i mam trochę mniej czasu. Ok, lecimy:
Jak już powiedziałem - makro to po prostu funkcja przyjmująca i zwracająca kod clojure. Kod jest przyjmowany, przetwarzany i zwracany w postaci sparsowanej - czyli jako zagniżdżone struktury danych clojure - listy, wektory, mapy itd. Głównie listy, które sa skłądnią wywoływania funkcji (i makr).
Napiszmy nasze pierwsze makro - zignoruje ono kod przekazany w parametrze i zamiast tego wypisze "BAZINGA!". Gdyby była to funkcja, to wyglądałaby tak:
(defn zrob-dowcip [kod] (println "BAZINGA!"))
Sprawdźmy, czy zadziała:
(zrob-dowcip (println (+ 1 2 3 (* 4 5 6)))) ; wypisze 126, BAZINGA, i zwróci nil.
Miało zignorować kod, a nie wykonywać go. Jednak funkcje tak nie mogą - funkcje zawsze wyliczają swoje argumenty przed wykonaniem. Można niby zacytować argumenty:
(zrob-dowcip '(println (+ 1 2 3 (* 4 5 6)))) ; BAZINGA, i zwróci nil.
Ale jest to niewygodne. Spróbujmy zrobić makro.
(defmacro zrob-dowcip [kod] (println "BAZINGA!"))
Przetestujmy (cytować kodu nie trzeba, bo to makro i nie wylicza argumentów przed wywołaniem):
(zrob-dowcip (println (+ 1 2 3 (* 4 5 6)))) ; wypisuje BAZINGA! i zwraca nil
Wygląda na to, że działa. Ale sprawdźmy jeszcze macroexpandem.
(macroexpand '(zrob-dowcip (println (+ 1 2 3 (* 4 5 6))))) ; wyświetla BAZINGA i zwraca nil
Nie o to nam chodziło - miało podmienić kod na (println "BAZINGA!"), i przekazać dalej, a ono po prostu wykonuje treść zrob-dowcip. Tak naprawdę napisaliśmy makro, które podmienia kod na nil i wypisuje BAZINGA! w międzyczasie, a nie to, co chcieliśmy.
Spróbujmy jeszcze raz - mamy zwrócić kod (println "BAZINGA!") a nie wykonać go, więc go zacytujmy:
(defmacro zrob-dowcip2 [kod] '(println "BAZINGA!"))
Efekt jest dalej taki sam:
(zrob-dowcip2 (println (+ 1 2 3 (* 4 5 6)))) ; wypisuje BAZINGA! i zwraca nil
Natomiast dzięki funkcji macroexpand widać, że makro robi to, co chcieliśmy:
(macroexpand '(zrob-dowcip2 (println (+ 1 2 3 (* 4 5 6))))) ; zwraca (println "BAZINGA!"))
Czyli w kroku rozwinięcia makra kod (println (+ 1 2 3 (* 4 5 6)))) zostaje zamieniony na (println "BAZINGA!"), a następnie jest wyliczony i dlatego wypisuje "BAZINGA!" i zwraca nil.
Ok, to teraz makro, które coś zrobi z kodem, który dostaje w parametrze. Niech np wykona ten kod 2 razy. Proste, prawda?
(defmacro dwukrotnie [kod]
'(do (kod) ; tutaj jest cytowanie, bo chcemy zwrócić (do ...) kod, a nie wykonać
(kod)))
Sprawdźmy.
(dwukrotnie (println (+ 1 2))) ; wywali się z błędem CompilerException java.lang.RuntimeException: Unable to resolve symbol: kod in this context
Compiler exception, bo wykonanie makra dzieje się na etapie kompilacji (w clojure i innych lispach kompilacja i wykonywanie programu mogą się przeplatać :) ). Czemu nie zadziałało? Bo odwołuje się do symbolu "kod", który nie jest zdefiniowany w czasie wykonywania kodu po przekształceniu przez makro. Zobaczmy:
(macroexpand '(dwukrotnie (println (+ 1 2)))) ; zwraca (do (kod) (kod))
Niby tak mu kazaliśmy, ale nie o to nam chodziło... Wolelibyśmy, żeby za kod podstawić to, co przekazaliśmy do makra. Jednak cytowanie przy użyciu ' zatrzymuje wyliczenie wszystkiego wewnątrz podczas wykonywania makra, więc pod lokalnie zdefiniowany symbol "kod" nie jest podstawiona jego wartość podczas wykonywania makra, tylko jest on zwracany razem z resztą. Można to rozwiązać sklejając cytowany 'do z wyliczaną wartością, ale to niewygodne, szczególnie w bardziej rozbudowanych makrach.
Przydałby się nam jakiś sposób na zacytowanie tylko kawałka kodu, i wypełnienie w nim pustych miejsc. Taki system szablonów. Oczywiście twórca clojure o tym pomyślał‚ - w końcu spora część języka to macra i musiał napotkać ten problem. Rozwiazaniem jest cytowanie składniowe(syntax-quote), czyli ` w odróżnieniu od zwykłego quote, czyli '.
Sprawdźmy, jak działa syntax quote w porównaniu do normalnego:
(def x 997)
(def y 1337)
(+ x y (- x y)) ; bez cytowania zwróci 1994 - po prostu wyliczyliśmy wszystko
'(+ x y (- x y)) ; zwykłe cytowanie zwróci (+ x y (- x y)) - nic nie zostało wyliczone
`(+ x y (- x y)) ; cytowanie składniowe (bez unquote) zwróci (clojure.core/+ user/x user/y (clojure.core/- user/x user/y)) - symbole dostały namespace-a, ale dalej nic nie wyliczono
`(+ ~x ~y (- x y)) ; zwróci (clojure.core/+ 997 1337 (clojure.core/- user/x user/y)) - tam, gdzie była tylda przed wyrażeniem zostało ono wyliczone.
Ta tylda nazywa się unquote i razem z syntax-quote daje nam system szablonów, dziki któremu pisanie makr jest dużo prostsze. Inny przykład, żeby pokazać, że tylda włącza wyliczanie całego wyrażenia po niej, łącznie z podwyraĹĽeniami:
`(+ x y ~(- x y)) ; zwróci (clojure.core/+ user/x user/y -340)
Ok, to skoro umiemy cytować część kodu, i podmieniać resztę - spróbujmy napisać nasze makro "dwukrotnie" jeszcze raz:
(defmacro dwukrotnie [kod]
`(do ~kod
~kod))
(macroexpand '(dwukrotnie (println "A"))) ; zwraca (do (println "A") (println "A"))
(dwukrotnie (println "A")) ; wypisuje A 2 razy i zwraca nil
Gratulacje - nasze makro działa. W kolejnym odcinku makra, które się do czegoś przydają.
Zadanie 6. Napisać makro (if-not [warunek na-false na-true] ...) działające jak odwrotność if.
Zdefiniujmy sobie funkcję pomocniczą walk, która przejdzie nam rekurencyjnie po drewku kodu, i na każdej kolekcji wykona funkcję fs, a na nie-kolekcji - funkcję f.
(
defn
```**```
walk [kod f fs]
(
```**```
if
```**```
(coll? kod)
(fs (
```**```
map
```**#```
(walk % f fs) kod))
(f kod)))
```I teraz chcemy dla każdego elementu, który nie jest kolekcją - albo go zachować (jeśli jest identyfikatorem), albo go
(
defn
```**```
rec-filter-ids [kod]
(
```**```
filter symbol?
```**```
(flatten kod)))
W clojure jak funkcja ma więcej, niż kilka linijek, to zwykle znaczy, że robisz coś na około :)