Poczytaj mi Clojure, cz. 8

Funkcje i domknięcia

Grafika

Podstawowymi jednostkami funkcyjnie zorientowanych programów komputerowych są funkcje. W tym odcinku poznamy sposoby ich tworzenia i używania, a także dowiemy się czym są domknięcia oraz funkcje wyższego rzędu.

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ęć, ponieważ powstające w funkcjach rezultaty działań pośrednich mogą być bezpiecznie usunięte z pamięci po zakończeniu wywoływania funkcji.

funkcyjnych językach programowania (ang. functional programming languages) i językach 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).

Języki funkcyjne to takie, w których zastosowanie znajduje funkcyjny paradygmat programowania. Przetwarzanie danych bazuje w nim na obliczaniu wartości funkcji, które są czyste (ang. pure), czyli mogą być wyrażone jako funkcje znane z matematyki. Oznacza to, że dla takich samych argumentów o ustalonych wartościach za każdym razem będą zwracały takie same wyniki i nie zmienią stanu otoczenia (niezwiązanych z nimi obiektów w pamięci). Ich interfejsem do kontaktu z innymi częściami programu będą wyłącznie: 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))
(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ą wartości obiektów znajdujących się poza bezpośrednim zasięgiem ich ciał. 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, uznanym za język czysto funkcyjny.

Paradygmat funkcyjny oznacza również, że w programie nie operujemy na zmiennych, lecz na wartościach, a w rezultacie unikamy obsługi współdzielonych, zmieniających się stanów, czyli takich obiektów, których pamięciowa zawartość może być bezpośrednio modyfikowana z różnych miejsc w programie. Sposobem na wyrażanie logiki aplikacji w świecie funkcyjnym jest budowanie funkcji, które na podstawie przekazywanych argumentów zwracają wyniki, a gdy pojawia się potrzeba kontaktu ze środowiskiem zewnętrznym (np. podsystemem wejścia/wyjścia) korzysta się specjalnych konstruktów, np. monad.

Clojure pozwala programować bez stosowania zmiennych, współdzielonych stanów, jednak nie ogranicza programisty do czysto funkcyjnego podejścia. Dopuszcza się manipulowanie pewnymi pamięciowymi obiektami dostępnymi z wielu miejsc w programie, jednak z użyciem specjalnych konstrukcji (tzw. typów referencyjnych i odpowiednich form ich obsługi), które gwarantują odpowiednią izolację między wątkami i określone strategie aktualizowania wartości.

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ć odwołania do funkcji w strukturach danych (np. w zmiennych globalnych);

  • przekazywać funkcje jako argumenty do makr, form specjalnych lub innych funkcji, a także zwracać funkcje jako rezultaty wykonania operacji, czyli tworzyć funkcje wyższego rzędu, a w związku z tym:

    • korzystać ze struktur danych, w których dostęp do elementów wiąże się z automatycznym wywołaniem ustawionej wcześniej funkcji (zwłoczne obliczanie wartości);

    • tworzyć funkcje pochodne względem istniejących, które różnią się liczbą przyjmowanych argumentów (zastosowanie częściowe, zwijanierozwijanie).

Używanie funkcji

Aby korzystać z funkcji, należy ją wcześniej zdefiniować (ang. define), korzystając z jednej z służących do tego form, przedstawionych w postaci odpowiedniego S-wyrażenia.

Zawartość funkcji

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ływaniu 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 (wcześniejszym przeliczeniu ich do form stałych).

Wywoływanie będzie zwykle odbywało się z użyciem symbolicznego wyrażenia listowego, które zostanie potraktowane jak forma wywołania funkcji. Pierwszym elementem takiego listowego S-wyrażenia będzie najczęściej forma symbolowa, którą powiązano z funkcją z użyciem zmiennej globalnej lub funkcyjny obiekt wyrażony w inny sposób (np. będący efektem wywołania funkcji bądź makra, które zwraca funkcję). Kolejnymi elementami listowego wyrażenia będą 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 formie 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 mianem lambda-wyrażeń (ang. lambda expressions).

Po co nam funkcje, do których nie możemy odwołać się z użyciem czytelnych nazw? Przydają się tam, gdzie występuje konieczność utworzenia i przekazania niewielkiego obiektu funkcyjnego jako argumentu funkcji bądź makra, a także wtedy, gdy zechcemy samodzielnie powiązać taki obiekt z symbolem, np. w zasięgu leksykalnym formy specjalnej let, makra letfn i podobnych.

Jako pierwszy, opcjonalny argument forma fn przyjmuje wyrażoną symbolem nazwę (sic!), która będzie dostępna w jej ciele i pozwoli funkcji odwoływać się do własnego kodu, gdy zajdzie taka konieczność. Nazwa ta nie będzie widoczna w szerszym niż ciało funkcji kontekście leksykalnym i jest pomocna w rekurencyjnych wywołaniach.

Kolejnym, obowiązkowym już argumentem jest wektor parametryczny, który tworzy tzw. wektorową formę powiązaniową. Używamy go, aby deklarować zestaw przyjmowanych przez funkcję argumentów. Wektor ten może być pusty (w przypadku braku przyjmowanych argumentów), lecz należy go podać.

W najprostszej postaci wektor parametryczny może składać się z niezacytowanych symboli, które będą tworzyły wtedy formy powiązaniowe. 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 formy 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 będą S-wyrażeniami stanowiącymi ciało funkcji. Wartość ostatnio obliczonego wyrażenia będzie wartością zwracaną przez funkcję po jej wywołaniu.

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.

Forma 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))))))
(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 forma fn zwraca funkcyjny obiekt, jednak nie możemy odwołać się do niego w dowolnym miejscu programu, ponieważ nie nadaliśmy mu żadnej globalnej tożsamości. Wiemy jednak, że łatwo ten problem rozwiązać, jeżeli 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 odwoływać się do 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)))))))
(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ę, zostanie ona wywołana:

Przykład wywołania globalnie nazwanej funkcji
1
2
(rok-przestępny? 2000)
; => true
(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 formą wywołania funkcji, czyli 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ć formę 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ść (również będąca jakąś formą) podstawiana w miejscu wywołania funkcji w celu dalszego przeliczania.

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

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

Lambda-wyrażenia, #()

Makro czytnika #() (symbol kratki i listowe S-wyrażenie), pozwala wyrażać literał funkcji anonimowej, który – jak sama nazwa wskazuje – pozwala zwięźle i czytelnie zapisywać funkcje anonimowe. Zastosujemy go tam, gdzie ciała funkcji nie składają się ze zbyt wielu wyrażeń.

Zawarte w zapisie S-wyrażenie powinno być formą wywołania, czyli jego pierwszym elementem musi być forma symbolowa, odnosząca się do jakiegoś podprogramu (funkcji, makra bądź formy specjalnej), a kolejne S-wyrażenia stanowiły będą argumenty przekazywane podczas wywoływania.

W wyrażeniu można korzystać z symbolu %, w miejsce którego zostaną podstawione pozycyjne 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ą umieszczoną tuż za znakiem procenta. Możliwe jest również podanie symbolu %&, który zostanie zastąpiony wektorem, gdzie każdy kolejny element będzie wartością kolejnego przekazanego argumentu (jak w argumencie wariadycznym).

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.

zaleceniu odnośnie stylu programowania w Clojure, jeden ze współtwórców języka, Stuart Sierra, zwraca uwagę, że stosowanie numerowanych argumentów pozycyjnych w skróconym zapisie definiującym anonimowe funkcje może utrudniać późniejszą analizę kodu źródłowego z uwagi na mało znaczącą symbolikę.

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
(every? #(= \e %) &#34;To jest test.&#34;) ; czy każda litera to e? ; =&gt; false ; nie (every? #(= \e %) &#34;eeeee&#34;) ; czy każda litera to e? ; =&gt; true ; tak

Funkcje nazwane, defn

Aby programiści nie musieli samodzielnie nadawać funkcjom nazw (z użyciem zmiennych globalnych), w Clojure istnieje makro defn, które uczytelnia i przyspiesza proces definiowania funkcji. Działa ono podobnie do omówionej wcześniej formy fn, jednak wytwarza funkcję nazwaną (ang. named function), do której można odwoływać się z użyciem symbolicznego identyfikatora o nieograniczonym zasięgu i kontrolowanej widoczności. 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 metadanowa zawierająca metadane, 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 strukturze – 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ą po wywołaniu funkcji i zrealizowaniu jej podprogramu.

Podobnie jak w przypadku formy 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ą argument (parametr: rok).
  (or                              ; Ciało funkcji: prawda, gdy rok albo:
   (zero? (mod rok 400))           ;  · podzielny bez reszty przez 400;
   (and                            ;  · jednocześnie:
    (zero? (mod rok 4))            ;    · dzieli się bez reszty przez 4;
    (not (zero? (mod rok 100)))))) ;    · i dzieli się z resztą przez 100.
(defn rok-przestępny? ; Tworzymy globalną funkcję [rok] ; przyjmującą argument (parametr: rok). (or ; Ciało funkcji: prawda, gdy rok albo: (zero? (mod rok 400)) ; · podzielny bez reszty przez 400; (and ; · 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 z obiektem typu Var, tworząc zmienną globalną, która odwołuje się do obiektu zdefiniowanej 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 będzie przyjmowała. Formy powiązaniowe symboli umieszczone w tym wektorze nazywamy parametrami i zostaną one powiązane z wartościami odpowiadających im argumentów 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 forma, ale w naszym przypadku jest to listowe S-wyrażenie rozpoczynające się symbolem or, a więc forma wywołania 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ś z podanych mu argumentów jest różny od false i różny od nil. Gdy trafi na taki, zwraca go jako wartość, a w przeciwnym razie 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żeli ż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)
(rok-przestępny? 2000)

Pytajnik na końcu 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ę z poziomu innej przestrzeni ustawionej jako bieżąca. 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 utworzenie 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
(ns test) ; zmiana przestrzeni nazw na test (defn- funkcja-prywatna [] &#34;test&#34;) ; definicja funkcji prywatnej (funkcja-prywatna) ; wywołanie funkcji prywatnej ; =&gt; &#34;test&#34; (ns user) ; zmiana przestrzeni nazw na user (test/funkcja-prywatna) ; próba wywołania funkcji prywatnej ; &gt;&gt; clojure.lang.Compiler$CompilerException: java.lang.IllegalStateException: ; &gt;&gt; var: #&#39;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, 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ą jeszcze znane wartości kojarzone z symbolami. 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, 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 form specjalnych i makr służących do definiowania funkcji (np. fndefn).

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

  • [symbol…],

na przykład:

  • [długość szerokość coś].

Każdy z poszczególnych parametrów umieszczonych w takim wektorze musi być niezacytowanym symbolem, który podczas wywołania funkcji będzie powiązany z wartością odpowiadającego mu, spodziewanego argumentu na odpowiadającej pozycji. Wartości argumentów będą w tym przypadku 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), poliadowymi (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, 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 forma 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
(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) ; =&gt; 1 (funkcja 0 0) ; będzie obliczone drugie ciało funkcji (2 argumenty) ; =&gt; 2 (funkcja 0 0 0) ; będzie obliczone trzecie ciało funkcji (3 argumenty) ; =&gt; 3

Funkcje wariadyczne

Funkcje o zmiennej argumentowości (ang. variable arity functions), zwane też funkcjami zmiennoargumentowościowymi, funkcjami wariadycznymi (ang. variadic functions) lub funkcjami zmiennie poliadowymi (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)
(defn funkcja [a b c &amp; reszta] reszta) (funkcja 0 0 0) ; =&gt; nil (funkcja 0 0 0 1) ; =&gt; (1) (funkcja 0 0 0 1 2) ; =&gt; (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 dochodziło do zmiany wartości przetwarzanych elementów.

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)
(identity 1) ; =&gt; 1 (identity nil) ; =&gt; nil (map identity [1 2 3]) ; =&gt; (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])
(defn funkcja [a b c &amp; reszta] reszta) (:arglists (meta #&#39;funkcja)) ; =&gt; ([a b c &amp; 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żeli 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
(defn funkcja [a b c &amp; reszta] (+ 3 (count reszta))) (funkcja 1 2 3 4 5 6 7) ; =&gt; 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), zwane też wymaganymi (ang. required);

  • opcjonalne (ang. optional), zwane też dodatkowymi (ang. additional):

    • o określonych wartościach domyślnych;
    • bez określonych wartości domyślnych.

Z kolei ze względu na sposób uporządkowania możemy podzielić argumenty na:

  • 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 form 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)
(defn funkcja [a b c] (list a b c)) (funkcja 1 2 3) ; =&gt; (1 2 3) (funkcja 3 2 1) ; =&gt; (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)
(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.

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ą formę 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)
(defn funkcja [ &amp; {a :a, b :b, c :c}] (list a b c)) (funkcja :a 1 :b 2 :c 3) ; =&gt; (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żeli 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)
(defn funkcja [ &amp; {:keys [a b c]}] (list a b c)) (funkcja :a 1 :b 2 :c 3) ; =&gt; (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, 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)
(defn funkcja [ &amp; {:strs [a b c]}] (list a b c)) (funkcja &#34;a&#34; 1, &#34;b&#34; 2, &#34;c&#34; 3) ; =&gt; (1 2 3) (defn funkcja [ &amp; {:syms [a b c]}] (list a b c)) (funkcja &#39;a 1 &#39;b 2 &#39;c 3) ; =&gt; (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}
(defn funkcja [ &amp; {:keys [:a :b :c] :as wszystkie}] wszystkie) (funkcja :a 1 :b 2 :c 3) ; =&gt; {: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)
(defn funkcja [ &amp; {:keys [:a :b :c] :or {:a 1, :b 2, :c 3}}] (list a b c)) (funkcja :a 1 :b 2) ; =&gt; (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)
(defn funkcja [ &amp; {:keys [:a :b :c] :or {:a 1, :b 2, :c 3} :as wszystkie}] (list wszystkie a b c)) (funkcja :a 1 :b 2) ; =&gt; ({: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, 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
(defn funkcja [a b c] (list a b c)) ; definicja funkcji (funkcja 1 2 3) ; poprawna liczbą argumentów ; =&gt; (1 2 3) (funkcja 1 2) ; błędna liczbą argumentów ; &gt;&gt; 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)
;; sprawdzanie, czy podano wszystkie argumenty obowiązkowe (defn arg-obowiązkowe [wszystkie &amp; 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 [ &amp; {: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 ; =&gt; (1 2 3) (funkcja :a 1 :c 3) ; błędna liczba argumentów ; &gt;&gt; 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)
;; definicja funkcji (defn funkcja [&amp; reszta] ; reszta jako wszystkie {:pre [(&gt;= (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 ; =&gt; (1 2 3) (funkcja 1 2) ; błędna liczba argumentów ; &gt;&gt; java.lang.AssertionError: Assert failed: (&gt;= (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żeli 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żeli 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)
;; Funkcja wieloczłonowa (wieloargumentowościowa) (defn funkcja ([a b c] (list a b c)) ([a b] (funkcja a b 3))) (funkcja 1 2 3) ; =&gt; (1 2 3) (funkcja 1 2) ; =&gt; (1 2 3) ;; Funkcja wariadyczna (zmiennoargumentowościowa) (defn funkcja [a b &amp; reszta] (let [c (if (&gt; (count reszta) 0) (first reszta) 3)] (list a b c))) (funkcja 1 2 3) ; =&gt; (1 2 3) (funkcja 1 2) ; =&gt; (1 2 3) ;; Funkcja z argumentami nazwanymi (przeszukiwanie mapy) (defn funkcja [ &amp; {: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) ; =&gt; (1 2 3) (funkcja :a 1 :b 2) ; =&gt; (1 2 3) ;; Funkcja z argumentami nazwanymi (parametryczna mapa inicjująca) (defn funkcja [ &amp; {:keys [:a :b :c] :or {:a 1, :b 2, :c 3}}] (list a b c)) (funkcja :a 1 :b 2 :c 3) ; =&gt; (1 2 3) (funkcja :a 1 :b 2) ; =&gt; (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.

Możemy rozpatrywać dekompozycję jako jeden z wariantów tzw. dopasowywania do wzorców (ang. pattern matching). 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)
(defn funkcja [ &amp; parametr-wariadyczny ] (let [{:keys [a b c]} parametr-wariadyczny] (list a b c))) (funkcja :a 1 :b 2) ; =&gt; (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]
[: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 na czytelne uzyskiwanie dostępu do wartości argumentów, które są zorganizowane w strukturach asocjacyjnych (np. [mapach]).

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)
(defn funkcja [ {:keys [a b c]} ] (list a b c)) (funkcja {:a 1 :b 2}) ; =&gt; (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) jest czytelnym sposobem uzyskiwania dostępu do argumentów zorganizowanych w strukturze o następczym interfejsie dostępu (np. liście czy wektorze). Jest ona 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)
;; a – argument obowiązkowy ;; b, c – argumenty opcjonalne z dekompozycji parametru wariadycznego (defn funkcja [a &amp; [b c]] (list a b c)) (funkcja 1 2) ; =&gt; (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)
(defn funkcja [[a b c]] (list a b c)) (funkcja [1 2]) ; =&gt; (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)
(defn funkcja [[a b _ d]] (list a b d)) (funkcja [1 2 3 4]) ; =&gt; (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)
(defn funkcja [[a &amp; reszta]] reszta) (funkcja [1 2 3 4]) ; =&gt; (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 dalej przekazać jako argumenty 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 pozycyjnych

Przekazywanie argumentów pozycyjnych jest proste. W wywołaniu kolejnej 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)
(defn inna [a b] (list a b)) (defn druga [a b c] (inna a b)) (druga 1 2 3) ; =&gt; (1 2)

Przekazywanie 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)
(defn inna [&amp; reszta] reszta) ;; przekazywanie wszystkich argumentów wariadycznych (defn druga [a &amp; reszta] (apply inna reszta)) (druga 1 2 3 4) ; =&gt; (2 3 4) ;; przekazywanie części argumentów wariadycznych (defn druga [a &amp; reszta] (apply inna (drop 1 reszta))) (druga 1 2 3 4) ; =&gt; (3 4)

Przekazywanie dekomp. pozycyjnie

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 argumentów wariadycznych ;; z destrukturyzowanego pozycyjnie parametru wariadycznego (defn druga [a &amp; [b c d :as reszta]] (apply inna reszta)) (druga 2 3 4) ; =&gt; (2 3 4) ;; przekazywanie wybranych argumentów wariadycznych ;; z destrukturyzowanego pozycyjnie parametru wariadycznego (defn druga [a &amp; [b c d]] (inna c d)) (druga 1 2 3 4) ; =&gt; (3 4)

Przekazywanie dekomp. asocjacyjnie

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}
(defn inna [&amp; {:as reszta}] reszta) ;; przekazywanie wszystkich argumentów nazwanych (defn druga [&amp; {:keys [:a :b :c :d] :as reszta}] (apply inna (mapcat identity reszta))) (druga :c 3 :d 4) ; =&gt; {:c 3 :d 4} ;; przekazywanie wszystkich argumentów nazwanych (defn druga [&amp; {:as reszta}] (apply inna (mapcat identity reszta))) (druga :a 1 :c 3) ; =&gt; {:a 1 :c 3} ;; przekazywanie wybranych argumentów nazwanych (defn druga [&amp; {:keys [:a :b :c :d]}] (inna :c c :d d)) (druga :a 1 :b 2 :c 3 :d 4) ; =&gt; {:c 3 :d 4}

Asercje

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

Asercje znajdują zastosowanie w testowaniu programów komputerowych, 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. contract programming), zwanym również projektowaniem bazującym na kontrakcie (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 formy 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, zgłoszony zostanie wyjątek AssertionError.

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żeli po mapie podanej po wektorze parametrycznym nie pojawi się ciało funkcji, 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 formami 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 przynajmniej jeden z przyjmowanych argumentów musi być obiektem typu funkcyjnego:

Przykład tworzenia funkcji wyższego rzędu
1
2
3
(defn funk [operator arg1 arg2]
  (+ (operator arg1 arg2)
     (operator arg1 arg2)))
(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
(funk + 2 2) ; =&gt; 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."
(defn lubię-kolor [kolor] (fn [] (str &#34;Lubię kolor &#34; kolor &#34;.&#34;))) ;; nadawanie nazw, a potem wywoływanie (def lubię-kolor-niebieski (lubię-kolor &#34;niebieski&#34;)) (def lubię-kolor-zielony (lubię-kolor &#34;zielony&#34;)) (def lubię-kolor-czerwony (lubię-kolor &#34;czerwony&#34;)) ((lubię-kolor &#34;niebieski&#34;)) ; =&gt; &#34;Lubię kolor niebieski.&#34; (lubię-kolor-niebieski) ; =&gt; &#34;Lubię kolor niebieski.&#34; (lubię-kolor-zielony) ; =&gt; &#34;Lubię kolor zielony.&#34; (lubię-kolor-czerwony) ; =&gt; &#34;Lubię kolor czerwony.&#34;

Powyżej mamy funkcję lubię-kolor, która przyjmuje jeden argument (nazwę koloru), a następnie zwraca bezargumentową funkcję. Wartością tej ostatniej 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 jej definicję zasięgu leksykalnego (wartości z nazwą koloru, którą przekazano do funkcji wyższego rzędu z użyciem argumentu kolor), nawet jeżeli 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łniania (ang. complement) jest takim przekształceniem predykatu (funkcji zwracającej wartość logiczną), że nowo powstała funkcja zwraca wartość logiczną przeciwną do oryginalnej.

W Clojure możemy przekształcać funkcję, aby zwracała komplementarną do oryginalnie zwracanej wartość logiczną, 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żeli 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
(def niepusty? (complement empty?)) (niepusty? []) ; =&gt; false (niepusty? [1]) ; =&gt; 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, nawet gdy taka nieustalona wartość zostanie przekazana jako argument.

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żeli 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-"
(def tekst-lub-brak (fnil identity &#34;-brak-&#34;)) (tekst-lub-brak &#34;napis&#34;) ; =&gt; &#34;napis&#34; (tekst-lub-brak nil) ; =&gt; &#34;-brak-&#34;

Funkcja stała, constantly

Funkcja stała (ang. constant function) to taka funkcja, która niezależnie od wartości przekazywanych 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ę tam, gdzie wymagane jest przekazanie funkcji transformującej bądź decydującej, a chcemy korzystać z ustalonej wartości.

Przykładem może być tu alter-var-root, używana do zmiany powiązania głównego obiektu typu Var. 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]
;; nazywamy obiekt funkcyjny zwracany przez wywołanie constantly (def pięć (constantly 5)) ;; tworzymy zmienną globalną na potrzeby przykładu (def globalna 1) (pięć) ; =&gt; 5 (pięć 25) ; =&gt; 5 (alter-var-root #&#39;globalna pięć) ; =&gt; 5 globalna ; =&gt; 5 (update-in [1 2 3] [0] pięć) ; =&gt; [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 mniej licznej 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 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
(def dodaj-dwa (partial + 2)) (dodaj-dwa 5) ; =&gt; 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 więc, aby w takim modelu 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 [email protected]body)]
             (apply myfn# args#)))))))

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

(suma 1 2)    ; => 3
((suma 1) 2)  ; => 3
(defmacro defc [ident bindings &amp; body] (let [n# (count bindings)] `(def ~ident (fn [&amp; args#] (if (&lt; (count args#) ~n#) (apply partial ~ident args#) (let [myfn# (fn ~bindings [email protected])] (apply myfn# args#))))))) (defc suma [a b] (+ a b)) (suma 1 2) ; =&gt; 3 ((suma 1) 2) ; =&gt; 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 z zestawu będzie przyjmowała argumenty, które podano przy wywołaniu nowo utworzonej 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"
(def tekst-z-mnożenia (comp str *)) ;; odpowiednik (str (* 1)) (tekst-z-mnożenia 1) ; =&gt; &#34;1&#34; ;; odpowiednik (str (* 1 2)) (tekst-z-mnożenia 1 2) ; =&gt; &#34;2&#34; ;; odpowiednik (str (* 2 3 4)) (tekst-z-mnożenia 2 3 4) ; =&gt; &#34;24&#34;

Redukowanie (zwijanie), reduce

Zwijanie (ang. fold), zwane też redukowaniem (ang. reduce) 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ływania 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
(reduce + [1 2 3 4]) ; =&gt; 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ę, 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")
(map inc [1 2 3 4]) ; =&gt; (2 3 4 5) (map + [1 2 3] [1 2 3]) ; =&gt; (2 4 6) (map (partial str &#34;Witaj, &#34;) &#39;[świecie matko ojcze]) ; =&gt; (&#34;Witaj, świecie&#34; &#34;Witaj, matko&#34; &#34;Witaj, ojcze&#34;)

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 bez tych elementów, 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)
(filter odd? [1 2 3 4 5]) ; =&gt; (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, a zwraca wieloargumentową funkcję, która wywołuje każdą z nich, zaś 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])
(def imiona [&#34;Paweł&#34; &#34;Gaweł&#34; &#34;Szaweł&#34;]) (map (juxt identity count) imiona) ; =&gt; ([&#34;Paweł&#34; 5] [&#34;Gaweł&#34; 5] [&#34;Szaweł&#34; 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
(apply str [1 2 3]) ; =&gt; &#34;123&#34; (apply + [1 2 3]) ; =&gt; 6 (apply (comp keyword str +) [1 2 3]) ; =&gt; :6

Uwaga: Funkcji apply nie możemy stosować, przekazując jej jako pierwszy argument wartości makr, ponieważ makra są obliczane we wcześniejszej fazie działania programu.

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ę (wartość różną od nil i różną od false). Jeżeli choć jeden argument przekazany do wygenerowanej funkcji spełni ten warunek, 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, 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)
(def są-dodatnie? (some-fn pos?)) (są-dodatnie? 0) ; =&gt; false (są-dodatnie? 1) ; =&gt; true (są-dodatnie? -1) ; =&gt; false (są-dodatnie? 0 1 -1) ; =&gt; true (są-dodatnie? -1 1) ; =&gt; true (def są-sobą? (some-fn identity)) (są-sobą? 0) ; =&gt; 0 (są-sobą? nil) ; =&gt; nil (są-sobą? false) ; =&gt; false (są-sobą? false nil 1 2) ; =&gt; 1 (def są-dodatnie-lub-parzyste? (some-fn pos? even?)) (są-dodatnie-lub-parzyste? 0) ; =&gt; true (bo prawda dla even?) (są-dodatnie-lub-parzyste? 2) ; =&gt; true (bo prawda dla pos?) (są-dodatnie-lub-parzyste? -2) ; =&gt; true (bo prawda dla even?) (są-dodatnie-lub-parzyste? 0 1 2) ; =&gt; true (bo prawda dla even? przy 0) (są-dodatnie-lub-parzyste? -3 0) ; =&gt; true (bo prawda dla even? przy 0) (są-dodatnie-lub-parzyste? -3 -1) ; =&gt; 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ę (wartość różną od nil i różną od false). Jeżeli choć jeden argument przekazany do wygenerowanej funkcji nie spełni tego warunku, 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, 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)
(def wszystkie-dodatnie? (every-pred pos?)) (wszystkie-dodatnie? 0) ; =&gt; false (wszystkie-dodatnie? 1) ; =&gt; true (wszystkie-dodatnie? -1) ; =&gt; false (wszystkie-dodatnie? 0 1 -1) ; =&gt; false (bo 0 nie spełnia warunku) (wszystkie-dodatnie? -1 1) ; =&gt; false (bo -1 nie spełnia warunku) (def wszystkie-sobą? (every-pred identity)) (wszystkie-sobą? 0) ; =&gt; true (wszystkie-sobą? nil) ; =&gt; false (bo rezultatem predykatu jest nil) (wszystkie-sobą? false) ; =&gt; false (bo rezultatem predykatu jest false) (wszystkie-sobą? false nil 1 2) ; =&gt; false (bo pierwszy predykat zwraca false) (def wszystkie-dodatnie-i-parzyste? (every-pred pos? even?)) (wszystkie-dodatnie-i-parzyste? 0) ; =&gt; false (bo fałsz dla pos?) (wszystkie-dodatnie-i-parzyste? 2) ; =&gt; true (bo prawda dla obu) (wszystkie-dodatnie-i-parzyste? 3) ; =&gt; false (bo fałsz dla even?) (wszystkie-dodatnie-i-parzyste? 2 3) ; =&gt; 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. wybranych lub wszystkich 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 i 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żeli tak jest, 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
(defn funkcja [x] (println &#34;wywołanie dla x =&#34; x) x) (def funkcja (memoize funkcja)) (funkcja 0) ; &gt;&gt; wywołanie dla x = 0 ; =&gt; 0 (funkcja 1) ; &gt;&gt; wywołanie dla x = 1 ; =&gt; 1 (funkcja 1) ; =&gt; 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, w której „zamknięto” używane w wyrażeniach jej ciała 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 własnego kodu źródłowego, ale również zasięg obejmujący jej definicję.

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

Przymiotnik „domknięcie” używany jest na nazwanie zjawiska zamykania części leksykalnego środowiska w obiekcie funkcji. Do tego środowiska mogą należeć np. powiązania symboli z wartościami tuż przed zdefiniowaniem funkcji w obejmującym ją zasięgu. Funkcja niejako ujmuje, otacza środowisko, a jego część może „zabrać ze sobą” i użytkować, gdy realizowany będzie podprogram zdefiniowany jej ciałem.

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

Jak tworzone są domknięcia?

Dla każdej formy symbolowej 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 kodzie danej funkcji, niezależnie od tego, gdzie została wywołana. 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. parametrami 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żeli pochodząca z otoczenia funkcji wartość obiektu referencyjnego, która wyraża zmieniający się stan pewnej tożsamości, ulega zmianie, wtedy w każdym kolejnym wywołaniu tej funkcji jej uzyskiwana wartość bieżąca będzie 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
;; 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) ; =&gt; 4 (dodaj-trzy 2) ; =&gt; 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
(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 ; =&gt; 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
(def zwiększ-x (let [w (def x 50)] (fn [] (alter-var-root w inc)))) (zwiększ-x) ; =&gt; 51 (zwiększ-x) ; =&gt; 52 (zwiększ-x) ; =&gt; 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-->>
;; Zmienne lokalne (defn funkcja [] (with-local-vars [x 5] (fn [] (var-get x)))) ((funkcja)) ; =&gt; #&lt;Unbound Unbound: #&lt;Var: --unnamed--&gt;&gt;

Zobacz także:

Jesteś w sekcji

comments powered by Disqus