Wpis z mikrobloga

#programowanie #learnclojurewithmikroblog #clojure

Odcinek 5 - wstęp do makr

Prawdziwa potęga lispów tkwi w makrach, ale zanim do nich dojdziemy, musimy dokładnie zrozumieć sposób działania języka.

Na wejściu jest string lub strumień wejściowy od użytkownika (w interpreterze) lub z pliku (w kompilatorze).

"(println (reduce + [1 2 3 4]))"
1. Część clojure o nazwie Reader parsuje tego stringa do clojurowej struktury danych, na razie nic nie wykonując. Można zobaczyć, jak to działa używając funkcji read-string.

(read-string "(println (reduce + [1 2 3 4]))") ; zwróci (println (reduce + [1 2 3 4])) czyli sparsowaną strukturę danych
Przyjrzyjmy się dokładniej typom każdego z elementów tej struktury. Jak pamiętamy - funkcja (type x) zwraca nam typ wartości x.

Moglibyśmy ręcznie wpisać (type (read-string "....")) dla każdego elementu, ale od czego mamy język programowania.

(


defn 
```**```
pokaz-typy [sparsowany-kod]
``````
  (
```**```
println 
```**```
(
```**```
str 
```**```
sparsowany-kod
``````
                
```_```
" -> "
```_```
                (type sparsowany-kod)))
``````
  (
```**```
if 
```**```
(coll? sparsowany-kod)
``````
      (
```**```
map 
```**```
pokaz-typy sparsowany-kod)))
``````
(pokaz-typy  (read-string "(println (reduce + [1 2 3 4]))")) ; wyświetli:

(println (reduce + [1 2 3 4])) -> class clojure.lang.PersistentList

println -> class clojure.lang.Symbol

(reduce + [1 2 3 4]) -> class clojure.lang.PersistentList

reduce -> class clojure.lang.Symbol

+ -> class clojure.lang.Symbol

[1 2 3 4] -> class clojure.lang.PersistentVector

1 -> class java.lang.Long

2 -> class java.lang.Long

3 -> class java.lang.Long

4 -> class java.lang.Long

```; i zwróci (nil (nil nil (nil nil nil nil))) bo każdy println zwraca nil, a nie chciało mi się tego wywalaćZauważmy, że kod w języku clojure jest jednocześnie strukturą danych w języku clojure! Można sobie zapisać kawałek już sparsowanego kodu w clojure na bok, przekazywać go funkcji w parametrze, operować na tym kodzie wewnątrz funkcji itp.To pewnie musi być trudne, pisać funkcje operujące na kodzie programu, prawda? CHWILECZKĘ, przecież przed chwilą napisaliśmy funkcję, która operuje na sparsowanym kodzie i pokazuje nam typ każdego wyrażenia! Nie różniło się to od pisania funkcji operujących na listach, bo KOD W CLOJURE TO SĄ LISTY (i wektory, i mapy) :)Taka własność języka nazywa się [http://pl.wikipedia.org/wiki/Homoikoniczno%C5%9B%C4%87](http://pl.wikipedia.org/wiki/Homoikoniczno%C5%9B%C4%87) i jest cechą języków lisp, która spowodowała, że od 64 lat ten język nie zmienił składni. Albowiem możemy sobie napisać funkcję, która bierze i zwraca sparsowany kod - taka funkcja nazywa się MAKRO :) I można zmieniać składnię języka bez zmiany języka - po prostu pisząc bibliotekę z makrami.Jednak zanim napiszemy pierwsze prawdziwe makro musimy dobrze zrozumieć sposób, w jaki clojure wykonuje kod. Reader mamy prawie obcykany. Jeszcze tylko trzeba wiedzieć, że jest kilka skrótów, które reader nam zapewnia. Najważniejszy to ' czyli cytowanie. Przykład:```
(read-string "'alamakota") ; tutaj jest 'alamakota ważny jest ten pojedyńczy ciapek - reader zamienia to na (quote alamakota).

```quote czyli cytowanie istnieje dlatego, że czasem nie chcemy, żeby clojure nam coś wyliczyło, ale chcemy to przekazać. Np```
'(+ 1 2) ; zwróci (+ 1 2) czyli nie wyliczy wartości tego wyrażenia, ale zwróci samo wyrażenie

(+ 1 2) ; zwróci 3

