Poczytaj mi Clojure, cz. 7: Funkcje i domknięcia

Podstawowym budulcem funkcyjnie zorientowanych programów komputerowych są funkcje. W tym odcinku poznamy sposoby tworzenia i używania funkcji, a także dowiemy się czym są domknięcia.

Funkcje i domknięcia

Funkcja (ang. function) w programowaniu to podprogram, który przeprowadza pewną operację obliczeniową na danych wejściowych zwanych argumentami (ang. arguments) i emituje jakąś zwracaną wartość (ang. return value).

Uruchamianie podprogramu funkcji nazywamy jej wywoływaniem (ang. calling).

Funkcje są przydatne, ponieważ:

  • pozwalają na łączenie wielu wyrażeń w jeden logiczny zbiór, a następnie wartościowanie tego zbioru dla różnych warunków wejściowych;
  • umożliwiają dzielenie złożonych problemów na mniejsze części;
  • umożliwiają selektywne testowanie realizowanych przez program czynności;
  • pomagają unikać powtarzania się, czyli wielokrotnego wprowadzania tych samych lub podobnych konstrukcji, aby realizować takie same obliczenia (oddzielanie danych od operacji);
  • oszczędzają pamięć – powstające w funkcjach rezultaty działań pośrednich mogą być bezpiecznie usunięte z pamięci po wyjściu z nich.

W językach funkcyjnych lub czerpiących z paradygmatu funkcyjnego funkcje są podstawowym elementem programów. Możemy dzięki nim nie tylko dzielić problemy na części, ale również abstrahować je. Polega to na przekształcaniu funkcji przez inne funkcje (przyjmowaniu ich jako argumenty, wywoływaniu i zwracaniu jako wartości).

Funkcja może być czysta (ang. pure), co oznacza, że dla takich samych argumentów o ustalonych wartościach za każdym razem zwróci taki sam wynik i nie zmieni stanu otoczenia (innych obiektów w pamięci). Jej interfejsem do kontaktu z innymi częściami programu będą zawsze: argumenty (wejście) i zwracana wartość (wyjście).

Przykład funkcji, która przyjmuje dwa argumenty i zwraca sumę ich wartości
1
(fn [a b] (+ a b))

W Clojure można stosować zarówno funkcje czyste, jak również takie, których wyniki zależą od otoczenia bądź wpływają na nie, np. wyświetlają coś na ekranie, zapisują bądź odczytują dane z plików czy modyfikują powiązania globalnych stanów. Mówimy wtedy, że poza realizowaniem właściwych dla niej obliczeń funkcja ma tzw. efekty uboczne (ang. side effects). W związku z tym Clojure nie może być, w przeciwieństwie do np. Haskella, uznany za język czysto funkcyjny.

Funkcje w Clojure, podobnie jak w innych Lispach, są jednostkami pierwszej kategorii (ang. first-class citizens). Termin ten oznacza, że obiekty reprezentujące funkcje traktowane są w taki sam sposób, jak instancje innych, powszechnie wykorzystywanych typów danych. W związku z tym możemy:

  • przechowywać funkcje w strukturach danych (np. zmiennych globalnych);
  • przekazywać funkcje jako argumenty do makr, form specjalnych lub innych funkcji;
  • zwracać funkcje jako rezultaty wykonania operacji (wartości wyrażeń);
  • dynamicznie tworzyć funkcje podczas pracy programu;
  • traktować funkcje jak wartości, których tożsamość nie zależy od nazwy.

Używanie funkcji

Aby korzystać z funkcji, należy ją wcześniej zdefiniować, to znaczy skorzystać z odpowiedniego wyrażenia symbolicznego, które będzie zapisem właściwej formuły tworzącej funkcję. W wyrażeniu tym zawarte będzie ciało funkcji (ang. function body) składające się z wyrażeń przeliczanych podczas wywoływania funkcji, poprzedzone wektorem parametrycznym, który określi przyjmowane przez funkcję argumenty i zasady powiązywania ich wartości z widocznymi w jej ciele parametrami. Wartością zwracaną wywoływanej funkcji będzie wartość ostatnio obliczanego wyrażenia z jej ciała.

Korzystanie z funkcji polega na jej wywoływaniu, czyli na odwołaniu się do jej obiektu w celu zrealizowania umieszczonego tam podprogramu (obliczeniu wartości wyrażeń składających się na jej ciało) z przekazaniem przez wartość podanych argumentów.

Wywoływanie będzie zwykle odbywało się z użyciem symbolicznego wyrażenia listowego, które zostanie potraktowane jak formuła funkcyjna. Pierwszym elementem takiej listy jest symbol powiązany z obiektem funkcji lub jej obiekt wyrażony w inny sposób. Kolejnymi elementami listy są przekazywane argumenty, których wartości zostaną obliczone zanim podprogram funkcji rozpocznie pracę.

Definiowanie funkcji

Funkcje w Clojure możemy tworzyć z użyciem kilku form specjalnych i makr.

Funkcje anonimowe, fn

Dzięki formule specjalnej fn możemy tworzyć funkcje anonimowe (ang. anonymous functions). Cechuje je to, że nie są powiązane z żadnym symbolicznym identyfikatorem i w związku z tym nazywa się je także funkcjami nienazwanymi (ang. unnamed functions). Definicje funkcji anonimowych określamy z kolei mianem lambda-wyrażeń (ang. lambda expressions).

Jako pierwszy, nieobowiązkowy argument formuła fn przyjmuje wyrażoną symbolem nazwę, która będzie dostępna w jej ciele i pozwoli funkcji odwoływać się do samej siebie, gdy zajdzie taka konieczność. Nazwa ta nie będzie widoczna w szerszym niż ciało funkcji kontekście leksykalnym. Korzystając tylko z formuły fn, nie stworzymy więc funkcji, którą można wywołać w innym miejscu programu, posługując się symbolicznym identyfikatorem.

Kolejnym, obowiązkowym argumentem jest wektor parametryczny, który tworzy tzw. wektorową formułę powiązaniową. Z jego pomocą możemy deklarować listę przyjmowanych przez funkcję argumentów. Wektor ten może być pusty, lecz należy go podać.

W najprostszej postaci wektor parametryczny może składać się z niezacytowanych symboli. Każdy z nich podczas wywoływania funkcji będzie leksykalnie powiązany z umieszczoną na odpowiadającej mu pozycji wartością przekazanego podczas wywołania argumentu. Powiązanie będzie widoczne w ciele funkcji, tzn. w wyrażeniach przekazanych jako ostatnie argumenty formuły fn.

(Dokładniejsze informacje dotyczące argumentów funkcji i konstruowania wektorów z parametrami znajdują się w sekcji “Argumenty funkcji”).

Po parametrach wyrażonych wektorem możemy umieścić kolejny argument opcjonalny – mapowe S-wyrażenie, określające warunki, które muszą być łącznie spełnione, aby wywołanie funkcji uznać za poprawne i nie przerywać działania programu. Mechanizm ten to tzw. asercja.

Wszystkie następne argumenty to wyrażenia stanowiące ciało funkcji. Wartość ostatnio obliczonego wyrażenia będzie wartością zwracaną przez funkcję.

Opcjonalnie możemy podać wiele ciał funkcji. Każde z nich powinno być wtedy zapisane jako listowe S-wyrażenie, którego pierwszym elementem będzie wektor parametryczny. W takim przypadku mamy do czynienia z tzw. funkcją wieloczłonową (zwaną też wieloargumentowościową). Podczas jej wywołania wartościowane będzie to ciało, do którego wektora parametrycznego pasuje przekazany podczas wywołania zestaw argumentów.

Formuła fn zwraca obiekt typu funkcyjnego (ang. function type object).

Użycie:

  • (fn nazwa?  parametry asercje? & wyrażenie…),
  • (fn nazwa? (parametry asercje? & wyrażenie…)…).

gdzie poszczególne etykiety mają następujące znaczenia:

  •     nazwa – wewnętrzna nazwa funkcji (Symbol),
  • parametry – wektor parametryczny (wektorowe S-wyrażenie),
  •   asercje – warunki wykonania (mapowe S-wyrażenie),
  • wyrażenie – ciało funkcji składające się z wyrażeń.
Przykład użycia formy specjalnej fn
1
2
3
4
5
6
(fn                  ; Definicja funkcji anonimowej:
  [rok]              ;   · argumenty funkcji,
  (or                ;   · ciało funkcji:
   (zero? (mod rok 400))
   (and (zero? (mod rok 4))
        (not (zero? (mod rok 100))))))

Zauważmy, że w powyższym przykładzie formuła fn zwraca funkcyjny obiekt, jednak nie możemy go później wywołać, ponieważ nie stworzyliśmy żadnej powiązanej z nim tożsamości, która byłaby identyfikowana jakąś nazwą. Wiemy jednak, że łatwo ten problem rozwiązać, jeśli uważnie śledziliśmy rozdział dotyczący zmiennych globalnych – jesteśmy w stanie stworzyć obiekt typu Var identyfikowany symbolem i powiązać z nim utworzoną funkcję. Możemy również skorzystać z konstrukcji let, jeżeli chcemy używać funkcji w ograniczonym leksykalnie kontekście.

Przykład powiązania zdefiniowanej funkcji ze zmienną globalną
1
2
3
4
5
6
7
8
(def                 ; nowa zmienna globalna (obiekt typu Var)
  rok-przestępny?    ;   · symbol nazywający zmienną;
  (fn                ;   · wartość powiązana ze zmienną, czyli funkcja:
    [rok]            ;     · parametry funkcji (przyjmowane argumenty);
    (or              ;     · ciało funkcji:
     (zero? (mod rok 400))
     (and (zero? (mod rok 4))
          (not (zero? (mod rok 100)))))))

Gdy teraz w programie umieścimy listowe S-wyrażenie i jako jego pierwszy element podamy symbol identyfikujący funkcję, to zostanie ona wywołana:

Przykład wywołania globalnie nazwanej funkcji
1
2
(rok-przestępny? 2000)
; => true

Co się stało? Przeszukana została mapa przestrzeni nazw, w której do symbolu rok-przestępny? przypisaliśmy obiekt typu Var. Ten z kolei powiązany jest z obiektem stworzonej przez nas funkcji (przechowuje do niego odniesienie). Obiekt Var internalizowany w przestrzeni nazw to inaczej zmienna globalna.

Pierwszy element listowego S-wyrażenia w postaci symbolu (rok-przestępny?) sprawia, że mamy do czynienia z formułą funkcyjną, czyli taką konstrukcją, której wartość może być obliczona przez wywołanie podprogramu funkcji. Funkcja ta powinna być powiązana z symbolem umieszczonym jako pierwszy element (globalnie, leksykalnie lub dynamicznie).

Sam symbol powinien być niezacytowany, czyli wyrażać formułę symbolową, która zostanie na początku (podczas wspomnianego przeszukania przestrzeni nazw i dereferencji zmiennej globalnej) zamieniona w funkcyjny obiekt. Obliczanie odbywa się w ten sposób, że kod funkcji jest wykonywany, a zwracana wartość (też będąca formułą) podstawiana w miejsce formuły funkcyjnej w celu dalszego przeliczania.

