Poczytaj mi Clojure – cz. 3: Pierwsze kroki

Znając podstawowe pojęcia obecne w języku Lisp i jego dialekcie Clojure, możemy przystąpić do instalacji interpretera, która dzięki odpowiedniemu narzędziu przebiega sprawnie i automatycznie. Korzystając z trybu interaktywnego, będziemy mogli dowiedzieć się więcej o języku, posługując się praktycznymi przykładami.

Pierwsze kroki

Interpreter

Clojure jest Lispem, który działa pod kontrolą maszyny wirtualnej Javy (ang. Java Virtual Machine, skr. JVM). Konstrukcje języka są zaimplementowane z użyciem odpowiednich obiektów i metod Javy, które z kolei poddawane są optymalizacji i przetwarzaniu przez kompilator JIT (ang. Just-in-Time compiler).

Nie będzie zbytnim nadużyciem, gdy nazwiemy Clojure językiem interpretowanym, chociaż w praktyce jest on również kompilowany (do pseudokodu, a następnie do kodu maszynowego). Przyjęło się, że sformułowania “interpreter Clojure” używa się raczej w odniesieniu do interaktywnej konsoli języka, którą będziemy mieli okazję poznać już za chwilę.

Instalacja

Najprostszym sposobem instalacji interpretera języka Clojure jest skorzystanie ze skryptu Leiningen. Przypomina on np. znanego ze środowisk opartych o Ruby’ego Bundlera i trochę RVM-a. Do poprawnej pracy wymagane jest oczywiście środowisko uruchomieniowe Javy.

Najlepiej utworzyć w katalogu domowym podkatalog bin, jeśli jeszcze nie istnieje, a w pliku .profile dodać ustawianie zmiennej PATH w taki sposób, aby przeszukiwana była również ta ścieżka:

1
2
3
if [ -d "$HOME/bin" ] ; then
    PATH="$HOME/bin:$PATH"
fi

Potem możemy już pobrać i zainstalować stabilne wydanie Leiningena:

1
2
3
4
5
[ ! -d "$HOME/bin" ] && mkdir "$HOME/bin"

wget -O ~/bin/lein \
  https://raw.github.com/technomancy/leiningen/stable/bin/lein && \
  chmod 755 ~/bin/lein && ~/bin/lein upgrade

Kolejny krok to uruchomienie interpretera w trybie interaktywnym (REPL):

lein repl

REPL

Interaktywny interpreter języka Lisp to pętla (ang. loop) zajmująca się wczytywaniem (ang. read), wartościowaniem (ang. evaluate) i wypisywaniem (ang. print) – po angielsku read–eval–print loop (skr. REPL). Nazywa się ją też czasem pętlą wczytaj–wykonaj–wypisz, chociaż osobiście wolę tłumaczyć słowo “eval” jako “oblicz”.

Dzięki REPL możliwe jest tworzenie krótkich, testowych programów i odpluskwianie (ang. debugging) już działających w kontekście pętli.

Standardowym wejściem, wyjściem i wyjściem diagnostycznym REPL nie musi być tylko urządzenie prawdziwego lub wirtualnego terminalu, ale także gniazdo dziedziny Uniksa czy TCP/IP. Istnieją projekty, jak np. CIDER, które pozwalają na integrowanie popularnych edytorów, takich jak ViM czy Emacs, z działającym jako demon programem REPL. Możliwa jest wtedy wygodna analiza tworzonych programów czy wykonywanie wybranych fragmentów w celach testowych.

Funkcje pomocnicze