```Ok, Reader zwrócił nam sparsowany kod, widzieliśmy, że wywołania funkcji to listy, a identyfikatory mają typ clojure.lang.Symbol. Każdy identyfikator po sparsowaniu jest wartością o typie Symbol.Po sparsowaniu kodu clojure wykonuje eval na wyniku. Niestety z powodu zabezpieczeń na stronie tryclj.com nie pobawicie się evalem, bo ktoś by zaraz zhackował stronę - polecam zainstalowanie sobie clojure lokalnie, albo uwierzenie mi na słowo.eval na wartościach prostych zwraca tą wartość (eval 1) ; zwraca 1eval na kolekcjach rekurencyjnie wylicza elementy kolekcji a potem zwraca kolekcję tych wyliczonych wartości (eval [1 2 (+ 3 4)]) ; zwraca [1 2 7]eval na symbolu szuka wartości dla tego symbolu we wszystkich "zaimportowanych" namespacach, łącznie z aktualnym (eval println) ; zwraca tekstową reprezentację funkcji println, czyli #```
(def x "alamakota")

(eval x) ; zwraca "alamakota"

```Wartości w clojure nie są jednak trzymane bezpośrednio w symbolach, ale w obiektach Var. Namespace przechowują mapę Symbol -> Var, i kiedy ktoś odwołuje się do symbolu X, eval szuka Var dla tego symbolu w aktualnych namespacach, i jak go dostanie - zwraca wartość z tego Var. To szczegół implementacyjny, ale dzięki temu, że Varom można zmieniać wartości w obrębie wątku - w clojure można zrobić tak:```
(def + -)

(+ 1 2) ; zwróci -1

```Inaczej, niż w rubym, czy Pythonie - nie popsujemy w ten sposób całego programu, bo Vary można przedefiniowywać tylko w ramach jednego wątku. Jeśli mamy program wielowątkowy, to każdy wątek widzi globalną definicję +, a jak sobie ją zmieni, to widzi tą swoją - zmienioną definicję, ale pozostałe wątki dalej widzą globalne definicje. To dotyczy nie tylko funkcji, ale wartości dowolnego typu.Ok, wracając do eval-a.eval na liście jest specjalny - bo lista wywołuje funkcje. Eval na liście robi tak: - wylicza pierwszy element - jeśli to wyrażenie specjalne (nazywane też special form) to clojure robi coś specjalnego (tutaj lista wszystkich [http://clojure.org/special_forms](http://clojure.org/special_forms) ) - jeśli to makro, to wykonuje makro na argumentach, podstawia wynik makra zamiast siebie, i robi eval jeszcze raz - jeśli to funkcja, to wylicza rekurencyjnie pozostałe elementy listy, i wykonuje funkcję na tych argumentachTo jest główna różnica między makrami i funkcjami - makra mogą zadecydować, że NIE WYLICZĄ swoich argumentów. Normalne funkcje zawsze wyliczają swoje argumenty zanim się na nich wywołają.Jeszcze tylko pokażę, jak można sprawdzić, co robią makra.```
(macroexpand '(defn suma [coll] (reduce + coll))) ; zwróci (def suma (clojure.core/fn ([coll] (reduce + coll))))

Jak widać musiałem zacytować wyrażenie, żeby macroexpand go dostało. Jak również widać - defn to macro, które po prostu robi (def nazwa (fn funkcja ...)).

Na dzisiaj to tyle - jutro napiszemy nasze pierwsze makro.

Zadanie 5. napisać funkcję, która dostanie sparsowany kod, i zwróci wszystkie identyfikatory, które w nim występują. (defn identyfikatory [kod] ...)

PS. ionterpretery clojure i innych lispów nazywane są REPL, od Read Eval Print Loop. Bo robią w kółko (read ...) -> (eval ...) -> (print ...).
  • 12
Rozwiązanie zadania 4. http://pastebin.com/4kzuWbwg

Przykład użycia:

(do-times (fn [] (println "ala")) 10) ; wyświetli 10 "ala" i zwróci nil
Jestem ciekaw, czy dzisiejszy odcinek był jasny, bo na makrach i cytowaniu łatwo się zgubić, a potem się na tym non-stop bazuje.

Wiecie na przykład, czemu do (pokaz-typy [kod]) przekazywałem (read-string "(println (reduce + [1 2 3 4]))") a nie samo (println (reduce + [1 2 3 4])) ?
@tell_me_more: Ze mną to raczej poprowadzisz dyskusję, ale nie na poziomie kogoś kto się uczy :D (choć sam się uczę Scheme, ale z pozostałych języków ma się już jakiś bagaż, więc wszystko przychodzi łatwiej).
@tell_me_more: Po przeczutaniu ang Wiki rozumiem. Chodzi o to, że higienic macro wywoływana jest podobnie jak funkcja. W kontekście deklaracji a nie wywołania. Przykładowo:

(define-syntax my-unless

(syntax-rules ()

[(_ condition body)

(if (not condition)

body

(void))]))

Wykona się tak samo niezależnie czy nadpiszemy

not
.
@erwit: tak, jeden praktycznie gotowy, tylko muszę go sformatować jakoś lepiej (syntax quote czyli ` psuje formatowanie kodu na wykopie, a bez tego ciężko makra w clojure robić ;) ), pewnie w weekend dodam, ostatnio miałem mało czasu