Gdyby zamiast listy rozpoczynającej się symboliczną nazwą funkcji pojawiła się po prostu jej zdefiniowana wcześniej nazwa, to rezultatem przeliczenia formuły symbolowej byłby sam obiekt tej funkcji:

Przykład reprezentacji obiektu funkcyjnego prezentowanej w REPL
1
2
rok-przestępny?
; #=> #<user$nasza_funkcja [email protected]>

Lambda-wyrażenia, #()

Makro czytnika #() pozwala zwięźle i czytelnie zapisywać funkcje anonimowe, których ciała nie składają się ze zbyt wielu wyrażeń. Zawarte w nim S-wyrażenie powinno być formułą funkcyjną, czyli jego pierwszym elementem musi być niezacytowany symbol, który odnosi się do jakiegoś podprogramu (funkcji, makra bądź formuły specjalnej), a kolejne argumentami przekazywanymi do tego podprogramu.

W wyrażeniu można korzystać z symbolu %, w miejsce którego zostaną podstawione argumenty wywołania lub z symboli %1, %2, %3 itd. W miejsca tych ostatnich zostaną podstawione argumenty o konkretnych pozycjach, określonych liczbą całkowitą znajdującą się za znakiem procenta. Możliwe jest również podanie symbolu %&, który zostanie zastąpiony wektorem, którego elementami będą wszystkie przekazane argumenty.

Wartością zwracaną przez makro jest obiekt typu funkcyjnego.

Makro #() jest często wykorzystywane tam, gdzie trzeba naprędce stworzyć funkcję anonimową celem przekazania jej do funkcji wyższego rzędu, np. w funkcjach filtrujących czy warunkujących, które wymagają podania zewnętrznego operatora.

Użycie:

  • #(operator & operand…).