W środowisku interaktywnym możemy korzystać z przydatnych, pomocniczych funkcji:

  • (doc         nazwa) – wyświetla dokumentację dla elementu o wskazanej nazwie;
  • (find-doc fragment) – działa jak doc, ale można podać fragment nazwy lub wyrażenie regularne;
  • (apropos  fragment) – zwraca sekwencję znalezionych elementów we wszystkich przestrzeniach nazw na podstawie fragmentu nazwy lub wyrażenia regularnego;
  • (javadoc     klasa) – dla podanej klasy lub obiektu Javy otwiera plik z dokumentacją w przeglądarce;
  • (source     symbol) – zwraca kod źródłowy dla znalezionego symbolu (wymagane jest, aby symbol powiązany był ze zmienną globalną w przestrzeni nazw, której plik .clj jest w ścieżce przeszukiwania klas);
  • (pst              ) – wyświetla informacje dot. śledzenia stosu wywołań (ang. stack trace).

Zobacz także:

Wczytywanie i wartościowanie

Gdyby usunąć element interaktywny z REPL, to proces interpretowania programu można podzielić na dwie główne fazy: wczytywaniawartościowania.

W pierwszej fazie dochodzi do pobrania danych z dysku lub standardowego wejścia i sprawdzenia ich pod względem syntaktycznym. Dokonuje tego komponent zwany czytnikiem (ang. reader). Jeśli składnia jest poprawna, to S-wyrażenia są zmieniane na odpowiadające im reprezentacje w pamięci. Na przykład listowe S-wyrażenia stają się listami, a atomowe S-wyrażenia odpowiednimi obiektami. Mamy do czynienia z zamianą wczytywanych wyrażeń symbolicznych na zrozumiałe dla interpretera dane znajdujące się w pamięci.

Na tym jednak nie koniec, ponieważ należy z owych danych zrobić jakiś użytek. W tym momencie do gry wchodzi ewaluator (ang. evaluator), który przetwarza umieszczone w pamięci struktury danych i traktuje jak formuły przeznaczone do wartościowania.

W efekcie powstają nowe formuły (np. zwracane przez funkcje czy stanowiące wartości atomów). W zależności od intencji programisty, mogą być one użyte do prowadzenia dalszych obliczeń (np. podstawione jako argumenty innych funkcji), lub też ponownie wczytane i wartościowane – a więc stać się kodem.

Warto zapamiętać, że obliczanie wartości S-wyrażeń, zwane też ich wartościowaniem polega na tym, że po wczytaniu i rozpoznaniu w nich odpowiednich formuł języka wykonywane jest rekurencyjne wywoływanie ewentualnych podprogramów, aż do momentu, gdy uzyskane będą formuły stałe, czyli niezmienne wartości, których nie da się już przekształcić do innej postaci.

Rekurencja polega tu na tym, że niektóre formuły mogą stwarzać nowe konstrukcje, które nie będą formułami stałymi, więc aby takowe uzyskać, trzeba będzie dokonać dalszych obliczeń i np. wywołać zwróconą funkcję czy makro.

Praca z konsolą

Spróbujmy zaprzyjaźnić się z interaktywną konsolą języka, korzystając z polecenia lein:

Uruchamianie REPL
1
lein repl

Po uruchomieniu powinniśmy ujrzeć zapis podobny do poniższego:

nREPL server started on port 34731 on host 127.0.0.1 - nrepl://127.0.0.1:34731
REPL-y 0.3.5, nREPL 0.2.6
Clojure 1.6.0
OpenJDK 64-Bit Server VM 1.7.0_65-b32
    Docs: (doc function-name-here)
          (find-doc "part-of-name-here")
  Source: (source function-name-here)
 Javadoc: (javadoc java-object-or-class-here)
    Exit: Control+D or (exit) or (quit)
 Results: Stored in vars *1, *2, *3, an exception in *e

user=> 

Możemy już podawać S-wyrażenia, które zostaną obliczone, jeśli tylko będą wyrażały poprawne formuły (konstrukcje) języka. Zacznijmy od formuł stałych, czyli takich, które wyrażają własne wartości:

Przykłady formuł stałych
1
2
3
4
5
6
7
8
9
10
11
12
13
14
1              ; literał liczbowy (liczba)
; => 1

             ; literał znakowy (może być to znak wielobajtowy)
