Aktywne Wpisy
cinu4 +88
Najbardziej natrętnych neur0pków mam na czarno a i tak fala tuskoidów zalewa wykop przed wyborami. Gdyby mi ktoś w 2015 powiedział, że za 8 lat internet zacznie obciągać Donaldowi to bym wyśmiał, a teraz po prostu wiem, że ten naród skazany jest na bycie pod butem.
#bekazlewactwa #konfederacja #bekazpo #bekazneuropy #polityka #wybory
#bekazlewactwa #konfederacja #bekazpo #bekazneuropy #polityka #wybory
HrabiaTruposz +516
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 ...).
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])) ?
(= (read-string "(println (reduce + [1 2 3 4]))")
'(println (reduce + [1 2 3 4]))) ; zwraca true
(define-syntax my-unless
(syntax-rules ()
[(_ condition body)
(if (not condition)
body
(void))]))
Wykona się tak samo niezależnie czy nadpiszemy
not
.