Przykład użycia makra #()
1
2
3
4
5
(every? #(= \e %) "To jest test.")  ; czy każda litera to e?
; => false                          ; nie

(every? #(= \e %) "eeeee")          ; czy każda litera to e?
; => true                           ; tak

Funkcje nazwane, defn

Aby programiści nie musieli samodzielnie tworzyć powiązań symboli z tworzonymi funkcjami (za pośrednictwem zmiennych globalnych), w Clojure istnieje makro defn, które uczytelnia i przyspiesza proces definiowania funkcji. Działa ono podobnie do omówionego wyżej fn, jednak stwarza funkcję nazwaną (ang. named function), do której można odwoływać się z użyciem symbolicznego identyfikatora. Odwołanie to przechowywane jest w zmiennej globalnej internalizowanej w przestrzeni nazw.

Pierwszym argumentem wywołania defn powinna być nazwa funkcji wyrażona niezacytowanym symbolem. Następnym, opcjonalnym argumentem może być łańcuch dokumentujący, a kolejnym (również nieobowiązkowym) mapa metadanych, które zostaną ustawione dla zmiennej globalnej odwołującej się do obiektu funkcji. Nic nie stoi też na przeszkodzie, aby metadane zostały ustawione w symbolu określającym nazwę funkcji, zamiast w osobnej mapie – zostaną wtedy skopiowane do obiektu typu Var odnoszącego się do funkcji.

Po nazwie i opcjonalnym łańcuchu dokumentującym i/lub metadanych należy podać wektor parametryczny, który określa przyjmowane argumenty. Po parametrach można też umieścić opcjonalne warunki wykonania (tzw. asercje) w postaci mapy.

Kolejne argumenty to wyrażenia składające się na ciało funkcji. Wartość ostatnio wykonywanego stanie się wartością zwracaną.

Podobnie jak w przypadku formuły specjalnej fn możemy podać wiele ciał funkcji. Każde z nich powinno być w takim przypadku zapisane jako listowe S-wyrażenie, którego pierwszym elementem jest wektor parametryczny. Będziemy wtedy mieć do czynienia z tzw. funkcją wieloczłonową (zwaną też wieloargumentowościową). Podczas jej wywołania wartościowane będzie to ciało, do którego wektora parametrycznego pasuje przekazany podczas wywołania zestaw argumentów.

Makro defn zwraca obiekt typu funkcyjnego, a jego efektem ubocznym jest stworzenie identyfikowanej symbolem zmiennej globalnej w bieżącej przestrzeni nazw, która będzie powiązana z tym obiektem.

Użycie:

  • (defn nazwa dok? metadane?  parametry asercje? & wyrażenie…),
  • (defn nazwa dok? metadane? (parametry asercje? & wyrażenie…)… metadane?).

gdzie poszczególne etykiety mają następujące znaczenia:

Przykład użycia makra defn
1
2
3
4
5
6
7
(defn rok-przestępny?              ; Tworzymy globalną funkcję
  [rok]                            ; przyjmującą 1 argument (wyrażony parametrem rok).
  (or                              ; Ciało funkcji: czy prawdą jest, że albo:
   (zero? (mod rok 400))           ;  · rok dzieli się bez reszty przez 400;
   (and                            ;  · rok jednocześnie:
    (zero? (mod rok 4))            ;    · dzieli się bez reszty przez 4;
    (not (zero? (mod rok 100)))))) ;    · i dzieli się z resztą przez 100.

Wyjaśnijmy co dzieje się w powyższym przykładzie:

W pierwszej linii mamy wywołanie makra defn. W Clojure służy ono do tego, aby do przestrzeni nazw wpisać symbol i skojarzyć go ze zmienną globalną, która odwołuje się do obiektu funkcji. W tym przypadku symbolem będzie rok-przestępny?.

Kolejna linia (i jednocześnie trzeci element podanej listy) to wektor parametryczny, który pozwala zadeklarować, jakie argumenty funkcja może przyjmować. Symbole umieszczone w wektorze nazywamy parametrami i zostaną one powiązane z wartościami odpowiadających im argumentów, które zostaną przekazane w momencie wywołania funkcji. W podanym wyżej przykładzie symbol rok zostanie powiązany z wartością pierwszego (i jedynego) argumentu.

(Dokładniejsze informacje dotyczące argumentów funkcji i konstruowania wektorów z parametrami znajdują się w sekcji “Argumenty funkcji”).

Czwarty element listy (linia nr 3) to ciało funkcji. Może nim być dowolna lispowa formuła, ale w naszym przypadku jest to lista rozpoczynająca się symbolem or, a więc wywołanie makra o takiej właśnie nazwie.

Po wywołaniu każda funkcja zwraca wartość ostatnio obliczonego wyrażenia z jej ciała, a więc w tym przypadku wartość wywoływanego makra or. Makro to sprawdza, czy któryś podanych mu argumentów jest różny od false i różny od nil. Jeśli trafi na taki, zwraca go jako wartość, a jeśli nie, zwraca false.

Do wywołania or przekazujemy rezultat obliczenia wartości dwóch funkcji. Ostatnia z nich sprawdza, czy rok dzieli się bez reszty przez 400 (wtedy na pewno jest przestępny), a pierwsza jest makrem and, które zwraca ostatni z argumentów, jeśli żaden z nich po wyliczeniu nie jest wartością false ani nil.

Wewnątrz and mamy dwa warunki: sprawdzenie czy rok dzieli się bez reszty przez 4 (czy dzielenie modulo zwraca zero) i czy (jednocześnie) dzieli się z resztą przez 100.

Funkcję możemy wywołać, stosując znaną już notację listową:

1
(rok-przestępny? 2000)

Pytajnik na końcu jej nazwy jest kwestią konwencji i oznacza, że mamy do czynienia z predykatem, tzn. funkcją zwracającą wartość logiczną po przeprowadzeniu jakiegoś rachunku na danych wejściowych.

Zobacz także:

Funkcje prywatne, defn-

Makro defn- działa identycznie jak defn, lecz ustawia metadaną :private, dzięki czemu internalizowana w przestrzeni nazw zmienna globalna, która odnosi się do obiektu funkcji, będzie zmienną prywatną. Oznacza to, że widoczność symbolicznego identyfikatora nazywającego funkcję zostanie ograniczona do bieżącej przestrzeni nazw. Nie będzie więc możliwe zastosowanie symbolu z dookreśloną przestrzenią nazw, aby wywołać tak zdefiniowaną funkcję, jeśli znajduje się w innej niż bieżąca przestrzeni. W Clojure jest to sposób kapsułkowania (ang. encapsulation) kodu.

Argumenty wywołania w przypadku makra defn- są takie same, jak w defn.

Makro defn- zwraca obiekt typu funkcyjnego, a jego efektem ubocznym jest stworzenie identyfikowanej symbolem, prywatnej zmiennej globalnej w bieżącej przestrzeni nazw, która będzie powiązana z tym obiektem.

Użycie:

  • (defn- nazwa dok? metadane?  parametry asercje? & wyrażenie…),
  • (defn- nazwa dok? metadane? (parametry asercje? & wyrażenie…)… metadane?).
Przykład użycia makra defn-
1
2
3
4
5
6
7
8
9
(ns test)                           ; zmiana przestrzeni nazw na test
(defn- funkcja-prywatna [] "test")  ; definicja funkcji prywatnej
(funkcja-prywatna)                  ; wywołanie funkcji prywatnej
; => "test"

(ns user)                           ; zmiana przestrzeni nazw na user
(test/funkcja-prywatna)             ; próba wywołania funkcji prywatnej
; >> clojure.lang.Compiler$CompilerException: java.lang.IllegalStateException:
; >>   var: #'test/funkcja-prywatna is not public

Argumenty funkcji

Argumenty funkcji (ang. function arguments, skr. args) są sposobem przekazywania wartości początkowych do podprogramu realizującego algorytm funkcji. Każdy argument jest identyfikowany w ciele funkcji symbolem o ustalonej nazwie. Jeżeli w danym kontekście leksykalnym istnieją już powiązania symboli o nazwach takich, jak symbole identyfikujące argumenty, to ich widoczność zostanie przesłonięta.

Argumenty funkcji tym różnią się od innych konstrukcji, które pozwalają wytwarzać powiązania z wartościami, że w chwili definiowania nie są znane obiekty pamięciowe kojarzone z symbolami. Wartości pojawiają się dopiero, gdy kod funkcji poddawany jest wartościowaniu.

Parametry funkcji

Parametrami funkcji (ang. function parameters, skr. params) nazwiemy symbole podane w jej definicji, które zostaną powiązane z przekazywanymi argumentami, gdy funkcja zostanie wywołana.

Terminu argument używamy więc, gdy mówimy o wywoływaniu funkcji czy odwoływaniu się do jej obiektu, natomiast parametr, gdy opisujemy samą funkcję. Symboliczne identyfikatory w ciele funkcji będziemy więc nazywali parametrami, ale akceptowane przez funkcję dane wejściowe argumentami.

Wektor parametryczny

To, jakie argumenty funkcja będzie przyjmowała i jak będą się nazywały odpowiadające im parametry, możemy wyrazić, korzystając z wektora parametrycznego (ang. parameter vector). Należy go przekazać jako jeden z argumentów formuł specjalnych i makr służących do definiowania funkcji (np. fn i defn).

W podstawowej postaci wektor parametryczny jest wektorowym S-wyrażeniem, które składa się z symboli:

  • [symbol…].

Każdy z poszczególnych parametrów umieszczonych w takim wektorze musi być niezacytowanym symbolem, który będzie powiązany z wartością odpowiadającego mu, spodziewanego argumentu. Argumenty będą tu wyrażeniami inicjującymi parametry.

Poza podstawową formą wektora parametrycznego, dzięki której można określać tzw. argumenty pozycyjne funkcji, istnieją też jego warianty pozwalające na obsługę argumentów nazwanych czy nawet abstrakcyjnych powiązań strukturalnych (zarówno pozycyjnych, jak i asocjacyjnych).

Argumentowość funkcji

Argumentowość (ang. arity), zwana też arnością lub członowością, jest cechą, która oznacza liczbę i charakter argumentów przyjmowanych przez funkcję.

Możemy więc mówić na przykład o funkcjach unarnych (jednoargumentowych, zwanych też monadycznymi), binarnych (dwuargumentowych, zwanych też diadycznymi), ternarnych (trójargumentowych, zwanych też triadycznymi) itd.

Funkcje, które przyjmują wiele argumentów nazywamy ogólnie wieloargumentowymi (ang. multi-argument), poliadycznymi (ang. polyadic) lub multarnymi (ang. multary, multiary).

Obsługa argumentowości

Funkcje mogą przyjmować stałą lub zmienną liczbę argumentów. W przypadku zmiennej ich liczby istnieją dwa rodzaje funkcji, które pozwalają na obsługę takich przypadków:

  • funkcje wieloczłonowe – mające wiele ciał różniących się zestawami przyjmowanych argumentów (argumentowościami);
  • funkcje wariadyczne – przyjmujące dowolną liczbę argumentów wariadycznych, grupowanych w strukturze parametru wariadycznego widocznego w ich ciałach.

Funkcje wieloczłonowe

Funkcje wieloargumentowościowe (ang. multi-arity functions), zwane też funkcjami o wielu argumentowościach lub funkcjami wieloczłonowymi, to takie funkcje, które mają więcej niż jedną argumentowość, czyli można tworzyć a następnie wywoływać różne ich warianty, z których każdy przyjmuje inny zestaw argumentów.

W Clojure możemy tworzyć funkcje wieloczłonowe, umieszczając w ich definicjach wiele ciał wraz z wektorami parametrycznymi.

Technika ta nazywa się ogólnie przeciążaniem argumentowości funkcji, przeciążaniem listy argumentów lub przeciążaniem arności (ang. arity overloading). Różni się ona od często występującego w innych językach programowania mechanizmu przeciążania funkcji (ang. function overloading) tym, że mamy do czynienia z pojedynczym obiektem funkcyjnym, a nie z osobnymi funkcjami z różną liczbą, rodzajem bądź kolejnością obsługiwanych argumentów.

Każda formuła ciała funkcji wieloczłonowej powinna być reprezentowana listowym S-wyrażeniem, którego pierwszym elementem jest wyrażenie powiązaniowe, a kolejnymi elementami wyrażenia listowe realizujące algorytm funkcji – będą one przeliczane, gdy wywołany zostanie pasujący do argumentowości wariant.

Użycie:

  • (fn   nazwa? (parametry & wyrażenie…)+),
  • (defn nazwa dok? metadane? (parametry asercje? & wyrażenie…)+ metadane?).
Przykład definiowania funkcji wieloargumentowościowej
1
2
3
4
5
6
7
8
9
10
11
12
13
(defn funkcja
  ([a] 1)          ; pierwsze ciało
  ([a b] 2)        ; drugie ciało
  ([a b c] 3))     ; trzecie ciało

(funkcja 0)        ; będzie obliczone pierwsze ciało funkcji (1 argument)
; => 1

(funkcja 0 0)      ; będzie obliczone drugie ciało funkcji (2 argumenty)
; => 2

(funkcja 0 0 0)    ; będzie obliczone trzecie ciało funkcji (3 argumenty)
; => 3

Funkcje wariadyczne

Funkcje o zmiennej argumentowości (ang. variable arity functions), zwane też funkcjami zmiennoargumentowościowymi, funkcjami wariadycznymi (ang. variadic functions) lub funkcjami poliadycznie zróżnicowanymi (ang. variably polyadic functions), są funkcjami, które mogą przyjmować dowolną liczbę argumentów.

W Clojure funkcje tego rodzaju definiowane są z użyciem dodatkowego parametru (tzw. parametru wariadycznego) umieszczonego na ostatniej pozycji wektora parametrycznego. Podczas wywoływania funkcji powiązana z nim struktura będzie zawierała wszystkie nadmiarowe argumenty, które przekazano. Dla przechowania 17 lub mniejszej liczby elementów użyta będzie uporządkowana tablica (obiekt typu clojure.lang.ArraySeq), a dla liczby większej łańcuch komórek Cons (typu clojure.lang.Cons).

Dla oznaczenia wielu argumentów zgrupowanych w strukturze używa się symbolu ampersandu (&) umieszczonego przed symboliczną nazwą parametru wariadycznego.

Użycie:

  • & symbol.
Przykład definiowania funkcji zmiennoargumentowościowej
1
2
3
4
5
6
(defn funkcja [a b c & reszta]
  reszta)

(funkcja 0 0 0)      ; => nil
(funkcja 0 0 0 1)    ; => (1)
(funkcja 0 0 0 1 2)  ; => (1 2)

Funkcja tożsamościowa, identity

Funkcja tożsamościowa (ang. identity function), zwana także funkcją identycznościową, to taka funkcja jednoargumentowa, której zwracaną wartością jest zawsze wartość przekazana jako jej argument.

W Clojure funkcją tożsamościową jest wbudowana funkcja identity. Przyjmuje ona jeden argument i zwraca jego wartość.

Funkcja identity przydaje się często tam, gdzie musimy przekazać operator do funkcjonału (np. funkcji filtrującej czy odwzorowującej sekwencję), lecz nie potrzebujemy, aby wpływał on w jakikolwiek sposób na przetwarzane elementy.

Użycie:

  • (identity wartość).
Przykłady użycia funkcji identity
1
2
3
4
5
(identity 1)    ; => 1
(identity nil)  ; => nil

(map identity [1 2 3])
; => (1 2 3)

Liczba przyjmowanych argumentów

W języku Clojure lista argumentów funkcji nazwanych jest przechowywana w metadanych obiektu typu Var zawierającym odniesienie do jej obiektu. Kluczem tej konkretnej metadanej jest :argslist.

Przykład uzyskiwania listy argumentów zdefiniowanej funkcji
1
2
3
(defn funkcja [a b c & reszta] reszta)
(:arglists (meta #'funkcja))
; => ([a b c & reszta])

Niestety powyższa metoda nie zadziała dla funkcji anonimowych. Aby uzyskać listę argumentów dla obiektu funkcji, należy posłużyć się metodami Javy.

Liczba przekazywanych argumentów

Liczbę argumentów rzeczywiście przekazanych do funkcji możemy zbadać w jej ciele, sumując stałą liczbę wymaganych argumentów pozycyjnych z liczbą argumentów w parametrze wariadycznym (jeśli istnieje).

Przykład wyliczania liczby argumentów
1
2
3
4
5
(defn funkcja [a b c & reszta]
  (+ 3 (count reszta)))

(funkcja 1 2 3 4 5 6 7)
; => 7

Rodzaje argumentów

Argumenty można klasyfikować, przyjmując różne kryteria. Ze względu na konieczność występowania możemy wyróżnić ich następujące rodzaje:

  • obowiązkowe (ang. obligatory),
  • opcjonalne (ang. optional):
    • o określonych wartościach domyślnych;
    • bez określonych wartości domyślnych.

Z kolei ze względu na sposób uporządkowania następujące:

  • pozycyjne (ang. positional),
  • nazwane (ang. named).

Argumenty pozycyjne

Argumenty pozycyjne (ang. positional arguments) to takie argumenty funkcji, których znaczenie bazuje na kolejności ich przekazywania. W praktyce oznacza to, że powiązania przekazywanych wartości z symbolami wektora parametrycznego zależą od uporządkowania elementów.

Z argumentami pozycyjnymi mamy do czynienia w większości wbudowanych funkcji, makr i formuł specjalnych języka Clojure.

Przykład funkcji przyjmującej argumenty pozycyjne
1
2
3
4
(defn funkcja [a b c] (list a b c))

(funkcja 1 2 3)  ; => (1 2 3)
(funkcja 3 2 1)  ; => (3 2 1)

Uwaga: W Clojure możemy definiować funkcje, które przyjmują maksymalnie 25 argumentów pozycyjnych. Jeżeli występuje konieczność obsługi większej ich liczby, należy skorzystać z parametru wariadycznego.

Argumenty nazwane

Argumenty nazwane (ang. named arguments) to takie argumenty funkcji, których kolejność występowania nie ma znaczenia, ponieważ występują w formie asocjacji wyrażanych parami klucz–wartość, gdzie kluczami są ich nazwy. Oznacza to, że przekazując takie argumenty w sposób jawny, wyrażamy też ich identyfikatory, zamiast polegać na kolejności występowania.

Przykład przekazywania argumentów nazwanych do funkcji
1
2
(funkcja :a 1  :b 2  :c 3)
(funkcja :a 1, :b 2, :c 3)

Nazwy argumentów wyrażone mogą być dowolnymi wartościami, np. symbolami czy łańcuchami znakowymi, chociaż na zasadzie konwencji stosuje się słowa kluczowe. W takim przypadku argumenty nazwane bywają określane mianem argumentów bazujących na słowach kluczowych (ang. keyword arguments).

W Clojure argumenty nazwane obsługiwane są w funkcjach wariadycznych, a dokładniej przez automatyczną dekompozycję struktury parametru wariadycznego, który grupuje wszystkie lub nadmiarowe argumenty.

W Clojure możemy wyrażać wprost, jakich argumentów nazwanych się spodziewamy (ułatwi nam to omówiona niżej dyrektywa :keys), albo skorzystać z dyrektywy :as, aby uzyskać dostęp do mapy, którą sami przetworzymy – przydaje się to na przykład w obsłudze opcji, które nie są znane w momencie definiowania funkcji lub w przetwarzaniu argumentów, których część jest używana przez inną funkcję. Argumenty nazwane jesteśmy bowiem w stanie w całości lub części przekazać do wywołania innej funkcji – wymaga to skorzystania z kilku konstrukcji, które będą odpowiednikiem apply dla argumentów wyrażonych asocjacyjnie.

Powiązania parametrów określających argumenty nazwane z wartościami mogą być wyrażane asocjacyjnymi strukturami danych, np. mapami. Parametry takie nazywa się parametrami nazwanymi (ang. named parameters).

Parametry nazwane w definicji funkcji grupuje się w umieszczonym po symbolu ampersandu parametrze wariadycznym z użyciem tzw. mapy powiązaniowej (ang. binding map), która powinna wyrażać mapową formułę powiązaniową.

Mapa powiązaniowa musi być zawarta w wektorze parametrycznym – dzięki temu można deklarować zarówno argumenty nazwane, jak i pozycyjne. Klucze mapy powinny być niezacytowanymi symbolami, które powiązane zostaną z wartościami przekazywanych argumentów, a przypisane do nich wartości powinny być nazwami tych (spodziewanych) argumentów (zwyczajowo wyrażone słowami kluczowymi).

Użycie:

  • [ & {symbol nazwa-argumentu …}…].
Przykład definiowania i wywołania funkcji przyjmującej argumenty nazwane
1
2
3
4
5
(defn funkcja [ & {a :a, b :b, c :c}]
  (list a b c))

(funkcja :a 1 :b 2 :c 3)
; => (1 2 3)

W powyższym przykładzie w wektorze parametrycznym umieszczona została mapa parametryczna, której kluczami są symbole, a wartościami słowa kluczowe określające nazwy oczekiwanych argumentów. Symbol a zostanie więc powiązany z wartością, którą przekazano do funkcji w parze z kluczem :a, symbol b z wartością nazwaną :b itd.

Dyrektywa :keys

W ostatnim przykładzie możemy zauważyć pewną redundancję – podane nazwy symboli i nazwy argumentów w mapie parametrycznej są takie same (chociaż nic nie stoi na przeszkodzie, aby były różne). W takich przypadkach warto skorzystać z dyrektywy :keys. Jeśli zostanie ona podana jako klucz mapy parametrycznej, to przypisany do niej wektor będzie potraktowany tak, jakby zawierał nazwy symboli i odpowiadających im poszukiwanych kluczy, z których wartościami należy te symbole powiązać. Słowa kluczowe w wektorze mogą być zapisane wprost lub wyrażone niezacytowanymi symbolami.

Użycie:

  • [ & {:keys [nazwa-argumentu…] …}].
Przykład użycia dyrektywy :keys
1
2
3
4
5
(defn funkcja [ & {:keys [a b c]}]
  (list a b c))

(funkcja :a 1 :b 2 :c 3)
; => (1 2 3)

Dyrektywy :strs:syms

Alternatywnymi (bądź uzupełniającymi) w stosunku do :keys dyrektywami są :strs (oznaczająca łańcuchy znakowe) i :syms (oznaczająca symbole). Gdy pojawią się w mapie, to przypisane do nich wektory będą grupowały nazwy argumentów wyrażane odpowiednio łańcuchami znakowymi lub symbolami, a nie słowami kluczowymi.

Użycie:

  • [ & {:strs [nazwa-argumentu…] …}],
  • [ & {:syms [nazwa-argumentu…] …}].
Przykład użycia dyrektyw :strs i :syms
1
2
3
4
5
6
7
8
9
10
11
(defn funkcja [ & {:strs [a b c]}]
  (list a b c))

(funkcja "a" 1, "b" 2, "c" 3)
; => (1 2 3)

(defn funkcja [ & {:syms [a b c]}]
  (list a b c))

(funkcja 'a 1 'b 2 'c 3)
; => (1 2 3)

Dyrektywa :as

Inną dyrektywą, która może okazać się pomocna, jest :as. Pozwala ona na bezpośredni dostęp do struktury zawierającej przekazywane argumenty nazwane. Przydaje się to na przykład podczas badania obecności argumentu nazwanego, gdy może on mieć wartość nil, a także podczas obsługi opcji przekazywanych do funkcji, których strukturami chcemy samodzielnie zarządzać, a nie korzystać z dostępu do każdej z nich z osobna.

Zauważmy, że wartość ta zostanie powiązana z symbolem parametru zarówno wtedy, gdy argumentu o danej nazwie nie przekazano podczas wywoływania funkcji, jak i wtedy, gdy argument znajduje się w mapie, ale po prostu ma wartość nil.

Użycie:

  • [ & {:as symbol …}].
Przykład użycia dyrektywy :keys
1
2
3
4
5
(defn funkcja [ & {:keys [:a :b :c] :as wszystkie}]
  wszystkie)

(funkcja :a 1 :b 2 :c 3)
; => {:a 1 :b 2 :c 3}

Dyrektywa :or

Dzięki dyrektywie :or możemy ustawiać wartości domyślne dla argumentów, których nie przekazano podczas wywoływania funkcji. Po słowie kluczowym należy podać parametryczną mapę inicjującą, której kluczami będą nazwy argumentów, a wartościami przypisane do nich domyślne wartości.

Użycie:

  • [ & {:or {nazwa-argumentu wartość-domyślna …}} …].
Przykład użycia dyrektywy :or
1
2
3
4
5
(defn funkcja [ & {:keys [:a :b :c] :or {:a 1, :b 2, :c 3}}]
  (list a b c))

(funkcja :a 1 :b 2)
; => (1 2 3)

Łączenie dyrektyw

Jak zdążyliśmy zauważyć, dyrektywy można łączyć. Na przykład możemy ustawiać domyślne wartości parametrów dla pominiętych argumentów, a jednocześnie uzyskiwać dostęp do przekazanej struktury.

Przykład łącznego użycia dyrektyw :keys, :or i :as
1
2
3
4
5
6
7
(defn funkcja [ & {:keys [:a :b :c]
                   :or   {:a 1, :b 2, :c 3}
                   :as   wszystkie}]
  (list wszystkie a b c))

(funkcja :a 1 :b 2)
; => ({:a 1 :b 2} 1 2 3)

Argumenty obowiązkowe

Argumenty obowiązkowe (ang. obligatory arguments) to takie argumenty, które muszą być do funkcji przekazane podczas jej wywoływania.

W przypadku argumentów pozycyjnych, wyrażonych jako symbole w wektorze parametrycznym, mamy do czynienia z domniemaniem, że wszystkie są obowiązkowe. Wynika to z konieczności zachowania porządku w tego typu strukturze. Gdyby jakiś (poza ostatnim) argument nie został przekazany, to nie można byłoby przed wywołaniem funkcji rozpoznać który, ponieważ w języku dynamicznie typizowanym, jakim jest Clojure, wyłącznie kolejność argumentów pozycyjnych komunikuje ich znaczenia.

Przykład funkcji przyjmującej pozycyjne argumenty obowiązkowe
1
2
3
4
5
6
7
(defn funkcja [a b c] (list a b c)) ; definicja funkcji

(funkcja 1 2 3)                     ; poprawna liczbą argumentów
; => (1 2 3)

(funkcja 1 2)                       ; błędna liczbą argumentów
; >> clojure.lang.ArityException: Wrong number of args (2) passed to: user/funkcja

W przypadku argumentów nazwanych należy wprowadzić dodatkowe testy w ciele funkcji lub posłużyć się asercją, aby uzależnić możliwość jej wywołania od obecności wymaganych kluczy w przekazanej mapie.

Przykład funkcji przyjmującej nazwane argumenty obowiązkowe
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
;; sprawdzanie, czy podano wszystkie argumenty obowiązkowe
(defn arg-obowiązkowe [wszystkie & klucze]
  (let [s-klucze (set klucze)
        liczba-kluczy (count s-klucze)
        obecne (select-keys wszystkie s-klucze)
        liczba-obecnych (count obecne)]
    (= liczba-obecnych liczba-kluczy)))

;; definicja funkcji
(defn funkcja [ & {:keys [:a :b :c] :as wszystkie}] ; mapa jako wszystkie
  {:pre [(arg-obowiązkowe wszystkie :a :b)]}        ; asercja
  (list a b c))                                     ; ciało funkcji

(funkcja :a 1 :b 2 :c 3)                            ; poprawna liczba argumentów
; => (1 2 3)

(funkcja :a 1 :c 3)                                 ; błędna liczba argumentów
; >> java.lang.AssertionError: Assert failed: (arg-obowiązkowe wszystkie :a :b)

Podobnie rzecz się ma w przypadku funkcji wariadycznych, w których ostatni parametr jest strukturą zawierającą wszystkie nadmiarowe argumenty pozycyjne. W najprostszym przypadku będziemy tam mieć do czynienia z wektorem, którego kolejne elementy są przekazanymi argumentami. Pojawia się tu znów kwestia ich rozpoznawania, ponieważ mamy do czynienia z wyrażaniem znaczenia przez porządek. Testować możemy więc wartości konkretnych argumentów lub podaną ich liczbę.

Przykład funkcji wariadycznej przyjmującej argumenty obowiązkowe, pozycyjne
1
2
3
4
5
6
7
8
9
10
11
;; definicja funkcji
(defn funkcja [& reszta]                        ; reszta jako wszystkie
  {:pre [(>= (count reszta) 3)]}                ; asercja
  (let [[a b c r] reszta]                       ; ciało funkcji i dekompozycja
    (list a b c)))

(funkcja 1 2 3)                                 ; poprawna liczba argumentów
; => (1 2 3)

(funkcja 1 2)                                   ; błędna liczba argumentów
; >> java.lang.AssertionError: Assert failed: (>= (count reszta) 3)

Argumenty opcjonalne

Argumenty opcjonalne (ang. optional arguments) to takie, które mogą być pominięte (nieprzekazywane) podczas wywoływania funkcji. W dokumentacji oznaczane są symbolem pytajnika umieszczonego za symbolem.

Argumenty opcjonalne mogą przyjmować wartości domyślne (ang. default values). W takim przypadku istnieje kilka sposobów ustawiania tych wartości w zależności od rodzaju argumentów:

  • Dla argumentów pozycyjnych:

    • przez użycie funkcji o wielu argumentowościach i stworzenie wariantu z zestawem przyjmowanych argumentów, w którym jeden lub więcej z nich pominięto, lecz podano wartości domyślne w umieszczonym w jej ciele wywołaniu wariantu przyjmującego wszystkie argumenty;
    • przez użycie funkcji o zmiennej argumentowości i ustawienie wartości w ciele funkcji, jeśli wykryto wartości nil lub mniejszą liczbę przekazanych argumentów.
  • Dla argumentów nazwanych:

    • przez użycie parametrów nazwanych i ustawienie wartości w ciele funkcji, jeśli wykryto wartości nil lub braki kluczy w mapie parametrycznej;
    • przez użycie parametrów nazwanych i ustawienie w mapie inicjującej wartości domyślnych określonych kluczem :or.
Przykłady stosowania argumentów opcjonalnych w funkcjach
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
;; Funkcja wieloczłonowa (wieloargumentowościowa)
(defn funkcja
  ([a b c] (list a b c))
  ([a b]   (funkcja a b 3)))

(funkcja 1 2 3)
; => (1 2 3)

(funkcja 1 2)
; => (1 2 3)

;; Funkcja wariadyczna (zmiennoargumentowościowa)
(defn funkcja [a b & reszta]
  (let [c (if (> (count reszta) 0) (first reszta) 3)]
    (list a b c)))

(funkcja 1 2 3)
; => (1 2 3)

(funkcja 1 2)
; => (1 2 3)

;; Funkcja z argumentami nazwanymi (przeszukiwanie mapy)
(defn funkcja [ & {:keys [:a :b :opt-c] :as wszystkie}]
  (let [c (if (contains? wszystkie :opt-c) opt-c 3)]
    (list a b c)))

(funkcja :a 1 :b 2 :c 3)
; => (1 2 3)

(funkcja :a 1 :b 2)
; => (1 2 3)

;; Funkcja z argumentami nazwanymi (parametryczna mapa inicjująca)
(defn funkcja [ & {:keys [:a :b :c]
                   :or   {:a 1, :b 2, :c 3}}]
  (list a b c))

(funkcja :a 1 :b 2 :c 3)
; => (1 2 3)

(funkcja :a 1 :b 2)
; => (1 2 3)

Dekompozycja parametrów

Podane wcześniej przykłady parametrów nazwanych korzystają z mechanizmu zwanego dekompozycją (ang. decomposition) lub destrukturyzacją (ang. destructuring). Polega ona na wytwarzaniu powiązań symboli z wartościami umieszczonymi w strukturach o wielu elementach (asocjacyjnych lub pozycyjnych) bez konieczności wywoływania dodatkowych funkcji. Zamiast używać operacji, takich jak first, nth czy rest, korzysta się po prostu ze specyficznej składni, która w miarę jasny sposób wyraża, z jakimi symbolami mają zostać powiązane kolejne (w przypadku struktur sekwencyjnych, np. wektorów) lub odpowiednio nazwane (w przypadku struktur asocjacyjnych, np. map) elementy.

Dekompozycji możemy używać również w ciele funkcji, nie polegając na wewnętrznych mechanizmach makr służących do definiowania funkcji, ale korzystając np. z konstrukcji let. Spójrzmy:

Przykład dekompozycji struktury parametru wariadycznego z użyciem let
1
2
3
4
5
6
(defn funkcja [ & parametr-wariadyczny ]
  (let [{:keys [a b c]} parametr-wariadyczny]
    (list a b c)))

(funkcja :a 1 :b 2)
; => (1 2 nil)

W powyższym przykładzie parametr wariadyczny będzie wektorem zawierającym argumenty nazwane przekazane do funkcji wyrażone naprzemiennie: elementy parzyste będą kluczami (nazwami argumentów), a nieparzyste ich wartościami:

1
[:a 1 :b 2]

Zapis {:keys [a b c]} w wektorze powiązaniowym konstrukcji let jest składnią specyficzną dla dekompozycji. Mówi on, że w strukturze danych, która jest podstawą do wytworzenia powiązań, spodziewamy się kluczy o nazwach a, bc, a ich wartościami powinny być przypisane do kluczy wartości z tej struktury. Gdyby była to mapa, będą to po prostu odpowiednie wartości, ale ponieważ jest to wektor, zostanie on najpierw tymczasowo przekształcony do postaci asocjacyjnej. W efekcie symbolowi a zostanie przypisana wartość 1 (klucz :a), zaś symbolowi b wartość 2 (klucz :b). Powiązania symboli z wartościami będą widoczne w wyrażeniu podanym jako drugi argument wywołania let, czyli w kontekście leksykalnym.

Poza destrukturyzacją parametrów nazwanych możemy w Clojure dokonywać też dekompozycji innych wartości przekazywanych jako argumenty funkcji, np. map czy wektorów, bez korzystania z let, ale polegając na mechanizmie destrukturyzacji parametrów.

Zobacz także:

Dekompozycja asocjacyjna

Dekompozycja asocjacyjna (ang. associative decomposition), zwana też destrukturyzacją asocjacyjną (ang. associative destructuring), pozwala wygodnie przetwarzać argumenty, które są strukturami asocjacyjnymi (np. mapy).

Spójrzmy na przykład destrukturyzacji argumentu funkcji, którego wartością jest struktura asocjacyjna:

Przykład dekompozycji mapy przekazanej jako argument funkcji
1
2
3
4
5
(defn funkcja [ {:keys [a b c]} ]
  (list a b c))

(funkcja {:a 1 :b 2})
; => (1 2 nil)

Zauważmy, że nie korzystamy z parametru wariadycznego, który grupuje przekazane argumenty w wektorze. W związku z tym wywołując funkcję musimy wprost wyrazić strukturę danych i jako argument przekazać mapę.

Podobnie jak we wcześniejszych przykładach, związanych z argumentami nazwanymi, możemy korzystać z następujących dyrektyw:

  • :keys [symbol…] – wyrażającą, że spodziewamy się wartości skojarzonych z kluczami (w postaci słów kluczowych) o podanych nazwach i chcemy je powiązać z symbolami o takich samych identyfikatorach;
  • :syms [symbol…] – wyrażającą, że spodziewamy się wartości skojarzonych z kluczami wyrażonymi jako symbole i chcemy je powiązać z symbolami o takich samych nazwach;
  • :strs [symbol…] – wyrażającą, że spodziewamy się wartości skojarzonych z kluczami wyrażonymi jako łańcuchy tekstowe i chcemy je powiązać z symbolami o takich samych nazwach;
  • :as symbol – wyrażającą, że chcemy, aby przekazywana wartość argumentu została dodatkowo powiązana z podanym symbolem bez dekompozycji;
  • :or {klucz wartość …} – wyrażającą wartości domyślne dla kluczy, których nie odnaleziono w podanej strukturze.

Zobacz także:

Dekompozycja pozycyjna

Dekompozycja pozycyjna (ang. positional decomposition), zwana też destrukturyzacją pozycyjną (ang. positional destructuring) może być przydatnym sposobem kontroli listy argumentów przyjmowanych przez funkcje.

Możemy na przykład dokonywać destrukturyzacji parametru wariadycznego i w ten sposób obsługiwać argumenty opcjonalne, bez konieczności dodatkowego przetwarzania wartości parametru w ciele funkcji.

Przykład dekompozycji parametru wariadycznego
1
2
3
4
5
6
7
;; a    – argument obowiązkowy
;; b, c – argumenty opcjonalne z dekompozycji parametru wariadycznego
(defn funkcja [a & [b c]]
  (list a b c))

(funkcja 1 2)
; => (1 2 nil)
Przykład dekompozycji wektora przekazanego jako argument pozycyjny
1
2
3
4
5
(defn funkcja [[a b c]]
  (list a b c))

(funkcja [1 2])
; => (1 2 nil)
Przykład dekompozycji wektora z ignorowaniem elementu
1
2
3
4
5
(defn funkcja [[a b _ d]]
  (list a b d))

(funkcja [1 2 3 4])
; => (1 2 4)
Przykład dekompozycji wektora z grupowaniem elementów
1
2
3
4
(defn funkcja [[a & reszta]] reszta)

(funkcja [1 2 3 4])
; => (2 3 4)

Podobnie jak we wcześniejszych przykładach, związanych z argumentami pozycyjnymi, możemy korzystać z następujących dyrektyw:

  • :as symbol – wyrażającą, że chcemy, aby przekazywana wartość argumentu została dodatkowo powiązana z podanym symbolem bez dekompozycji;
  • _ – wyrażającą elementy struktur, które chcemy zignorować i nie dokonywać ich powiązań z żadnymi symbolami;
  • & – wyrażającą elementy struktur, które chcemy zgrupować w jednym wektorze (przypomina to parametr wariadyczny).

Zobacz także:

Przekazywanie wartości argumentów

Zdarza się, że przekazane do funkcji wartości wszystkich lub wybranych argumentów musimy przekazać do wywołania innej funkcji. W zależności od tego, czy mamy do czynienia z argumentami nazwanymi czy pozycyjnymi, wariadycznymi czy o ustalonej liczbie, będziemy korzystali z różnych sposobów.

Przekazywanie argumentów pozycyjnych

Przekazywanie argumentów pozycyjnych jest proste. W wywołaniu drugiej funkcji umieszczamy po prostu symbole powiązane z wartościami parametrów:

Przykład przekazywania wartości argumentów pozycyjnych
1
2
3
4
5
(defn inna    [a b] (list a b))
(defn druga [a b c] (inna a b))

(druga 1 2 3)
; => (1 2)

Przekazywanie argumentów wariadycznych

Przekazywanie argumentów zgrupowanych w parametrze wariadycznym wymaga wyekstrahowania z jego struktury wszystkich lub wybranych elementów, a następnie przekazanie ich jako argumenty do wywołania funkcji.

Możemy wyróżnić różne przypadki, zależnie od liczby przekazywanych w ten sposób argumentów (wszystkie lub część), a także od tego, czy wykorzystywana jest destrukturyzacja:

  • przekazywanie argumentu wariadycznego w całości,
  • przekazywanie argumentu wariadycznego w części.

W obydwu posłużymy się funkcją apply, która sprawi, że kolejne elementy struktury danych reprezentującej parametr wariadyczny zostaną podstawione jako argumenty wywołania funkcji.

W wywołaniu innej funkcji z przekazaniem jej tylko części argumentów, poza użyciem apply dokonamy pewnej operacji na parametrze wariadycznym, np. skorzystamy z funkcji drop, aby pominąć pierwszy element (odpowiadający pierwszemu argumentowi).

Przykłady przekazywania wartości argumentów wariadycznych
1
2
3
4
5
6
7
8
9
10
11
(defn inna [& reszta] reszta)

;; przekazywanie wszystkich argumentów wariadycznych
(defn druga [a & reszta] (apply inna reszta))
(druga 1 2 3 4)
; => (2 3 4)

;; przekazywanie części argumentów wariadycznych
(defn druga [a & reszta] (apply inna (drop 1 reszta)))
(druga 1 2 3 4)
; => (3 4)

Przekazywanie z dekompozycji pozycyjnej

Przekazywanie argumentów wariadycznych poddawanych dekompozycji pozycyjnej może odbywać się również w całości lub w części. W tym pierwszym przypadku należy użyć dyrektywy :as w parametrze wariadycznym, która powoduje, że poza dekompozycją będziemy mieli dostęp również do struktury parametru wariadycznego, zawierającej wszystkie powiązane z nim argumenty. Podobnie jak w przykładach z przekazywaniem parametru wariadycznego, z którym de facto mamy tu do czynienia, korzystamy z funkcji apply, aby kolejne elementy struktury stały się wartościami argumentów przekazywanych do wywołania funkcji.

Drugi przypadek to przekazywanie wartości wybranych argumentów pochodzących z dekompozycji parametru wariadycznego. Jego obsługa w praktyce nie różni się od zwykłego przekazywania argumentów pozycyjnych z użyciem symboli reprezentujących odpowiadające im parametry.

Przykłady przekazywania wartości parametrów destrukturyzowanych pozycyjnie
1
2
3
4
5
6
7
8
9
10
11
;; przekazywanie argumentów wariadycznych
;; z destrukturyzowanego pozycyjnie parametru wariadycznego
(defn druga [a & [b c d :as reszta]] (apply inna reszta))
(druga 2 3 4)
; => (2 3 4)

;; przekazywanie wybranych argumentów wariadycznych
;; z destrukturyzowanego pozycyjnie parametru wariadycznego
(defn druga [a & [b c d]] (inna c d))
(druga 1 2 3 4)
; => (3 4)

Przekazywanie z dekompozycji asocjacyjnej

Przekazywanie argumentów pochodzących z dekompozycji asocjacyjnej jest nieco bardziej skomplikowane, niż ta sama operacja dla argumentów poddawanych destrukturyzacji pozycyjnej. Mamy w nim do czynienia z mapą, której klucze i wartości muszą zostać przekształcone najpierw do postaci sekwencyjnej klucz-1 wartość-1 klucz-2 wartość-2 …, a dopiero elementy tej struktury mogą być przekazane jako kolejne argumenty wywołania funkcji.

Aby uzyskać dostęp do parametru wariadycznego, w mapie powiązaniowej należy umieścić dyrektywę :as. Następnie uzyskaną strukturę należy przekształcić do wspomnianej postaci. W tym celu możemy użyć funkcji mapcat, której jako transformator podamy identity, ponieważ nie zależy nam na przekształcaniu kluczy i wartości, lecz ich grupowaniu w sekwencji.

Zauważmy, że opisywana tu metoda pozwala na przekazywanie wszystkich argumentów nazwanych, które w Clojure obsługuje się właśnie z użyciem parametru wariadycznego i asocjacyjnej dekompozycji.

Przykłady przekazywania wartości parametrów destrukturyzowanych asocjacyjnie
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(defn inna [& {:as reszta}] reszta)

;; przekazywanie wszystkich argumentów nazwanych
(defn druga [& {:keys [:a :b :c :d] :as reszta}]
  (apply inna (mapcat identity reszta)))
(druga :c 3 :d 4)
; => {:c 3 :d 4}

;; przekazywanie wszystkich argumentów nazwanych
(defn druga [& {:as reszta}]
  (apply inna (mapcat identity reszta)))
(druga :a 1 :c 3)
; => {:a 1 :c 3}

;; przekazywanie wybranych argumentów nazwanych
(defn druga [& {:keys [:a :b :c :d]}] (inna :c c :d d))
(druga :a 1 :b 2 :c 3 :d 4)
; => {:c 3 :d 4}

Asercje

Asercja (ang. assertion) to mechanizm przerywania wykonywania programu, gdy podany predykat (funkcja sprawdzająca wystąpienie pewnych warunków) nie zwraca logicznej prawdy.

Asercje znajdują zastosowanie w testowaniu programów komputerowych, w zabezpieczaniu ich fragmentów przed występowaniem błędów związanych z danymi wejściowymi lub wartościami podawanymi przez programistę, a także w tzw. programowaniu kontraktowym (ang. Design by Contract, skr. DbC), gdzie sprawdzanie poprawności jest fundamentalną częścią procesu tworzenia oprogramowania.

W Clojure (od wydania 1.1) podczas definiowania funkcji z użyciem formuły specjalnej fn lub makra defn możemy po wektorze parametrycznym umieścić tzw. mapę warunkową (ang. condition map) zawierającą zestawy warunków, które muszą być spełnione, aby funkcja została wywołana. Jeżeli którykolwiek z podanych predykatów zwróci wartość false lub nil, to wygenerowany będzie wyjątek.

Zestawy warunków powinny być wyrażone wektorami, podanymi jako wartości kluczy :pre lub :post mapy. Klucz :pre to warunki, które zostaną sprawdzone zanim ciało funkcji będzie poddane wartościowaniu, natomiast :post grupuje wyrażenia warunkowe obliczane po zakończeniu wykonywania kodu funkcji.

Użycie:

  • {:pre  [wyrażenie…]},
  • {:post [wyrażenie…]},
  • {:pre  [wyrażenie…] :post [wyrażenie…]}.

Uwaga: Jeśli po mapie podanej po wektorze parametrycznym nie pojawi się ciało funkcji, to będzie ona potraktowana jak wyrażenie reprezentujące ciało, a nie jak mapa warunkowa.

Wszystkie wyrażenia logiczne umieszczone w każdym z wektorów muszą być spełnione łącznie, aby uznać dany warunek za spełniony.

W obu wektorach możemy tworzyć wyrażenia, które będą odwoływały się do parametrów funkcji, czyli do wartości przekazanych jej argumentów. Poza tym w wektorze przypisanym do klucza :post jesteśmy w stanie skorzystać z symbolu %, w miejsce którego zostanie podstawiona wartość zwracana przez funkcję.

Innym sposobem określenia warunków wykonywania funkcji jest dołączenie mapy warunkowej do metadanej :argslist przypisanej do zmiennej globalnej, która zawiera odniesienie do obiektu funkcji.

Korzystanie z asercji można wyłączyć (globalnie lub w danym wątku) powiązując zmienną dynamiczną *assert* z wartością false.

Funkcje wyższego rzędu

Ponieważ w Clojure funkcje są typem pierwszoklasowym (jednostkami pierwszej kategorii), więc możliwe jest tworzenie tzw. funkcji wyższego rzędu (ang. higher-order functions), zwanych także formułami funkcjonalnymi (ang. functional forms), funkcjonałami (ang. functionals) lub funktorami (ang. functors).

Funkcje wyższego rzędu to takie funkcje, które przyjmują przynajmniej jedną funkcję jako argument lub zwracają funkcję jako wartość.

W matematyce ten typ funkcji bywa nazywany operatorami, a analogiczną konstrukcją w rachunku różniczkowym i całkowym jest np. pochodna – funkcja stworzona na podstawie innej funkcji.

Funkcje wyższego rzędu doskonale nadają się do filtrowania i redukowania kolekcji danych, szczególnie wyposażonych w sekwencyjny interfejs dostępu. Wiele wbudowanych w Clojure form przyjmuje jako argumenty operatory wyrażone obiektami funkcyjnymi.

Używanie funkcji wyższego rzędu

Tworzenie funkcji wyższego rzędu

Tworzenie funkcji wyższego rzędu nie różni się od definiowania innych funkcji. Po prostu jeden lub więcej przyjmowanych argumentów musi być obiektami typu funkcyjnego.

Przykład tworzenia funkcji wyższego rzędu
1
2
3
(defn funk [operator arg1 arg2]
  (+ (operator arg1 arg2)
     (operator arg1 arg2)))

Powyższy kod definiuje nazwaną funkcję funk, która jako pierwszy argument przyjmuje inną funkcję, a następnie wywołuje ją, przekazując dwa kolejne, otrzymane argumenty. Czynność przeprowadzana jest dwukrotnie, a wyniki są sumowane.

Gdybyśmy chcieli jej użyć, możemy ustawić operator (+) i operandy:

1
2
(funk + 2 2)
; => 8

Można też definiować funkcje, które zwracają inne funkcje:

Przykład funkcji zwracającej funkcję
1
2
3
4
5
6
7
8
9
10
11
12
13
(defn lubię-kolor [kolor]
  (fn [] (str "Lubię kolor " kolor ".")))

;; nadawanie nazw, a potem wywoływanie

(def lubię-kolor-niebieski (lubię-kolor "niebieski"))
(def lubię-kolor-zielony   (lubię-kolor "zielony"))
(def lubię-kolor-czerwony  (lubię-kolor "czerwony"))

((lubię-kolor "niebieski"))  ; => "Lubię kolor niebieski."
(lubię-kolor-niebieski)      ; => "Lubię kolor niebieski."
(lubię-kolor-zielony)        ; => "Lubię kolor zielony."
(lubię-kolor-czerwony)       ; => "Lubię kolor czerwony."

Powyżej mamy funkcję lubię-kolor, która przyjmuje jeden argument (nazwę koloru), a następnie zwraca bezargumentową funkcję, której wartością jest złączenie napisu Lubię kolor, wprowadzonego koloru i kropki (do tego służy funkcja str).

Warto zauważyć, że zwracane przez lubię-kolor funkcje odwołują się do powiązania pochodzącego z obejmującego zasięgu leksykalnego (wartości z nazwą koloru przekazanej do funkcji wyższego rzędu z użyciem argumentu kolor), nawet jeśli wywoływane są w innych leksykalnych kontekstach. To “przywłaszczanie” i zapamiętywanie przez funkcje niektórych powiązań obecnych w otoczeniu nazywamy zamykaniem ich w funkcji, a funkcję, która tego dokonuje tzw. domknięciem.

Wbudowane funkcje wyższego rzędu

Clojure wyposażono w funkcje wyższego rzędu, które pozwalają dokonywać popularnych przekształceń z wykorzystaniem innych funkcji.

Dopełnienie, complement

Operacja dopełnienia (ang. complement) jest takim przekształceniem predykatu (funkcji zwracającej wartość logiczną), że nowo powstała funkcja zwraca wartość przeciwną do oryginalnej.

W Clojure możemy stosować dopełnienie, korzystając z funkcji complement. Przyjmuje ona jeden argument, który powinien być funkcją, a zwraca funkcję, która emituje przeciwną wartość logiczną. Mówiąc precyzyjnie, funkcja ta będzie zwracała wartość true, jeśli oryginalna (wywoływana przez nią) funkcja zwróci false lub nil. W przeciwnym przypadku będzie zwracała false (wewnętrznie korzysta z funkcji not).

Użycie:

  • (complement funkcja).
Przykład użycia funkcji complement
1
2
3
4
(def niepusty? (complement empty?))

(niepusty? [])   ; => false
(niepusty? [1])  ; => true

Argumenty wartościowe, fnil

Dzięki funkcji fnil możemy generować funkcje, które wywołują oryginalne funkcje, przy czym wartością przekazywanego im argumentu lub argumentów nigdy nie będzie nil.

Funkcja fnil przyjmuje dwa obowiązkowe argumenty. Pierwszym powinna być funkcja, a drugim wartość, która zastąpi przyjmowany argument podczas jej wywoływania, jeżeli będzie on równy nil.

Jeśli podano więcej argumentów, to zostaną one użyte jako domyślne wartości kolejnych argumentów oryginalnej funkcji.

Funkcja fnil zwraca obiekt typu funkcyjnego.

Użycie:

  • (fnil funkcja wartość-domyślna & wartość-domyślna…).
Przykład użycia funkcji fnil
1
2
3
4
(def tekst-lub-brak (fnil identity "-brak-"))

(tekst-lub-brak "napis")  ; => "napis"
(tekst-lub-brak nil)      ; => "-brak-"

Funkcja stała, constantly

Funkcja stała to taka funkcja, która niezależnie od wartości przekazanych argumentów zwraca zawsze stałą wartość.

W Clojure możemy łatwo definiować funkcje stałe, korzystając z funkcji wyższego rzędu o nazwie constantly. Przyjmuje ona jeden argument, a zwraca wieloargumentową funkcję, która wywołana będzie zawsze zwracała jego wartość.

Funkcje stałe w praktyce przydają się wszędzie tam, gdzie wymagane jest podanie funkcji, a chcemy korzystać z ustalonej wartości. Przykładem może być tu funkcja alter-var-root, używana do zmiany powiązania głównego obiektu typu Var (np. zmiennej globalnej). Wymaga ona podania funkcji, która na bazie poprzedniej wartości wyemituje nową. Zamiast tworzyć taką funkcję w postaci lambda-wyrażenia czy konstrukcji fn, możemy uczytelnić kod stosując constantly. Innym zastosowaniem może być przekazanie stałej funkcji do update-in, aby aktualizować elementy wektora stałą wartością.

Użycie:

  • (constantly wartość).
Przykłady użycia funkcji constantly
1
2
3
4
5
6
7
8
9
10
11
;; nazywamy obiekt funkcyjny zwracany przez wywołanie constantly
(def pięć (constantly 5))

;; tworzymy zmienną globalną na potrzeby przykładu
(def globalna 1)

(pięć)                            ; => 5
(pięć 25)                         ; => 5
(alter-var-root #'globalna pięć)  ; => 5
globalna                          ; => 5
(update-in [1 2 3] [0] pięć)      ; => [5 2 3]

Częściowe zastosowanie funkcji, partial

Operacja częściowego zastosowania funkcji (ang. partial function application) polega na przekształceniu funkcji, która przyjmuje pewną liczbę argumentów, w taki sposób, że powstaje nowa funkcja o mniejszej argumentowości. Wynikowa funkcja przyjmuje część oryginalnych argumentów i wraz z zestawem argumentów o wcześniej ustalonych wartościach przekazuje je do przekształcanej funkcji podczas jej wywoływania.

W Clojure możemy posłużyć się funkcją partial, aby dokonać operacji częściowego zastosowania funkcji. Przyjmuje ona jeden obowiązkowy argument, którym powinna być n-argumentowa funkcja i mniejszą lub równą n liczbę argumentów, a zwraca funkcję, która przyjmuje pozostałe (nie podane) argumenty i wywołuje przekazaną funkcję dla wszystkich.

Użycie:

  • (partial funkcja & argument…).
Przykład użycia funkcji partial
1
2
3
(def dodaj-dwa (partial + 2))
(dodaj-dwa 5)
; => 7

Rozwijanie funkcji (currying)

Operacja rozwijania funkcji (ang. currying) polega na przekształceniu funkcji przyjmującej pewną liczbę argumentów w taki sposób, że powstaje sekwencja funkcji, z których każda przyjmuje jeden argument. Nietrudno się domyślić, że w przypadku wszystkich funkcji poza ostatnią argumentami będą funkcje wywołujące kolejne funkcje z sekwencji. Rozwijanie funkcji bywa często mylone z częściowym zastosowaniem.

W czysto funkcyjnym języku Haskell, którego jednym z fundamentów jest automatyczne rozwijanie funkcji, istnieje wbudowana obsługa tego procesu, której nie znajdziemy w Lispach (włączając Clojure). Powodem jest przede wszystkim fakt, że dla poprawnej automatyzacji rozwijania argumentowość funkcji musi być zawsze ściśle określona. Najlepiej też, aby typizowanie było silne, a nie dynamiczne. Warto mieć jednak na względzie, że większość sytuacji można obsłużyć, posługując się funkcją partial, i stosując parametry wariadyczne.

Można pokusić się o stworzenie marka, które posłuży do definiowania automatycznie rozwijanych funkcji. W Sieci znajdziemy nawet gotowe przykłady.

Przykład implementacji i użycia makra tworzącego automatycznie rozwijalne funkcje
1
2
3
4
5
6
7
8
9
10
11
12
13
14
(defmacro defc
  [ident bindings & body]
  (let [n# (count bindings)]
    `(def ~ident
       (fn [& args#]
         (if (< (count args#) ~n#)
           (apply partial ~ident args#)
           (let [myfn# (fn ~bindings ~@body)]
             (apply myfn# args#)))))))

(defc suma [a b] (+ a b))

(suma 1 2)    ; => 3
((suma 1) 2)  ; => 3

Zobacz także:

Złożenie funkcji, comp

Złożenie funkcji (ang. function composition), zwane też superpozycją funkcji jest operacją polegającą na takim przekształceniu podanych jako argumenty funkcji w nową funkcję, że zwracaną wartością jest wartość pierwszej składanej funkcji wywołanej dla argumentu będącego rezultatem obliczenia wartości kolejnej. Najbardziej zagnieżdżona funkcja składana będzie przyjmowała argumenty, które podano przy wywołaniu wytworzonej funkcji (zwanej funkcją złożoną).

W Clojure do składania funkcji służy funkcja comp. Przyjmuje ona zero lub więcej argumentów, które powinny być funkcjami, a zwraca funkcję będącą ich złożeniem. Funkcje składane są od lewej podanej do prawej. Najbardziej zagnieżdżoną funkcją składaną będzie więc ta, którą przekazano jako ostatni argument i to do niej trafią oryginalnie przekazane argumenty.

Użycie:

  • (comp & funkcja…).
Przykład użycia funkcji comp
1
2
3
4
5
6
7
8
9
10
11
12
13
(def tekst-z-mnożenia (comp str *))

;; odpowiednik (str (* 1))
(tekst-z-mnożenia 1)
; => "1"

;; odpowiednik (str (* 1 2))
(tekst-z-mnożenia 1 2)
; => "2"

;; odpowiednik (str (* 2 3 4))
(tekst-z-mnożenia 2 3 4)
; => "24"

Redukowanie (zwijanie), reduce

Zwijanie (ang. fold), zwane też redukowaniem, to operacja wyrażana funkcjami wyższego rzędu, która polega na redukcji elementów kolekcji o dostępie sekwencyjnym do pojedynczej wartości przez wywoływanie podanego operatora na kolejnych elementach i zakumulowanym rezultacie wywołania go na poprzednich.

W Clojure zwijanie obsługiwane jest przez funkcje reduce, reduce-kvreductions (omówione w rozdziałach poświęconych kolekcjom i sekwencjom).

Użycie:

  • (reduce-kv  operator akumulator wektor),
  • (reduce     operator sekwencja),
  • (reductions operator sekwencja),
  • (reductions operator wartość sekwencja).
Przykład użycia funkcji reduce
1
2
(reduce + [1 2 3 4])
; => 10

Odwzorowywanie elementów, map

Za odwzorowywanie (ang. mapping) elementów struktury o sekwencyjnym interfejsie dostępu odpowiada funkcja map (dokładniej opisana w rozdziale IX). Dzięki niej możemy przekształcać wartości kolejnych elementów sekwencji na podstawie podanej funkcji.

Funkcja map przyjmuje dwa argumenty. Pierwszy z nich powinien być funkcją, która będzie wywołana dla każdego elementu podanej jako drugi argument kolekcji o sekwencyjnym interfejsie dostępu. Wartością zwracaną będzie leniwa sekwencja, której elementy są rezultatami wywołań przekazanego operatora na odpowiadających im elementach oryginalnej struktury.

Gdy podano więcej niż jedną sekwencję, to przekazana funkcja musi być w stanie przyjąć odpowiednio większą liczbę argumentów, ponieważ w takim przypadku przetwarzane będą kolejne elementy z każdej podanej sekwencji jednocześnie.

Odmianą funkcji map, która do funkcji przekształcającej przekazuje dodatkowo numer kolejny elementu (poczynając od 0) jest map-indexed. Istnieje również funkcja pmap, realizująca przekształcenie w sposób współbieżny.

Użycie:

  • (map         transformator sekwencja & sekwencja…),
  • (pmap        transformator sekwencja & sekwencja…),
  • (map-indexed transformator sekwencja & sekwencja…).
Przykłady użycia funkcji map
1
2
3
4
5
6
7
8
(map inc [1 2 3 4])
; => (2 3 4 5)

(map + [1 2 3] [1 2 3])
; => (2 4 6)

(map (partial str "Witaj, ") '[świecie matko ojcze])
; => ("Witaj, świecie" "Witaj, matko" "Witaj, ojcze")

Filtrowanie elementów, filter

Filtry (ang. filters) to popularna klasa funkcji wyższego rzędu, które służą do przekształcania struktury o sekwencyjnym interfejsie dostępu. Dzięki operacji filtrowania powstaje struktura pochodna z wyłączonymi elementami, dla których przekazany predykat zwraca wartość logicznej prawdy.

W Clojure istnieje funkcja filter, dzięki której możemy dokonywać filtrowania danych sekwencyjnych. Zwraca ona leniwą sekwencję, składającą się z elementów sekwencji podanej jako drugi argument, dla których jednoargumentowa funkcja podana jako pierwszy argument zwraca wartości różne od false i różne od nil.

Użycie:

  • (filter predykat sekwencja).
Przykład użycia funkcji filter
1
2
(filter odd? [1 2 3 4 5])
; => (1 3 5)

Podobne w działaniu funkcje opisano w rozdziałach VIII i IX: keep, keep-indexed, filterv.

Zestawienie funkcji, juxt

Zestawienie funkcji (ang. functions juxtaposition) to operacja, która dla zestawu podanych funkcji tworzy funkcję zwracającą zestaw wartości emitowanych przez wywołanie każdej z nich.

W Clojure zestawienie realizowane jest przez funkcję juxt. Przyjmuje ona jeden lub więcej argumentów, które powinny być funkcjami i zwraca wieloargumentową funkcję, która wywołuje każdą z nich, a rezultaty umieszcza w zwracanym wektorze.

Użycie:

  • (juxt funkcja & funkcja…).
Przykład użycia funkcji juxt
1
2
3
4
(def imiona ["Paweł" "Gaweł" "Szaweł"])

(map (juxt identity count) imiona)
; => (["Paweł" 5] ["Gaweł" 5] ["Szaweł" 6])

W powyższym przykładzie przekształcamy wektor imiona w ten sposób, że dla każdego elementu jest stosowana funkcja emitowana przez juxt, która dokonuje wytworzenia wektora. Pierwszym elementem każdego z wektorów jest wartość tego elementu (rezultat działania funkcji identity), a drugim liczba znaków łańcucha tekstowego stanowiącego wartość (rezultat działania funkcji count).

Argumenty z sekwencji, apply

Funkcja apply pozwala używać sekwencji jako źródła argumentów funkcji. Każdy kolejny element sekwencji podanej jako ostatni argument jej wywołania będzie podstawiony do zestawu argumentów przyjmowanych przez funkcję, którą należy podać jako pierwszy argument. Wszystkie dodatkowe argumenty przekazane do apply zostaną przekazane w niezmienionej postaci do wywoływanej funkcji przed argumentami pochodzącymi z sekwencji.

Użycie:

  • (apply funkcja sekwencja),
  • (apply funkcja argument-funkcji… sekwencja).
Przykłady użycia funkcji apply
1
2
3
(apply str                  [1 2 3])  ; => "123"
(apply +                    [1 2 3])  ; => 6
(apply (comp keyword str +) [1 2 3])  ; => :6

Uwaga: Funkcji apply nie możemy stosować, przekazując jej jako pierwszy argument wartości makr.

Suma predykatów, some-fn

Funkcja some-fn służy do generowania takich funkcji, które dla każdego przekazywanego im argumentu sprawdzają, czy dowolny z podanych predykatów wywołany z wartością tego argumentu zwróci logiczną prawdę (nie wartość nil i nie false). Jeżeli choć jeden argument przekazany do wygenerowanej funkcji spełni ten warunek, to testowanie kolejnych przekazanych argumentów zostanie przerwane i zwrócona będzie wartość będąca efektem wywołania ostatniego predykatu. Jeżeli dla żadnego z argumentów nie zostanie spełniony wspomniany warunek prawdy, to zwrócona zostanie wartość ostatnio wywołanego predykatu dla ostatniego sprawdzanego argumentu.

Funkcja some-fn przyjmuje przynajmniej jeden argument (predykat wyrażony obiektem funkcyjnym), a zwraca obiekt typu funkcyjnego.

Użycie:

  • (some-fn predykat & predykat…).
Przykłady użycia funkcji some-fn
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(def są-dodatnie? (some-fn pos?))
(są-dodatnie?      0)  ; => false
(są-dodatnie?      1)  ; => true
(są-dodatnie?     -1)  ; => false
(są-dodatnie? 0 1 -1)  ; => true
(są-dodatnie?  -1  1)  ; => true

(def są-sobą? (some-fn identity))
(są-sobą?             0)  ; => 0
(są-sobą?           nil)  ; => nil
(są-sobą?         false)  ; => false
(są-sobą? false nil 1 2)  ; => 1

(def są-dodatnie-lub-parzyste? (some-fn pos? even?))
(są-dodatnie-lub-parzyste?     0)  ; => true  (bo prawda dla even?)
(są-dodatnie-lub-parzyste?     2)  ; => true  (bo prawda dla pos?)
(są-dodatnie-lub-parzyste?    -2)  ; => true  (bo prawda dla even?)
(są-dodatnie-lub-parzyste? 0 1 2)  ; => true  (bo prawda dla even? przy 0)
(są-dodatnie-lub-parzyste? -3  0)  ; => true  (bo prawda dla even? przy 0)
(są-dodatnie-lub-parzyste? -3 -1)  ; => false (bo żaden nie spełnia żadnego)

Zobacz także:

Iloczyn predykatów, every-pred

Funkcja every-pred służy do generowania takich funkcji, które dla każdego przekazywanego im argumentu sprawdzają, czy każdy z podanych predykatów wywołany z wartością tego argumentu zwróci logiczną prawdę (nie wartość nil i nie false). Jeżeli choć jeden argument przekazany do wygenerowanej funkcji nie spełni tego warunku, to testowanie kolejnych przekazanych argumentów zostanie przerwane i zwrócona będzie wartość false. Jeżeli dla każdego z argumentów zostanie spełniony wspomniany warunek prawdy, to zwrócona zostanie wartość true.

Funkcja every-pred przyjmuje przynajmniej jeden argument (predykat wyrażony obiektem funkcyjnym), a zwraca obiekt typu funkcyjnego.

Użycie:

  • (every-pred predykat & predykat…).
Przykłady użycia funkcji every-pred
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(def wszystkie-dodatnie? (every-pred pos?))
(wszystkie-dodatnie?      0)  ; => false
(wszystkie-dodatnie?      1)  ; => true
(wszystkie-dodatnie?     -1)  ; => false
(wszystkie-dodatnie? 0 1 -1)  ; => false (bo 0 nie spełnia warunku)
(wszystkie-dodatnie?  -1  1)  ; => false (bo -1 nie spełnia warunku)

(def wszystkie-sobą? (every-pred identity))
(wszystkie-sobą?             0)  ; => true
(wszystkie-sobą?           nil)  ; => false (bo rezultatem predykatu jest nil)
(wszystkie-sobą?         false)  ; => false (bo rezultatem predykatu jest false)
(wszystkie-sobą? false nil 1 2)  ; => false (bo pierwszy predykat zwraca false)

(def wszystkie-dodatnie-i-parzyste? (every-pred pos? even?))
(wszystkie-dodatnie-i-parzyste?   0)  ; => false (bo fałsz dla pos?)
(wszystkie-dodatnie-i-parzyste?   2)  ; => true  (bo prawda dla obu)
(wszystkie-dodatnie-i-parzyste?   3)  ; => false (bo fałsz dla even?)
(wszystkie-dodatnie-i-parzyste? 2 3)  ; => false (bo fałsz dla even? przy 3)

Zobacz także:

Spamiętywanie

Spamiętywanie (ang. memoization) to sposób optymalizacji programów komputerowych przez zapisywanie rezultatów operacji (np. wartości zwracanych przez funkcje) w pamięci podręcznej, aby przy kolejnych wywołaniach tych samych podprogramów korzystać z już obliczonych wyników dla takich samych danych wejściowych (np. argumentów).

Możemy powiedzieć, że spamiętywanie jest pewną formą podręcznego buforowania (ang. caching) – pojawia się zysk wydajnościowy, jednak przy zwiększonym zapotrzebowaniu na zasoby pamięciowe, używane do przechowywania rezultatów.

Spamiętywanie przydaje się szczególnie w rekurencyjnych funkcjach czy generatorach sekwencji, ponieważ nie trzeba wtedy przeliczać od początku całego łańcucha zależnych od siebie wartości, lecz można rozpocząć od konkretnego elementu ze zbioru wyników (wartości bezpośrednio lub pośrednio poprzedzającej wyliczaną).

Dzięki wbudowanej funkcji memoize możemy dokonywać spamiętywania wartości zwracanych przez funkcję podaną jako pierwszy argument. Wywołanie memoize zwraca nową funkcję, która najpierw sprawdza w podręcznej mapie, czy dla przekazanych wartości argumentów stanowiących klucz nie zostały już zapamiętane wyniki. Jeśli tak jest, to oryginalna funkcja nie jest wywoływana, lecz zwracany jest zbuforowany wynik. Jeżeli w pamięci podręcznej nie znaleziono odpowiedniej asocjacji, jest ona do niej dodawana po zwróceniu wartości przez oryginalną funkcję, a następnie zwracana jest ta wartość.

Użycie:

  • (memoize funkcja).
Przykład użycia funkcji memoize
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(defn funkcja [x]
  (println "wywołanie dla x =" x)
  x)

(def funkcja (memoize funkcja))

(funkcja 0)
; >> wywołanie dla x = 0
; => 0

(funkcja 1)
; >> wywołanie dla x = 1
; => 1

(funkcja 1)
; => 1

Widzimy, że w przedostatniej linii przykładu nie został uruchomiony podprogram funkcji funkcja (zawierający efekt uboczny w postaci wypisywania tekstu na konsolę), lecz dla istniejącego w pamięci rezultatów klucza (1), funkcja spamiętująca zwróciła wcześniej zapisany wynik.

Zobacz także:

Inne funkcje wyższego rzędu

W Clojure poza omówionymi wcześniej istnieją również wbudowane funkcje wyższego rzędu o mniej ogólnym zastosowaniu, których opisy znajdziemy w innych rozdziałach (np. poświęconym sekwencjom czy kolekcjom):

Domknięcia

Domknięcie (ang. closure), zwane też domknięciem leksykalnym (ang. lexical closure) lub domknięciem funkcyjnym (ang. function closure) to taka funkcja, w której ciele są widoczne powiązania dostępne w leksykalnym zasięgu obejmującym jej definicję. Przykładem mogą być tu powiązania o zasięgu leksykalnym stworzone w nadrzędnych wyrażeniach let.

Domknięciem będzie więc funkcja (lub jej ekwiwalent), w której zamknięto użyte w jej ciele powiązania pochodzące z leksykalnego otoczenia (zawierającego jej definicję). Oznacza to, że na zasięg leksykalny funkcji składa się nie tylko zasięg jej ciała, ale również zasięg obejmujący jej definicję.

Nieco prościej: domknięcie to funkcja, w której możemy korzystać z powiązań widocznych w otoczeniu jej definicji.

W języku Clojure każda funkcja jest domknięciem.

Jak tworzone są domknięcia?

Dla każdej formuły symbolowej (niezacytowanego symbolu) użytej w ciele funkcji, która powiązana jest z wartością pochodzącą z obejmującego obszaru leksykalnego, będzie wygenerowana specjalna referencja. Dzięki niej możliwym stanie się odwoływanie do wskazywanej powiązaniem wartości w danej funkcji. Proces ten nazywamy kopiowaniem wartości do domknięcia, mimo że w przypadku Clojure nie są kopiowane wartości, ale właśnie odwołania do nich.

Możemy też spotkać się z określeniem, że pewne powiązanie (np. zmienna leksykalna pochodząca spoza funkcji) jest zamykane w funkcji.

W domknięciu nie będą widoczne:

  • symboliczne identyfikatory przesłonięte lokalnymi nazwami (np. argumentami funkcji czy powiązaniami leksykalnymi),
  • obecne w środowisku lokalne powiązania zmiennych (nieglobalne obiekty typu Var stworzone np. z użyciem makra with-local-vars).

Należy również ostrożnie podchodzić do lokalnych tymczasowych redefinicji zmiennych globalnych (czyt. nie polegać na ich powiązaniach w domknięciach).

Jeśli pochodząca z otoczenia funkcji wartość, która wyraża stan pewnej nazwanej tożsamości, ulega podmianie na inną, to w każdym kolejnym wywołaniu tej funkcji będzie ona różna.

Przykłady domknięć

Domknięcia a funkcjonały

Funkcje wyższego rzędu, których wartościami zwracanymi są obiekty typu funkcyjnego, mogą być ciekawym przykładem domknięć, ponieważ tworzone w nich obiekty funkcyjne mogą zamykać w sobie obecne w nich powiązania.

Przykład domknięcia w zwracanym obiekcie funkcyjnym
1
2
3
4
5
6
7
8
9
10
11
;; Funkcja, która stwarza funkcje dodające podaną liczbę do argumentu.
(defn powiększalnik [o-ile]
  (fn [składnik] (+ o-ile składnik)))

;; Zmienne globalne powiązane z funkcjami powiększającymi.
(def dodaj-dwa  (powiększalnik 2))
(def dodaj-trzy (powiększalnik 3))

;; Wywołania.
(dodaj-dwa 2)   ; => 4
(dodaj-trzy 2)  ; => 5

W powyższym przykładzie wartość parametru o-ile jest zamykana w anonimowej funkcji zwracanej przez funkcję powiększalnik.

Domknięcia a stałe tożsamości

Przykład definiowania funkcji wewnątrz formy specjalnej `let`
1
2
3
4
5
6
7
8
9
(def x 50)       ; powiązanie globalne x z wartością 50

(let [x 20]      ; powiązanie leksykalne x z wartością 20
  (defn funk []  ; definicja funkcji funk
    x))          ; zwracamy x

(funk)           ; wartościowanie funkcji funk
; => 20          ; uzyskano powiązanie z domknięcia
                 ; a nie z globalnego powiązania
Przykład aktualizowania zmiennej globalnej w formie specjalnej `let`
1
2
3
4
5
6
7
(def zwiększ-x
  (let [w (def x 50)]
    (fn [] (alter-var-root w inc))))

(zwiększ-x)  ; => 51
(zwiększ-x)  ; => 52
(zwiększ-x)  ; => 53

Ograniczenia domknięć

Niektóre powiązania nie będą domykane w tworzonych funkcjach. Należą do nich między innymi zmienne lokalne.

Przykład próby skorzystania z powiązań, które nie wchodzą w skład domknięć
1
2
3
4
5
6
7
;; Zmienne lokalne
(defn funkcja []
  (with-local-vars [x 5]
    (fn [] (var-get x))))

((funkcja))
; => #<Unbound Unbound: #<Var: --unnamed-->>

Zobacz także:

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

Komentarze