; => \ą

"test"         ; łańcuch znakowy
; => "test"

:test          ; klucz
; => :test

'test          ; symbol (w formie stałej)
; => test

W podanym wyżej przykładzie z linii pierwszej atomowe S-wyrażenie 1 (literał liczbowy) zostało obliczone, a ponieważ reprezentuje ono formułę stałą, więc rezultatem była jego własna wartość (1).

W dalszych przykładach będziemy korzystać z symbolu komentarza z dodaną strzałką, aby pokazywać rezultaty obliczeń wyświetlane przez REPL, a symbolu komentarza z podwójnym znakiem większości, żeby oznaczyć informacje ze standardowego strumienia wyjściowego lub strumienia błędów:

  • ; => – rezultaty obliczeń,
  • ; >> – standardowe wyjście.

W ostatniej linii wcześniejszego przykładu użyliśmy symbolu, który może mieć specjalne znaczenie, jeśli tylko pominiemy znacznik cytowania (apostrof) i zamiast 'test napiszemy po prostu:

1
2
test
; => #<core$test [email protected]>

Co się stało? Podaliśmy atomowe S-wyrażenie, które nie wyraża formuły stałej, ale formułę symbolową. To nie pomyłka – symbole mogą wyrażać różne formuły (istnieć w wielu formach). Jeżeli są zacytowane (z użyciem apostrofu lub konstrukcji quote), to przeliczane będą do wartości własnych (form stałych symboli), a jeśli zapiszemy je wprost, to zależnie od kontekstu albo ich wartościami staną się identyfikowane z ich pomocą struktury pamięciowe (formuła symbolowa), albo jakieś wartości zostaną z nimi powiązane (formuła powiązaniowa).

W powyższym przykładzie mamy do czynienia z drugim wspomnianym przypadkiem (symbol wyraża wartość), a wskazywanym obiektem pamięciowym jest funkcja test. Mechanizmy języka przeszukały globalną strukturę danych zwaną przestrzenią nazw i znalazły w niej przyporządkowanie symbolu test do obiektu funkcji.

Funkcja nie została wywołana, ponieważ nie życzyliśmy sobie tego. W Clojure, podobnie jak w innych Lispach, funkcje są jednostkami pierwszej kategorii, czyli mogą być traktowane tak samo, jak wartości innych typów danych, np. jak liczby czy napisy. Podając nazwę funkcji, uzyskaliśmy więc funkcyjny obiekt, a w REPL diagnostyczny napis, który go reprezentuje.

Możemy sprawdzić z jakiego typu (czy z jakiej klasy) obiektem mamy do czynienia, korzystając z funkcji type:

Przykład sprawdzania typu obiektu
1
2
(type test)
; => clojure.core$test

Gdybyśmy chcieli funkcję wywołać, musielibyśmy stworzyć listowe S-wyrażenie, które wyraża formułę funkcyjną:

Przykład wywołania funkcji
1
(test)

Nawiasy tworzą listowe S-wyrażenie. Jego pierwszym (i w tym przykładzie jedynym) elementem jest symbol. Symbol ten nie jest zacytowany, więc zostanie potraktowany jak formuła symbolowa, która identyfikuje pewien obiekt w pamięci. Jeżeli jest to obiekt typu funkcyjnego, to całe wyrażenie będzie potraktowane jak formuła funkcyjna, a podprogram funkcji zostanie wywołany. W tym przypadku pojawi się jednak komunikat o błędzie:

ArityException Wrong number of args (0) passed to: core/test
               clojure.lang.AFn.throwArity (AFn.java:429)

Próba wywołania funkcji spowodowała wygenerowanie wyjątku, który komunikuje, że podano złą liczbę argumentów. Dowiedzmy się co dokładnie robi funkcja test, korzystając z wbudowanej funkcji doc:

