Wpis z mikrobloga

#programowanie #clojure #learnclojurewithmikroblog

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.
  • 4
  • Odpowiedz
@tell_me_more: a, jeszcze jestem wam winny rozwiązanie zadania 5. Można np tak:

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
  • Odpowiedz
@tell_me_more: łatwiejszy sposób na napisnaie zad 6, dziś się zorientowałem, że przecież można:

(


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 :)
  • Odpowiedz