Przykład wywołania funkcji doc
1
2
(doc test)
; => nil

Na wyjściu zauważymy:

-------------------------
clojure.core/test
([v])
test [v] finds fn at
key :test in var metadata and calls it,
  presuming failure will throw exception

Okazuje się, że funkcja ta przyjmuje jeden wymagany argument, którego nie podaliśmy.

Skorzystajmy z innego przykładu wywoływania funkcji. Wybierzmy taką, która nie wymaga podania żadnych argumentów. Może operator dodawania?

Przykład wywołania funkcji sumującej
1
2
(+)
; => 0

Znów mamy listowe S-wyrażenie (potocznie listę lub po prostu wyrażenie), którego pierwszym elementem jest niezacytowany symbol. Gdy interpreter widzi taki zapis, to zakłada, że podana lista wyraża formułę funkcyjną, formułę specjalną lub formułę makrową. Przeszukuje więc odpowiednią przestrzeń pamięci i stara się odnaleźć tam symbol o nazwie takiej, jak nazwa symbolu podanego na pierwszym miejscu listy.

W przypadku wbudowanej funkcji o symbolicznej nazwie +, będziemy mieli do czynienia z podprogramem odpowiedzialnym za sumowanie. W Clojure funkcja sumująca może nie przyjmować żadnych argumentów, a wtedy – zgodnie z przewidywaniami – zwróci wartość 0.

Pierwszy element listowego S-wyrażenia możemy też nazywać operatorem, a jego argumenty operandami. Użyjmy więc operatora dodawania z jakimiś operandami:

Przykłady użycia operatora dodawania
1
2
3
4
5
(+ 2 2)
; => 4

(+ 1 2 3)
; => 6

Wartości argumentów przekazywanych do funkcji, podawane jako kolejne elementy symbolicznie wyrażonej listy, są obliczane zanim funkcja zostanie wywołana. Możemy więc konstruować zagnieżdżone S-wyrażenia, w których wykonamy wiele operacji, a ich rezultaty będą przekazywane przez wartość będącą efektami ich wykonania. Spróbujmy do liczby 2 dodać różnicę między 10 a 5:

Przykład zagnieżdżonych S-wyrażeń
1
2
(+ 2 (- 10 5))
; => 7

Sterowanie wykonywaniem

Clojure wyposażono w wiele formuł specjalnych i makr, które służą do sterowania procesem wartościowania wyrażeń, czyli wykonywania się programu.

Wyrażenia warunkowe

Znane z języków imperatywnych instrukcje warunkowe w Lispach mają odpowiedniki jako warunkowe formuły specjalne lub makra. Formuła specjalna i makro tym różnią się od funkcji, że przekazywane do nich wybrane argumenty mogą być wartościowane lub nie (zależnie od konkretnej formuły) przed wywołaniem podprogramu. Dzięki tej cesze możliwe jest budowanie m.in. takich wyrażeń, w których obliczenie wartości pewnych argumentów zależy od rezultatu obliczenia innych (wcześniejszych). Można więc sterować tym, czy i kiedy zadziała zasada przekazywania przez wartość, a tym samym budować konstrukcje warunkowe czy pętle.

Formuły specjalne i makra będą omówione później, a podstawowa różnica między nimi jest taka, że makra są elementem języka pozwalającym dokonywać przekształceń kodu źródłowego, natomiast formuły specjalne wbudowanymi w ewaluator konstrukcjami. Zarówno jedne, jak i drugie mogą mieć odmienne od przyjętego sposoby wartościowania i przekazywania argumentów czy wpływania na otoczenie (stany innych pamięciowych obiektów).

Wykonywanie warunkowe, formuła specjalna if

Formuła specjalna if służy do warunkowego obliczania wyrażeń.

Użycie:

  • (if warunek wyrażenie-prawda wyrażenie-fałsz?).

Formuła if przyjmuje dwa obowiązkowe argumenty. Pierwszym jest warunek do przeliczenia, który jeśli nie będzie przyjmował ani wartości false ani nil, sprawi, że kolejny argument zostanie również obliczony. Jeśli podano opcjonalny trzeci argument, to będzie on obliczony tylko wtedy, gdy pierwszy argument ma wartość false lub nil.

Formuła specjalna if zwraca wartość ostatnio obliczonego wyrażenia lub nil, jeżeli nie podano trzeciego argumentu, a pierwsze wyrażenie zwróciło logiczny fałsz.

Przykład użycia formuły specjalnej if
1
2
3
4
(if (= 2 2)
  "równe"
  "różne")
; => "równe"

Odwrotne wykonywanie warunkowe, makro if-not

Makro if-not jest odwrotną wersją formuły specjalnej if.

Użycie:

  • (if-not warunek wyrażenie-fałsz wyrażenie-prawda?).

Makro przyjmuje dwa obowiązkowe argumenty. Pierwszym jest warunek do przeliczenia, który jeśli będzie przyjmował wartość logicznego fałszu (false lub nil), sprawi, że kolejny argument zostanie również obliczony. Jeśli podano opcjonalny trzeci argument, to będzie on obliczony tylko wtedy, gdy pierwszy argument reprezentuje logiczną prawdę (ma wartość różną od false i różną od nil).

Makro if-not zwraca wartość ostatnio obliczonego wyrażenia lub nil, jeżeli nie podano trzeciego argumentu, a pierwsze wyrażenie zwróciło logiczną prawdę.

Przykład użycia makra if-not
1
2
3
4
(if-not (= 2 2)
  "różne"
  "równe")
; => "równe"

Iloczyn logiczny, makro and

Makro and służy do wyrażania operacji koniunkcji logicznej (iloczynu logicznego). Bywa często wykorzystywane w połączeniu z formułą specjalną if, aby czytelnie przedstawić złożone warunki logiczne.

Użycie:

  • (and & wyrażenie…).

Makro oblicza wartości kolejnych wyrażeń podanych jako jego argumenty (w porządku występowania) dopóki ich wartością jest logiczna prawda (nie wartość false i nie nil). Zwracaną wartością jest wtedy wartość ostatnio podanego wyrażenia. Jeżeli któreś z wyrażeń zwróci wartość false lub nil, przetwarzanie kolejnych argumentów jest wstrzymywane i zwracana jest jego wartość.

Jeżeli nie podamy żadnych argumentów, makro and zwraca wartość true.

Przykłady użycia makra and
1
2
3
4
5
6
7
8
9
10
11
12
13
(and)               ; => true
(and false       )  ; => false
(and nil         )  ; => nil
(and nil false   )  ; => nil
(and 1 2 3 4     )  ; => 4

(and true false  )  ; => false
(and false true  )  ; => false
(and false false )  ; => false
(and true true   )  ; => true

(if (and (= 2 2) (< 2 3)) true)
; => true

Suma logiczna, makro or

Makro or służy do wyrażania operacji sumy logicznej (logicznej alternatywy). Bywa często wykorzystywane w połączeniu z formułą specjalną if, aby czytelnie przedstawić złożone warunki logiczne.

Użycie:

  • (or & wyrażenie…).

Makro oblicza wartości kolejnych wyrażeń podanych jako jego argumenty (w kolejności występowania) do momentu aż wartością któregoś będzie logiczna prawda (nie wartość false i nie nil). Zwracaną wartością jest wtedy wartość ostatnio przetwarzanego wyrażenia. Jeżeli wartości wszystkich wyrażeń to false lub nil, zwracana jest wartość ostatniego podanego wyrażenia (false lub nil).

Gdy nie podamy argumentów, makro or zwraca wartość nil.

Przykłady użycia makra or
1
2
3
4
5
6
7
8
9
10
11
12
13
(or)               ; => nil
(or false       )  ; => false
(or nil         )  ; => nil
(or nil false   )  ; => false
(or 1 2 3 4     )  ; => 1

(or true false  )  ; => true
(or false true  )  ; => true
(or false false )  ; => false
(or true true   )  ; => true

(if (or (= 2 2) (< 4 3)) true)
; => true

Wykonywanie warunkowe, makro when

Makro when działa podobnie do konstrukcji if, przy czym jest nieco prostsze, ponieważ nie ma w nim miejsca na wyrażenie alternatywne (wykonywane, gdy podany warunek nie jest spełniony).

Użycie:

  • (when warunek & wyrażenie…).

Pierwszy argument powinien być formułą wyrażającą warunek logiczny (zwracającą true lub false, ew. nil), a każdy pozostały będzie potraktowany jak wyrażenie, które zostanie obliczone, gdy wartością zwracaną przez konstrukcję warunkową będzie logiczna prawda (wartość różna od false i różna od nil).

Przykład użycia makra when
1
2
3
4
5
6
(when (= 2 2)
  (println "można podać")
  (println "wiele wyrażeń"))

; >> można podać
; >> wiele wyrażeń

Odwrotne wykonywanie warunkowe, makro when-not

Makro when-not jest odwrotną wersją makra when.

Użycie:

  • (when-not warunek & wyrażenie…).

Pierwszy argument powinien być predykatem, a każdy pozostały będzie potraktowany jak wyrażenie, które zostanie obliczone, gdy wartością predykatu będzie logiczny fałsz (wartość false lub nil).

Przykład użycia makra when-not
1
2
3
4
5
6
(when-not (= 2 1)
  (println "można podać")
  (println "wiele wyrażeń"))

; >> można podać
; >> wiele wyrażeń

Lista warunków, makro cond

Makro cond pozwala zapisać listę warunków z przypisanymi do niej wyrażeniami, które zostaną obliczone, gdy dany warunek będzie spełniony.

Użycie:

  • (cond & opcja…),

gdzie opcja to:

  • warunek wyrażenie,
  • :else wyrażenie-domyślne.

Makro przyjmuje parzystą liczbę argumentów. Dla każdej pary (opcji) sprawdza, czy podane wyrażenie warunkowe zwraca wartość różną od false i od nil. Jeżeli tak jest, wartościowany będzie przypisany mu argument z pary (wyrażenie) i przetwarzanie dalszych argumentów zostanie wstrzymane, a zwracaną wartością będzie wartość obliczonego wyrażenia.

Opcjonalnie można dodać ostatnią parę (opcję), której pierwszy element równy będzie :else. Przypisane mu wyrażenie zostanie obliczone, gdy żadne wcześniejsze nie zakończyło pracy. Pozwala to ustawiać domyślną wartość zwracaną.

Jeżeli nie podano opcji domyślnej, a żaden warunek nie był spełniony, zwracaną wartością będzie nil.

Przykład użycia makra cond
1
2
3
4
5
6
(cond
  (= 2 3) "2 i 3 są równe"
  (< 2 3) "2 jest mniejsze niż 3"
  :else   "nic")

; => "2 jest mniejsze niż 3"

Lista przypadków, formuła specjalna case

Formuła specjalna case działa podobnie do cond, ale zamiast wyrażeń warunkowych w opcjach należy podawać stałe wartości, które będą dopasowywane do wartości pierwszego przekazanego argumentu wywołania.

Użycie:

  • (case wartość opcja…),

gdzie opcja to:

  • wartość wyrażenie,
  • (wartość…) wyrażenie,
  • wyrażenie-domyślne.

Jeśli chcemy porównywać podaną wartość z wieloma z serii dla każdej z możliwych opcji, to należy tę serię możliwości zapisać w postaci listowego S-wyrażenia.

Wartości, z którymi porównywana będzie wartość pierwszego argumentu muszą być formułami stałymi, ponieważ są opracowywane w trakcie kompilacji. Możemy więc podawać literały czy inne atomowe reprezentacje, które wyrażają własne wartości. Wyrażenia do obliczenia w przypadku pozytywnego przejścia testu mogą z kolei przedstawiać dowolne formuły.

Opcjonalnie możliwe jest podanie jako ostatniego argumentu wyrażenia, które zostanie obliczone, jeśli badana wartość nie spełni żadnego z wcześniej podanych warunków. Warto z tej możliwości korzystać, ponieważ w przeciwnym wypadku program zgłosi wyjątek, gdy żadne z dopasowań nie będzie spełnione.

Przykład formuły specjalnej case
1
2
3
4
5
6
7
8
9
(def x 'herbatniki) ; x odnosi się do symbolu herbatniki

(case x
  ""                         "nic"
  (draże miętusy)       "cukierki"
  (herbatniki pierniki)  "ciastka"
  (str x " to produkt nieznany"))

; => "ciastka"

Warto zauważyć, że symbole i listy nie zostały zacytowane. To właśnie właściwość formuł specjalnych – pewne wyrażenia nie są wartościowane, nie zachodzi więc konieczność stosowania specjalnych zabiegów, które zapobiegałyby wartościowaniu.

Lista warunków z predykatem, makro condp

Makro condp działa podobnie jak cond, ale jest o wiele bardziej elastyczne.

Użycie:

  • (condp predykat wyrażenie & opcja…)

Jako pierwszy argument należy podać predykat, czyli funkcję, która powinna zwracać wartość logiczną, a jako drugi wyrażenie. Kolejne, nieobowiązkowe argumenty to składające się z par opcje, które mogą być wyrażone następującymi zapisami:

  • wyrażenie-testowe wyrażenie-wynikowe,
  • wyrażenie-testowe :>> funkcja-wynikowa,
  • wyrażenie-domyślne.

Symboliczny zapis :>> to słowo kluczowe, które ma w kontekście makra condp specjalne znaczenie.

Dla każdej podanej opcji zostanie wywołana formuła funkcyjna:

  • (predykat wyrażenie-warunkowe wyrażenie).

Predykat będzie wywoływany dla każdej podanej w kolejności opcji z dwoma przekazywanymi argumentami. Pierwszy będzie wartością wyrażenia testowego danej opcji, a drugi wyrażeniem podanym jako argument wywołania całego makra.

Jeżeli predykat zwróci wartość odpowiadającą logicznej prawdzie (różną od false i różną od nil), to sprawdzanie kolejnych opcji zakończy się i zostanie zwrócone wyrażenie wynikowe przypisane do bieżącej opcji lub wartość powstała na skutek wywołania funkcji wynikowej (w przypadku, gdy użyto słowa kluczowego :>>). W tym ostatnim przypadku do funkcji (która powinna przyjmować jeden argument) zostanie przekazana wartość zwracana przez predykat.

W przypadku, gdy jako ostatnią z opcji podano dodatkowe wyrażenie (wyrażenie-domyślne), jego wartość zostanie zwrócona, jeśli żadna z opcji nie zostanie dopasowana do podanych danych. Gdy taka sytuacja wystąpi, ale nie podano domyślnego wyrażenia, zwróconą wartością będzie nil.

Przykład użycia makra condp
1
2
3
4
5
6
7
8
9
10
(condp
  =           ; predykat
  (+ 2 2)     ; wyrażenie (da w wyniku 4)
  1 "jeden"   ; opcja 1: łańcuch znakowy
  2 "dwa"     ; opcja 2: łańcuch znakowy 
  3 "trzy"    ; opcja 3: łańcuch znakowy
  4 "cztery"  ; opcja 4: łańcuch znakowy
  "żadna")    ; wyrażenie opcji domyślnej

; => "cztery"

Organizowanie kodu

W idealnym, funkcyjnym programie każda konstrukcja jest wyrażeniem, które daje się obliczyć i zwraca jakąś wartość, nie generując efektów ubocznych. Jednak są klasy problemów, które rozwiązywane w sposób czysto funkcyjny powodowałyby powstawanie mało czytelnego lub mało wydajnego kodu. Dialekty języka Lisp są więc tolerancyjne pod względem sposobu reprezentowania algorytmu i pozwalają wyrażać rozwiązania problemów na różne sposoby.

Grupowanie S-wyrażeń, formuła specjalna do

Chyba najbardziej powszechną konstrukcją, która pozwala na chwilę zapomnieć o tym, że mamy do czynienia z wartościowanymi wyrażeniami i potraktować elementy programu jak instrukcje jest formuła specjalna do. Służy ona do grupowania wielu wyrażeń w taki sposób, że tylko wartość ostatniego zostanie zwrócona, chociaż ewentualne efekty uboczne (np. wyświetlanie tekstu na ekranie) będą mogły powstawać we wszystkich.

Użycie:

  • (do & wyrażenie…).

Formuła specjalna do przyjmuje zero lub więcej S-wyrażeń, które będą obliczone w kolejności ich podawania. Wartością zwracaną będzie rezultat obliczenia wartości stałej ostatniego z nich.

Jeżeli nie podamy żadnego S-wyrażenia, wartością zwracaną będzie nil.

Przykłady użycia formuły specjalnej do
1
2
3
4
5
6
7
8
9
10
11
12
13
(do)
; => nil

(do 123 456)
; => 456

(do
  (println "test")
  123
  (+ 1 1)
  (+ 2 2))
; >> test
; => 4

Przyłączanie argumentu, makro doto

Makro doto pozwala przyłączać podaną wartość jako pierwszy argument wywołania podanych wyrażeń. Najczęściej bywa używane, aby w przejrzysty sposób wyrazić wywołania metod Javy lub wtedy, gdy chcemy, żeby wartością zwracaną była wartość pierwszego argumentu wywołania jakiejś funkcji, makra lub formuły specjalnej.

  • (doto wartość & wyrażenie…).

Makro przyjmuje jeden obowiązkowy argument, którym powinna być dowolna wartość. Każdy kolejny argument będzie potraktowany jak S-wyrażenie, które przed wartościowaniem zostanie przekształcone w taki sposób, że jego pierwszym argumentem wywołania będzie właśnie ta wartość.

Wartością zwracaną przez makro doto jest wartość podana jako pierwszy argument.

Przykłady użycia makra doto
1
2
3
4
5
6
7
8
9
10
11
12
13
14
(doto 2
  (println "za pierwszym razem")
  (println "za drugim razem"))

; >> 2 za pierwszym razem
; >> 2 za drugim razem
; => 2

(doto (java.util.ArrayList.)
  (.add 1)
  (.add 2)
  (.add 3))

; => [1 2 3]

Przewlekanie S-wyrażeń, makro ->

[TBW]

Użycie:

  • (-> wyrażenie & wyrażenie…).

Przewlekanie S-wyrażeń od końca, makro -->

[TBW]

Użycie:

  • (--> wyrażenie & wyrażenie…).

Przewlekanie wartościowych S-wyrażeń, makro some->

[TBW]

Użycie:

  • (some-> wyrażenie & wyrażenie…).

Przewlekanie warunkowe, makro cond->

[TBW]

Użycie:

  • (cond-> wyrażenie & opcje…).

Przewlekanie warunkowe od końca, makro cond-->

[TBW]

Użycie:

  • (cond--> wyrażenie & opcje…).

Powiązywanie z nazwą, makro as->

[TBW]

Użycie:

  • (as-> wyrażenie symboliczna-nazwa & wyrażenie…).

Ten materiał jest częścią większej całości, którą można zobaczyć odwiedzając poniższy odnośnik:

Komentarze