stats

Poczytaj mi Clojure, cz. 5

Systemy typów

Grafika

Typy danych pozwalają klasyfikować wartości pod względem różnych cech i wykształcać relacje między tak powstałymi klasami. Programiście pomaga to definiować operacje przeprowadzane na danych różnych rodzajów, a mechanizmom języka zarządzać pamięcią i wykrywać niektóre rodzaje błędów. W Clojure mamy do czynienia z kilkoma powiązanymi ze sobą systemami typów, które możemy rozszerzać, a wykorzystując ich polimorficzne mechanizmy jesteśmy w stanie abstrahować zarządzanie danymi i budować ujednolicone interfejsy wymiany informacji.

Systemy typów

Z powodu korzystania z jednowymiarowej pamięci operacyjnej o skończonym rozmiarze program komputerowy musi być w stanie przewidzieć (w czasie kompilacji lub uruchamiania), jak wiele miejsca zarezerwować na określone dane i jaką nadać im postać. Poza tym programista powinien móc na jakiejś podstawie rozróżniać, z jakimi rodzajami informacji ma do czynienia, aby stosować względem reprezentujących je struktur odpowiednio dobrane algorytmy. Aby rozwiązać te kwestie, można wprowadzić klasyfikację rodzajów danych, a każdą wartość przechowywaną w pamięci odpowiednio oznaczać.

Typy danych

Typ danych (ang. data type) to taka klasa wartości, którą cechują pewne właściwości wspólne dla danego rodzaju informacji i sposobów zarządzania nią przez mechanizmy języka programowania. Właściwościami tymi mogą być zakresy, wielkości zajmowanych przestrzeni, sposoby uporządkowania czy reprezentowania danych, a także inne atrybuty przyjęte za wyróżniające w ramach tak zwanego systemu typów.

Typy określają, które operacje i reprezentacje są poprawne dla wartości. Zależnie od języka informacja o typie może być jawna albo wnioskowana, a ograniczenia mogą być sprawdzane przed wykonaniem lub podczas działania programu. Mocne i słabe typizowanie jest czymś innym niż statyczne i dynamiczne sprawdzanie: język mocno typizowany nie musi wymagać adnotacji ani zgłaszać każdej niezgodności podczas kompilacji.

Informacja o typie może również wspierać optymalizację. Kompilator albo JIT może wybrać instrukcję prymitywną, rozpoznać metodę hosta bez refleksji, zwinąć wywołanie lub wyspecjalizować gorący kod maszynowy dla obserwowanych klas. Techniki te zależą od implementacji i kontekstu; polimorficzna funkcja Clojure nie jest automatycznie kompilowana do osobnego wariantu źródłowego dla każdego możliwego typu argumentu.

Charakterystyki systemów typów

System typów (ang. type system) to w językach programowania mechanizm, który dokonuje oznaczania każdej wartości, odniesienia do wartości lub obszaru przechowującego wartość typem danych, a także dba o przestrzeganie reguł typizowania i zachowywania relacji między typami.

Można powiedzieć, że każdemu typowi danych przypisany jest w ramach systemu typów zestaw reguł, które muszą być przestrzegane. Chroni to przed występowaniem błędów w czasie pracy aplikacji i pozwala tworzyć przewidywalne interfejsy wymiany danych między różnymi częściami oprogramowania.

Rodzaje typizowania

Sprawdzanie, czy operacje i reguły systemu typów są stosowane, może odbywać się podczas kompilacji lub w trakcie działania programu. W tym pierwszym przypadku nazwiemy ten proces statycznym typizowaniem (ang. static typing), a w drugim dynamicznym typizowaniem (ang. dynamic typing).

W językach statycznie typizowanych sposobem na zwolnienie programisty z obowiązku określania typu każdego wyrażenia jest inferencja typu (ang. type inference). Polega ona na automatycznym wykrywaniu przez kompilator typów danych poszczególnych wyrażeń i wewnętrznym oznaczaniu ich we właściwy sposób.

Określenia słabo typizowany (ang. weakly typed) i mocno typizowany (ang. strongly typed) są używane niespójnie. Ogólnie opisują łatwość, z jaką język dopuszcza operacje albo niejawne konwersje między niezgodnymi typami; nie mówią, kiedy odbywa się kontrola. Użyteczniej jest więc podać konkretne reguły konwersji i błędów niż wywodzić je z którejś etykiety.

W Clojure wartości mają typy uruchomieniowe, a większość zależnych od wartości kontroli zgodności odbywa się podczas działania programu. Kompilator nadal odrzuca niepoprawne formy, nierozpoznane symbole, niektóre statycznie znane błędne liczby argumentów i część niemożliwych wywołań hosta.

Clojure jest również niejawnie typizowany w praktycznym sensie: deklaracje zwykle nie potrzebują adnotacji typów. Kompilator potrafi śledzić wybrane typy hosta i prymitywne na potrzeby generowania kodu, lecz to lokalne wnioskowanie nie jest całościowym statycznym dowodem kontraktów funkcji.

Hierarchie typów

W obrębie systemu typów mogą być utrzymywane hierarchiczne relacje przynależności. Możemy wtedy mówić o podtypach (ang. subtypes) i nadtypach (ang. supertypes) względem wybranych typów. Tego rodzaju mechanizmy pozwalają nie tylko rozróżniać relacje klas wartości i korzystać z tych informacji do sterowania logiką aplikacji, ale często też bywają zintegrowane z konstrukcjami pozwalającymi operować na danych. Na przykład w językach mocniej typizowanych do funkcji, której typ argumentów z góry określono, możemy przekazywać wartości również ich podtypów. W Javie będą to m.in. metody, które przyjmują argumenty typu Object, określonego klasą będącą przodkiem niemal wszystkich innych klas obiektów (za wyjątkiem samej klasy Object oraz interfejsów). Możemy więc do takich funkcji przekazywać wartości innych typów (np. String czy Integer), które są bezpośrednimi lub pośrednimi podtypami typu Object.

Obsługę podtypów i nadtypów, pozwalającą operować na przekazywanych wartościach w wyżej opisany sposób, nazywamy polimorfizmem podtypowym (ang. subtype polymorphism) lub polimorfizmem inkluzyjnym (ang. inclusion polymorphism). Dzięki niej możemy abstrahować operacje na danych, zachowując relacje między ich określonymi typami i korzystać z tzw. koercji.

Role systemów typów

Podsumowując, możemy wyróżnić następujące funkcje typizowania w zależności od kategorii:

  • Modelowanie dziedziny (semantyka biznesowa):

    • rozróżnianie klas pojęciowych (np. typy CzłowiekRoślina),

    • określanie relacji między klasami wartościkonkretyzowanie
      (np. typ Zwierzę i podtyp Ssak).

  • Budowanie abstrakcji:

    • rozróżnianie danych implementacyjnych (np. typy StringInteger),

    • rozróżnianie interfejsów (np. Seq, Comparable).

  • Kontrakty:

    • precyzowanie wymogów interfejsów odnośnie danych we/wy
      (np. typów argumentów czy wartości zwracanych przez funkcje bądź API),

    • wymuszanie kompletności obsługi przypadków
      (np. na bazie sum typów).

  • Reprezentacja:

    • określanie właściwości struktur danych
      (np. rozmiarów czy sposobów odzwierciedlania wartości w pamięci),

    • kontrola organizowania danych w pamięci
      (np. intlong, struktury wartości a referencje),

    • podpowiedzi dla kompilatora dot. optymalizacji.

  • Weryfikacja poprawności:

    • określanie warunków poprawności
      (np. maksymalnych zakresów wartości czy dopasowania do wzorców),

    • wczesne wykrywanie błędów niezgodności typów i niekompatybilnych właściwości.

  • Dokumentowanie:

    • narzędzia dla IDE
      (np. automatyczne uzupełnianie, refaktoryzacja, nawigacja),

    • generowanie schematów
      (np. OpenAPI czy GraphQL).

Systemy typów w Clojure

Clojure jest językiem niejawniedynamicznie typizowanym. Podpowiedzi typów na symbolach i wyrażeniach mogą pomóc kompilatorowi JVM uniknąć refleksji albo wybrać obsługiwane reprezentacje prymitywne, lecz ani nie walidują dowolnych wartości, ani nie zmieniają funkcji w statycznie sprawdzany kontrakt API.

Na potrzeby tego rozdziału warto rozróżnić trzy współdziałające warstwy związane z typami:

  • obiektowego systemu typów platformy gospodarza,
  • podstawowego systemu typów platformy gospodarza,
  • doraźne hierarchie Clojure, używane przede wszystkim przez multimetody.

Protokoły dodają otwartą dyspozycję według klas hosta, natomiast predykaty, specyfikacje lub schematy wyrażają kontrakty danych, których te trzy warstwy same nie obejmują.

Obiektowy system typów

Platformy uruchomieniowe języka Clojure, takie jak np. JVM czy CLR, są zorientowane obiektowo. W środowiskach takich możemy nie tylko korzystać z wbudowanych, ale też tworzyć nowe typy danych, nazywane czasem typami obiektowymi (ang. object types).

Obiekty

Obiekt (ang. object), czyli wartość określonego typu obiektowego, będziemy nazywali instancją (ang. instance) pewnej klasy.

Obiekt ma tożsamość klasy i może zawierać pola instancji. Metody definiuje jego klasa albo odziedziczone kontrakty; implementacja nie musi przechowywać osobnego odwołania do funkcji w każdym obiekcie.

Klasy

Klasa (ang. class) charakteryzuje typ danych – jest wzorcem służącym do jego zdefiniowania i składa się z:

  • deklaracji zmiennych składowych (ang. member variables), zwanych też polami (ang. fields) lub atrybutami (ang. attributes), w których instancje klas przechowują dane;

  • definicji funkcji składowych (ang. member functions), zwanych też metodami (ang. methods), które są operacjami skojarzonymi z instancjami albo samą klasą.

Klasa łączy kontrakt reprezentacji z metodami i regułami dziedziczenia. Struktura języka C może modelować część reprezentacji, lecz sama nie zapewnia tożsamości klasy, dyspozycji metod, widoczności ani semantyki dziedziczenia JVM.

Dziedziczenie

Dziedziczenie (ang. inheritance) tworzy klasę pochodną z dostępem do odziedziczonych składowych; nie modyfikuje istniejącej klasy bazowej. Na przykład klasa Zwierzę może być klasą bazową względem klasy Ssak. W tej ostatniej nie musimy wtedy definiować właściwości i operacji typowych dla obiektów typu Zwierzę, bo zostaną one odziedziczone. Możemy więc w podklasach dokonywać rozszerzania istniejących klas o nowe metody bądź pola, jak również zastępować istniejące bardziej odpowiednimi wariantami.

Z kolei sposobem na zadbanie o to, aby różne klasy były wyposażone w określone z góry zestawy operacji jest użycie tzw. interfejsów (ang. interfaces). Raz stworzony interfejs, zawierający deklaracje metod, może być potem implementowany przez wiele klas. Na przykład klasa Ssak może implementować interfejs Zwierzęce, który będzie zawierał deklaracje operacji typowych dla zwierząt (np. pobierz_liczbę_łap, ustaw_wagę itd.). Klasa Ssak będzie musiała zdefiniować je wszystkie, ponieważ klasa musi zaimplementować abstrakcyjne metody wymagane kontraktem. Współczesne interfejsy Javy mogą również zawierać implementacje metod domyślnych, statycznych i prywatnych, nie ograniczają się więc do samych deklaracji.

Dowiedzieliśmy się wcześniej o polimorfizmie podtypowym, który jest obecny także w obiektowych systemach typów. Może być on wtedy określony również jako dziedziczenie interfejsów (ang. interface inheritance), natomiast zgodnie z przyjętą nomenklaturą bezpośredni nadtyp danego typu nazwiemy też:

  • klasą bazową (ang. base class),
  • klasą macierzystą (ang. parent class),
  • nadklasą (ang. superclass);

a bezpośredni podtyp:

  • klasą pochodną (ang. derived class),
  • klasą potomną (ang. child class),
  • podklasą (ang. subclass).

Za podtypy będziemy również uważali wszystkie klasy potomne, zwane czasem potomkami (ang. descendants), a za nadtypy wszystkie klasy nadrzędne, zwane przodkami (ang. ancestors).

W przypadku Javy przodkiem wszystkich klas jest java.lang.Object.

Interfejsy

Interfejs (ang. interface) to zbiór deklaracji metod o wspólnym przeznaczeniu. Możemy traktować interfejs jak kontrakt między klasą, a resztą programu, w którym klasa przez obietnicę zaimplementowania interfejsu deklaruje, że określony nim zestaw zdolności znajdzie odzwierciedlanie w zdefiniowanych w niej metodach.

Klasa może implementować więcej niż jeden interfejs i musi dostarczyć abstrakcyjne metody, których nie zapewnia już nadklasa albo metoda domyślna interfejsu. Raz skompilowana klasa Javy nie uzyskuje nowego interfejsu Javy tylko dlatego, że życzy sobie tego kod klienta; rozszerzanie protokołów Clojure jest odrębnym, otwartym mechanizmem.

Przykład interfejsu w Javie
interface IPoliczalne {
  int zlicz();
}

class Napis implements IPoliczalne {
  String wartość;
  public int zlicz() {
    return wartość.length();
  }
}
interface IPoliczalne { int zlicz(); } class Napis implements IPoliczalne { String wartość; public int zlicz() { return wartość.length(); } }

Gdybyśmy na przykład stworzyli interfejs IPoliczalne, w którym zadeklarowana byłaby metoda zlicz służąca do kalkulowania liczby elementów jakiejś abstrakcyjnej kolekcji, każda klasa, którą oznaczylibyśmy jako implementującą IPoliczalne, musiałaby być wyposażona w metodę zlicz. W przypadku klasy odpowiedzialnej za reprezentowanie łańcuchów znakowych mogłaby ona obliczać liczbę jednostek kodowych UTF-16 składających się na przechowywany w egzemplarzu napis, w przypadku tablic liczbę ich elementów itd. W programie moglibyśmy następnie sprawdzać, czy dana klasa implementuje nasz interfejs i w ten sposób spodziewać się, że będzie zawierała określoną kontraktem metodę zliczającą.

W Clojure interfejsy hosta są istotne podczas współpracy z API Javy, definiowania sygnatur metod prymitywnych albo wystawiania kontraktu bezpośrednio dla Javy. Dla otwartych abstrakcji należących do Clojure protokoły są często elastyczniejszym wyborem.

Definiowanie interfejsów, definterface

W Clojure definterface może zdefiniować i załadować interfejs Javy podczas zwykłego ładowania kodu. Jego sygnatury metod abstrakcyjnych muszą zostać zaimplementowane przez klasy konkretne.

Protokoły uzupełniają interfejsy Javy, nie zaś powszechnie je zastępują. Wybór zależy od granicy: protokół służy otwartej dyspozycji Clojure, interfejs — kontraktowi hosta albo wymaganiu interoperacyjności.

Aby wytworzyć nowy interfejs Javy, możemy skorzystać z makra definterface.

Użycie:

  • (definterface nazwa sygnatura…);

Nazwa powinna być niezacytowanym symbolem, natomiast sygnatury listowymi S-wyrażeniami w postaci:

  • (nazwa-metody [parametr…]) lub
  • (^typ-zwracany nazwa-metody [^typ-parametru parametr…]).

Metody interfejsu Javy mają ustaloną liczbę argumentów; wiele wektorów parametrów deklaruje przeciążenia, a nie metodę wariadyczną Clojure.

Generowanie interfejsów, gen-interface

Do niskopoziomowego generowania interfejsów Javy w trybie AOT służy makro gen-interface. Podczas kompilacji zapisuje kod bajtowy interfejsu o kwalifikowanej pakietem nazwie do katalogu wskazywanego przez *compile-path*; poza kompilacją nie robi nic.

Użycie:

  • (gen-interface & opcja…).

Protokoły

Protokoły Clojure (ang. protocols) opisują nazwane operacje z dyspozycją czasu wykonania według klasy. Na JVM protokół generuje interfejs oraz Vary i mechanizmy dyspozycji, lecz jego semantyka rozszerzania jest bardziej otwarta niż deklaracja Javy implements.

Interfejs Javy wybierzemy, gdy wymaga go zewnętrzne API, gdy przeciążenia albo sygnatury metod prymitywnych należą do kontraktu hosta lub gdy javowi klienci mają widzieć właśnie ten typ. Protokół wybierzemy, gdy zasadniczą potrzebą jest niezależne rozszerzanie operacji należących do Clojure na istniejące klasy.

Możemy postrzegać protokoły jako jeden ze sposobów na to, aby móc rozszerzać istniejące funkcje o obsługę nowych typów danych bez konieczności ingerowania w stworzony wcześniej kod. Jest to mechanizm polimorfizmu dynamicznego, który rozwiązuje otwartą stronę tzw. problemu wyrazu.

W protokole deklarujemy zestaw funkcji, z którego każda musi być potem zdefiniowana w wariancie dla każdego obiektowego typu danych implementującego dany protokół. Gdy dojdzie do wywołania takiej polimorficznej funkcji, wtedy typ danych wartości przekazywanej jako jej pierwszy argument zdecyduje o tym, do której konkretnie implementacji funkcji zostanie skierowane wywołanie.

Tworzone protokoły można przypisywać do typów danych – należy wtedy od razu zdefiniować zadeklarowane funkcje. Można też wzbogacać nowo tworzone typy danych o implementacje protokołów, a nawet rozszerzać istniejące typy o zgodność z wybranymi protokołami.

Zobacz także:

Definiowanie protokołów, defprotocol

Aby utworzyć nowy protokół, możemy skorzystać z makra defprotocol.

Użycie:

  • (defprotocol nazwa łańcuch-dok? & opcja… & sygnatura…).

Podawana jako pierwszy argument nazwa protokołu powinna być niezacytowanym symbolem, który stanie się nazwą tworzonego interfejsu (w przypadku platformy JVM). Po nazwie można umieścić opcjonalny łańcuch dokumentujący wyrażony łańcuchem znakowym w postaci literału ujętego w znaki cudzysłowu. Kolejne argumenty makra to opcje i deklaracje polimorficznych funkcji wyrażone sygnaturami zbudowanymi na bazie listowych S-wyrażeń.

Przykład protokołu w Clojure
(defprotocol Policzalne
  (zlicz [this]))

(extend-type java.lang.String
  Policzalne
    (zlicz [s] (count s)))

(zlicz "abc")
; => 3
(defprotocol Policzalne (zlicz [this])) (extend-type java.lang.String Policzalne (zlicz [s] (count s))) (zlicz "abc") ; => 3

Makro defprotocol wymaga, aby deklarowane metody zawsze specyfikowały pierwszy argument, który będzie służył do przekazywania wartości dopasowanej do wykrytego typu danych podczas podejmowania decyzji o tym, jaki wariant funkcji wywołać.

Dokładny opis użycia makra defprotocol można znaleźć w rozdziale poświęconym polimorfizmowi.

Tworzenie typów

Tworzenie nowych typów obiektowych polega na definiowaniu klas, które będą określały ich właściwości. W Clojure możemy korzystać z pewnych abstrakcyjnych konstrukcji, dzięki którym fakt generowania klas systemu gospodarza jest przed nami ukrywany, chociaż istnieją też odpowiednie makra pozwalające na bliską integrację z mechanizmami Javy i bardziej precyzyjne wpływanie na budowę klas.

Definiowanie typów, deftype

Makro deftype definiuje nazwaną klasę JVM z żądanymi polami oraz metodami protokołów i interfejsów. Jej pakiet wynika z bieżącej przestrzeni nazw, a kod bajtowy jest generowany dynamicznie, dzięki czemu typ jest dostępny zarówno podczas pracy interaktywnej, jak i kompilacji AOT.

Podczas kompilacji AOT wygenerowane pliki klas są zapisywane pod katalogiem wskazanym przez *compile-path*, zgodnie z konwencją odwzorowania pakietu JVM na ścieżkę.

W instancjach tworzonego z użyciem makra deftype typu danych zawarte będą określone przez programistę pola. Będzie też można korzystać z metod zadeklarowanych w implementowanych protokołach Clojure oraz interfejsach Javy, a zdefiniowanych w wywołaniu makra.

Zbiór pól instancji deftype jest ustalony przez klasę. W odróżnieniu od rekordu deftype nie implementuje trwałej mapy i nie może otrzymać dodatkowych wpisów asocjacyjnych przez assoc.

Użycie:

  • (deftype nazwa [pole…] opcja-lub-specyfikacja…).

Pierwszym argumentem makra powinna być nazwa typu, a kolejnymi (opcjonalnymi) nazwy pól, które mogą zawierać sugestie typów.

Wyrażane symbolami pola można opcjonalnie wyposażyć w metadane, które mają wpływ na ich obsługę:

  • :volatile-mutable
    – ustawienie wartości true (lub użycie ^:volatile-mutable) sprawi, że pole będzie mutowalne z semantyką widoczności javowego volatile;

  • :unsynchronized-mutable
    – ustawienie wartości true (lub użycie ^:unsynchronized-mutable) sprawi, że pole będzie mutowalne bez gwarancji synchronizacji ani widoczności volatile.

Pola są domyślnie końcowe. Oba znaczniki mutowalności pozwalają przypisywać pole z metod typu; nie czynią mutowalną niemutowalnej wartości przechowywanej w polu. Należy ich używać tylko wtedy, gdy wymagany kontrakt mutacji i współbieżności jest jawny.

Po nazwach pól możemy podać opcje i tzw. specyfikacje (ang. skr. specs). Te ostatnie są istotne podczas implementowania metod określonych protokołami lub interfejsami platformy gospodarza.

Każda specyfikacja składa się z nazwy protokołu lub interfejsu platformy gospodarza, po której następują implementacje metod potrzebne do spełnienia tego kontraktu. Jest to wykorzystywane do tworzenia polimorficznych operacji, o których można przeczytać w rozdziale XXI.

Każda opcja powinna być parą klucz wartość, a obecnie jedynym użytecznym kluczem jest :load-ns. Jeżeli skojarzymy go z wartością true, wtedy importowanie klasy definiującej typ spowoduje, że załadowana zostanie przestrzeń nazw, w której ów typ został zdefiniowany.

Do nowo powstałego typu dodany będzie również konstruktor, który pozwala inicjować tworzone obiekty wartościami pól. Poza tym zdefiniowana zostanie wywołująca go funkcja:

  • (->nazwa wartość…).

gdzie nazwa jest nazwą utworzonego typu, wartość wartością pola. Pozwala ona tworzyć obiekty przez podanie wartości pól wyrażonych pozycyjnie.

Zarówno wygenerowany konstruktor, jak i funkcja ->typ są obsługiwane. Funkcja bywa wygodniejsza w Clojure i łatwiej przekazać ją jako wartość; bezpośrednia składnia konstruktora jest właściwa na jawnych granicach współpracy z JVM.

W efekcie pracy makra zdefiniowana zostanie klasa o podanych polach i metodach.

Przykład użycia makra deftype
 1(deftype Osoba [imię nazwisko wiek])
 2; => user.Osoba
 3
 4;; tworzenie instancji klasy Osoba przez wywołanie konstruktora
 5
 6(def Paweł (Osoba. "Paweł" "Wilk" 18))
 7
 8;; tworzenie instancji klasy Osoba przez wywołanie funkcji ->
 9
10(def Paweł (->Osoba "Paweł" "Wilk" 18))
11
12;; sprawdzanie typów
13
14(type Osoba)       ; => java.lang.Class
15(type Paweł)       ; => user.Osoba
16
17;; wywoływanie akcesorów pól
18
19(.imię     Paweł)  ; => "Paweł"
20(.nazwisko Paweł)  ; => "Wilk"
(deftype Osoba [imię nazwisko wiek]) ; => user.Osoba ;; tworzenie instancji klasy Osoba przez wywołanie konstruktora (def Paweł (Osoba. "Paweł" "Wilk" 18)) ;; tworzenie instancji klasy Osoba przez wywołanie funkcji -> (def Paweł (->Osoba "Paweł" "Wilk" 18)) ;; sprawdzanie typów (type Osoba) ; => java.lang.Class (type Paweł) ; => user.Osoba ;; wywoływanie akcesorów pól (.imię Paweł) ; => "Paweł" (.nazwisko Paweł) ; => "Wilk"
Tworzenie obiektów, ->typ

Obiekty typu zdefiniowanego przez deftype można tworzyć bezpośrednią składnią konstruktora JVM albo wygenerowaną funkcją ->typ, gdzie typ jest nazwą typu.

Użycie:

  • (->typ pole…).

Argumentami wywołania funkcji powinny być wartości kolejnych pól definiowanych przez typ obiektu. Wartością zwracaną jest obiekt zdefiniowanego typu.

Przykład użycia funkcji ->typ
1(deftype Osoba [imię nazwisko wiek])
2
3(instance? Osoba (->Osoba "Paweł" "Wilk" 18))
4; => true
(deftype Osoba [imię nazwisko wiek]) (instance? Osoba (->Osoba "Paweł" "Wilk" 18)) ; => true
Definiowanie rekordów, defrecord

Makro defrecord pozwala definiować tzw. rekordowe typy danych, które są nowymi typami platformy gospodarza służącymi do przechowywania informacji w postaci rekordów (ang. records).

Instancje rekordów zapewniają pełny interfejs trwałej mapy. Każde zadeklarowane pole jest dostępne pod kluczem będącym słowem kluczowym, przy zachowaniu wydajnego przechowywania opartego na polu.

Z rekordem można asocjować dodatkowe wpisy klucz–wartość. Trafiają one do trwałej mapy rozszerzeń; nie stają się nowymi polami JVM ani nie mutują istniejącej instancji.

Użycie:

  • (defrecord nazwa [pole…] opcja-lub-specyfikacja…).

Pierwszym argumentem makra powinna być nazwa typu rekordowego, a kolejnymi (opcjonalnymi) nazwy pól. Po nazwach pól możemy podać opcje i tzw. specyfikacje (ang. skr. specs).

Opis użycia makra defrecord można znaleźć w rozdziale poświęconym kolekcjom.

Generowanie klas, gen-class

Gdy pojawia się konieczność utworzenia nowej, „czystej” klasy Javy, możemy skorzystać z makra gen-class. Sprawia ono, że podczas kompilacji generowany jest kod bajtowy JVM dla klasy o podanej nazwie, która powinna być w pełni kwalifikowaną nazwą pakietu Javy. W katalogu systemu plikowego, którego nazwa znajduje się w zmiennej dynamicznej *compile-path* umieszczony zostanie plik z rozszerzeniem class.

Gdy program nie jest kompilowany (chodzi o kompilację AOT), to gen-class nie wywoła żadnego efektu.

Z użyciem gen-class będziemy mogli tworzyć klasy Javy, lecz z ograniczeniami (m.in. bez mutowalnych, publicznych pól). Wynika to z przeznaczenia gen-class oraz innych konstrukcji tego typu. Służą one przede wszystkim do zapewniania interoperacyjności z Javą, a nie do generowania wszystkich możliwych konstrukcji tego języka. Na przykład gen-class zastosowujemy po to, aby kod bajtowy pochodzący z Clojure integrować z bibliotekami Javy, które będą chciały odwoływać się bądź rozszerzać tak wyeksponowane klasy lub ich instancje.

Najpowszechniejszym przypadkiem użycia makra gen-class jest wykorzystanie go do wygenerowania klasy wyposażonej w publiczną, statyczną metodę main, której implementację można zdefiniować w postaci funkcji języka Clojure, np.: (defn -main [] (println "hej")). Dzięki temu daje się budować samodzielne aplikacje, które łatwo uruchamiać. Z podobnymi konstrukcjami spotkamy się też, gdy program w Clojure będzie przeznaczony do obsługi żądań w modelu serwletowym.

Użycie:

  • (gen-class & opcja…).

Możliwe do zastosowania opcje to pary klucz–wartość, w których klucze są słowami kluczowymi, a wartości różnymi typami, zależnymi od konkretnych opcji:

  • :name nazwa
    – nazwa klasy Javy;

  • :extends klasa
    – nazwa nadklasy, której publiczne metody zostaną nadpisane;

  • :implements [interfejs…]
    – nazwy interfejsów, których metody będą zdefiniowane;

  • :init konstruktor
    – nazwa funkcji inicjalizującej, która zwraca argumenty konstruktora nadklasy i opcjonalny stan instancji;

  • :constructors {[typy-parametrów] [typy-parametrów-nadklasy] …}
    – sygnatury konstruktorów, jeżeli odmienne od odziedziczonych;

  • :post-init wyzwalacz
    – nazwa funkcji, która zostanie wywołana podczas instancjonowania;

  • :methods [ [nazwa [typy-parametrów] typ-zwracany] …]
    – sygnatury dodatkowych metod, które znajdą się w klasie;

  • :main przełącznik
    – gdy przełącznik jest true, wygenerowana zostanie statyczna funkcja main;

  • :factory fabrykator
    – tworzy statyczne funkcje fabrykujące o podanej nazwie;

  • :state pole
    – tworzy publiczne, finalne pole o podanej nazwie;

  • :exposes {pole-chronione {:get nazwa :set nazwa} …}
    – generuje gettery i settery dla chronionych pól nadklasy;

  • :exposes-methods {nazwa-metody-nadklasy alias …}
    – udostępnia metody nadklasy pod lokalnymi aliasami;

  • :prefix przedrostek
    – ustawia przedrostek dla nazw funkcji Clojure implementujących metody;

  • :impl-ns przestrzeń-nazw
    – przestrzeń nazw z obiektami Var wskazującymi implementacje metod;

  • :load-impl-ns przełącznik
    – gdy true, wygenerowana klasa ładuje swoją przestrzeń implementacji ze statycznego inicjalizatora.

Wygenerowana klasa zawiera rzeczywiste metody Javy. Ich ciała odnajdują i wywołują Vary w przestrzeni nazw implementacji, wyprowadzając nazwę każdej funkcji implementującej z ustalonego przedrostka albo domyślnego -.

Przykład użycia gen-class
 1(ns pl.randomseed.Czlowiek)
 2
 3;; tworzymy klasę Javy
 4
 5(gen-class
 6 :name         pl.randomseed.Czlowiek
 7 :main         true
 8 :prefix       "-czlowiek-"
 9 :methods      [[witaj [String] String]])
10
11;; tworzymy implementacje metod
12
13(defn -czlowiek-witaj [_ imię]
14  (str "Witaj, " imię))
15
16(defn -czlowiek-main [argumenty]
17  (println (-czlowiek-witaj nil (or (first argumenty) "świecie"))))
(ns pl.randomseed.Czlowiek) ;; tworzymy klasę Javy (gen-class :name pl.randomseed.Czlowiek :main true :prefix "-czlowiek-" :methods [[witaj [String] String]]) ;; tworzymy implementacje metod (defn -czlowiek-witaj [_ imię] (str "Witaj, " imię)) (defn -czlowiek-main [argumenty] (println (-czlowiek-witaj nil (or (first argumenty) "świecie"))))

Aby skompilować powyższy przykład należy umieścić zaprezentowany kod źródłowy w odpowiednim pliku:

SH
 1mkdir -p /tmp/gen-class-przyklad/src/pl/randomseed
 2mkdir /tmp/gen-class-przyklad/classes
 3cd /tmp/gen-class-przyklad
 4
 5# edycja pliku
 6edit src/pl/randomseed/Czlowiek.clj
 7
 8# kompilacja
 9clojure -Sdeps '{:paths ["src"]}' -M \
10  -e "(binding [*compile-path* \"classes\"] (compile 'pl.randomseed.Czlowiek))"
mkdir -p /tmp/gen-class-przyklad/src/pl/randomseed mkdir /tmp/gen-class-przyklad/classes cd /tmp/gen-class-przyklad # edycja pliku edit src/pl/randomseed/Czlowiek.clj # kompilacja clojure -Sdeps '{:paths ["src"]}' -M \ -e "(binding [*compile-path* \"classes\"] (compile 'pl.randomseed.Czlowiek))"

Zobacz także:

Generowanie pośredniczących, proxy

Podobnym do gen-class makrem jest proxy. Również dokonuje ono wygenerowania nowej klasy, jednak z trzema istotnymi różnicami:

  • klasa rezyduje w pamięci
    (a nie tylko w pliku .class podczas kompilacji AOT);

  • klasa nie ma publicznej nazwy,
    do której można by się potem odwołać bez uciekania się do pewnych sztuczek;

  • klasa jest natychmiast instancjonowana
    (wartością zwracaną przez makro jest obiekt).

Do klas pośredniczących (ang. proxy) nie możemy dodawać nowych metod. Służą one do „opakowywania” istniejących klas i przesłaniania istniejących tam metod, albo do definiowania metod zadeklarowanych wcześniej (w przypadku interfejsów). Definiowane w konstrukcji proxy metody nie są metodami klasy, lecz funkcjami, do których wywołania metod anonimowej klasy są przekierowywane.

Makra proxy użyjemy najczęściej w celu szybkiego zapewnienia kompatybilności z integrowanymi bibliotekami Javy.

Użycie:

  • (proxy [klasa-lub-interfejs…] [argument…] & definicja…),

gdzie:

  • [klasa-lub-interfejs…] jest wektorowym S-wyrażeniem z nazwami klas i/lub interfejsów;

  • [argument…] jest wektorowym S-wyrażeniem z argumentami przekazywanymi do konstruktora nadklasy;

  • definicja… jest listowym S-wyrażeniem definiującym funkcje.

Przykład użycia makra proxy
;; definiowanie funkcji,
;; która wyprodukuje obiekt "opakowany" w proxy

(defn numeryczna [wartość]
  (proxy [java.lang.Number] []                ; proxy dla nadklasy liczb
    (toString [] (str "liczba: " wartość))))  ; implementacja metody toString

;; definiowanie zmiennej z wartością

(def x (numeryczna 123))

;; wyświetlanie wartości
;; (przeciążona metoda toString zwraca liczbę z przedrostkiem)

x
; => liczba: 123
;; definiowanie funkcji, ;; która wyprodukuje obiekt "opakowany" w proxy (defn numeryczna [wartość] (proxy [java.lang.Number] [] ; proxy dla nadklasy liczb (toString [] (str "liczba: " wartość)))) ; implementacja metody toString ;; definiowanie zmiennej z wartością (def x (numeryczna 123)) ;; wyświetlanie wartości ;; (przeciążona metoda toString zwraca liczbę z przedrostkiem) x ; => liczba: 123

Zauważmy, że wykorzystywana w naszej implementacji funkcja str wewnętrznie również odwołuje się do toString, ale w oryginalnej implementacji. Poza tym warto wiedzieć, że proxy domyka wszystkie leksykalne powiązania, które znajdują się w leksykalnym otoczeniu jej wywołania.

Operacja arytmetyczna na x wywołałaby ostatecznie jedną z abstrakcyjnych metod konwersji numerycznej klasy Number. Nasz obiekt pośredniczący nie implementuje żadnej z nich, więc wygenerowany mechanizm zastępczy rzuci UnsupportedOperationException. Odziedziczone metody konkretne nadal są dostępne; błąd wynika z niespełnionego kontraktu abstrakcyjnego.

W funkcjach implementujących metody możemy odwoływać się również do oryginalnych odpowiedników z klasy przodka. Służy do tego funkcja proxy-super.

Zobacz także:

Konkretyzowanie, reify

Makro reify tworzy instancję anonimowej klasy implementującej wybrane interfejsy Javy i protokoły Clojure. Jest zwykle mniejszą konstrukcją, gdy nie potrzebujemy konstruktora nadklasy ani dynamicznej mapy metod obiektu pośredniczącego.

Dzięki reify możemy tworzyć obiekty, które zachowują się zgodnie ze specyfikacją określoną podanymi wzorcami. W tym celu tworzona jest anonimowa klasa implementująca definiowane metody. Dzięki temu można szybko wytworzyć jednorazową instancję potrzebnego typu zmodyfikowaną pod względem zachowania w odpowiadający nam sposób.

Użycie:

  • (reify opcja… specyfikacja…).

Każda z podanych specyfikacji powinna składać się z nazwy protokołu, interfejsu, lub klasy Object, a także listowych S-wyrażeń zawierających definicje metod:

nazwa-wzorca  
(nazwa-metody [argument…] ciało) …

Definiowane metody domykają wartości powiązań z leksykalnego otoczenia.

Różnice w stosunku do makra proxy są następujące:

  • Definiowane metody stają się metodami wygenerowanej klasy anonimowej i mogą domykać wartości leksykalne. Ich wydajność nadal zależy od ścieżki wywołania, kompilatora i JVM; reify wybieramy przede wszystkim ze względu na kontrakt i zasięg, a nie ogólną obietnicę szybkości.

  • W związku z powyższym nie można dokonywać dynamicznego podmieniania metod.

  • Konkretyzować możemy wyłącznie interfejsy bądź protokoły, nie klasy.

  • Definicje metod powinny przyjmować jako pierwszy argument wartość obiektu własnego.

Spróbujmy zobaczyć reify w akcji, posługując się przykładem ze wzbogacaniem typu danych o cechę policzalności.

Przykład użycia makra reify
(defn numeryczna [wartość]
  (reify
    Object
    (toString [this] (str "liczba: " wartość))))

(str (numeryczna 3))

;; powiązujemy wartość numeryczną z x

(def x (numeryczna 123))

;; wyświetlamy wartość numeryczną
;; przekształconą do łańcucha znakowego
;; z użyciem metody toString
;; w naszej własnej wersji

(str x)
; => liczba: 123
(defn numeryczna [wartość] (reify Object (toString [this] (str "liczba: " wartość)))) (str (numeryczna 3)) ;; powiązujemy wartość numeryczną z x (def x (numeryczna 123)) ;; wyświetlamy wartość numeryczną ;; przekształconą do łańcucha znakowego ;; z użyciem metody toString ;; w naszej własnej wersji (str x) ; => liczba: 123

W powyższym przykładzie możemy zaobserwować, że doszło do domknięcia wartości argumentu funkcji wartość w metodzie anonimowej klasy wytworzonej przez makro reify. Wyprodukowany obiekt, gdy odwoła się do metody toString będzie korzystał z jej zmienionej wersji, w której wartością jest zawsze ta podana podczas wywołania funkcji numeryczna, czyli wygenerowania obiektu.

Rozszerzanie typów

Nadtypami istniejących typów obiektowych nazwiemy nie tylko typy danych zdefiniowane klasami, po których klasy definiujące typy dziedziczą, ale również implementowane interfejsy i protokoły.

Deklarować, że dany typ implementuje protokół, możemy również po tym, gdy dany typ został zdefiniowany. Gdy mamy już protokół, który jest (bądź nie) implementowany przez jakieś typy danych, możemy pokusić się o rozszerzenie o jego obsługę nowych typów. Będzie to polegało na skojarzeniu typu z protokołem i zdefiniowaniu wymaganych funkcji obsługi.

Aby dokonać rozszerzenia typów o obsługę protokołów możemy skorzystać z jednej z trzech form:

Działają one podobnie, lecz wybór konkretnej będzie zależał od tego, czy mamy wiele typów danych, które chcemy wzbogacić o implementację jakiegoś protokołu (extend-protocol), czy może mamy jeden typ, który pragniemy rozszerzyć (extend-type). Obydwa makra korzystają z funkcji extend, która działa podobnie do pierwszego wymienionego, lecz wymaga korzystania z map, w których kluczami nazywającymi definiowane funkcje są słowa kluczowe, a wartościami obiekty funkcyjne.

Dokładny opis użycia funkcji extend i makr extend-type oraz extend-protocol można znaleźć w rozdziale poświęconym polimorfizmowi.

Testowanie typów

Każda wartość Clojure na JVM poza nil ma uruchomieniową klasę Javy. Klasy z java.lang są importowane do każdej przestrzeni nazw, inne można importować albo nazywać w pełni, a Clojure definiuje dodatkowe klasy w clojure.lang. Wartość nil odpowiada javowemu null i nie ma klasy.

Spójrzmy na przykładowe literały i obiektowe typy danych odpowiadające reprezentowanym przez nie wartościom:

  •    "a"java.lang.String,
  •     \ajava.lang.Character,
  •   truejava.lang.Boolean,
  •    123java.lang.Long,
  •   123Mjava.math.BigDecimal,
  •   123Nclojure.lang.BigInt,
  •     :aclojure.lang.Keyword,
  •     'aclojure.lang.Symbol,
  •   '(1)clojure.lang.PersistentList,
  •    [1]clojure.lang.PersistentVector,
  • {:a 2}clojure.lang.PersistentArrayMap.
Badanie klasy, class

Funkcja class zwraca klasę podanego obiektu.

Użycie:

  • (class wartość).

Argumentem wywołania funkcji powinna być dowolna wartość, a wartością zwracaną będzie klasa.

Przykłady użycia funkcji class
 1(class 1)        ; => java.lang.Long
 2(class "abc")    ; => java.lang.String
 3(class \a)       ; => java.lang.Character
 4(class nil)      ; => nil
 5(class true)     ; => java.lang.Boolean
 6(class false)    ; => java.lang.Boolean
 7(class (fn []))  ; => klasa funkcji o nazwie zależnej od implementacji
 8(class [])       ; => clojure.lang.PersistentVector
 9(class :a)       ; => clojure.lang.Keyword
10(class 'a)       ; => clojure.lang.Symbol
11
12(class '^{:type ::Coś} a)
13; => clojure.lang.Symbol
(class 1) ; => java.lang.Long (class "abc") ; => java.lang.String (class \a) ; => java.lang.Character (class nil) ; => nil (class true) ; => java.lang.Boolean (class false) ; => java.lang.Boolean (class (fn [])) ; => klasa funkcji o nazwie zależnej od implementacji (class []) ; => clojure.lang.PersistentVector (class :a) ; => clojure.lang.Keyword (class 'a) ; => clojure.lang.Symbol (class '^{:type ::Coś} a) ; => clojure.lang.Symbol

Zwróćmy uwagę na różnicę w wartości zwracanej w ostatnim wyrażeniu względem przykładu wywołania type.

Asercja typu, cast

Funkcja cast służy do warunkowania dalszego wykonywania programu w zależności od tego, czy mamy do czynienia z podanym typem lub jednym z jego podtypów.

Użycie:

  • (cast typ wartość).

Pierwszym argumentem funkcji powinien być typ danych, a drugim wartość poddawana sprawdzaniu. Jeżeli typ wartości jest podanym typem lub jednym z jego podtypów (jedną z klas pochodnych lub implementacji interfejsu), zwracana jest podana wartość. W przeciwnym przypadku zgłaszany jest wyjątek java.lang.ClassCastException.

Przykłady użycia funkcji cast
 1(cast clojure.lang.Keyword :a)
 2; => :a
 3
 4(cast Object :a)
 5; => :a
 6
 7(cast String :a)
 8; >> java.lang.ClassCastException:
 9; >> Cannot cast clojure.lang.Keyword to java.lang.String
10
11(cast Long (int 5))
12; >> java.lang.ClassCastException:
13; >> Cannot cast java.lang.Integer to java.lang.Long
(cast clojure.lang.Keyword :a) ; => :a (cast Object :a) ; => :a (cast String :a) ; >> java.lang.ClassCastException: ; >> Cannot cast clojure.lang.Keyword to java.lang.String (cast Long (int 5)) ; >> java.lang.ClassCastException: ; >> Cannot cast java.lang.Integer to java.lang.Long

cast wykonuje sprawdzane rzutowanie referencji JVM; nie konwertuje wartości. Ostatnie wyrażenie kończy się więc błędem, zamiast zmienić IntegerLong.

Predykat klasowości, class?

Funkcja class? służy do sprawdzania, czy mamy do czynienia z klasą.

Użycie:

  • (class? wartość).

Funkcja zwraca true, jeżeli podana wartość jest klasą. W przeciwnym przypadku zwraca false.

Przykłady użycia funkcji class?
1(class? 1)                    ; => false
2(class? "abc")                ; => false
3(class? nil)                  ; => false
4(class? true)                 ; => false
5(class? Long)                 ; => true
6(class? java.lang.Long)       ; => true
7(class? clojure.lang.Symbol)  ; => true
(class? 1) ; => false (class? "abc") ; => false (class? nil) ; => false (class? true) ; => false (class? Long) ; => true (class? java.lang.Long) ; => true (class? clojure.lang.Symbol) ; => true
Predykat instancyjności, instance?

Funkcja instance? służy do sprawdzania, czy obiekt reprezentujący wartość jest instancją klasy lub interfejsu o podanej nazwie.

Użycie:

  • (instance? klasa wartość).

Funkcja zwraca true, jeżeli podana jako drugi argument wartość jest instancją klasy przekazanej jako pierwszy argument. W przeciwnym przypadku zwraca false.

Sprawdzone będą wszystkie interfejsy, które implementuje podany obiekt, a także wszystkie jego klasy nadrzędne w ścieżce dziedziczenia.

Przykłady użycia funkcji instance?
1(instance? Long       1)  ; => true
2(instance? Integer    1)  ; => false
3(instance? Number     1)  ; => true
4(instance? Object     1)  ; => true
5(instance? String "abc")  ; => true
6
7(instance? java.lang.Number      1)  ; => true
8(instance? clojure.lang.Keyword :a)  ; => true
9(instance? clojure.lang.IFn     :a)  ; => true
(instance? Long 1) ; => true (instance? Integer 1) ; => false (instance? Number 1) ; => true (instance? Object 1) ; => true (instance? String "abc") ; => true (instance? java.lang.Number 1) ; => true (instance? clojure.lang.Keyword :a) ; => true (instance? clojure.lang.IFn :a) ; => true
Predykat dziedziczenia, isa?

Funkcja isa? pozwala sprawdzić, czy podany typ jest bezpośrednim lub pośrednim podtypem innego typu.

Użycie:

  • (isa? potomek rodzic).

Pierwszym przyjmowanym argumentem jest klasa potomna, a drugim klasa macierzysta.

Wartością zwracaną będzie true, jeżeli podana klasa pochodna jest bezpośrednim lub pośrednim potomkiem klasy macierzystej. W przeciwnym razie zwracaną wartością będzie false.

Zbadane będą wszystkie nadklasy podanej klasy potomnej, a także wszystkie ich interfejsy.

Przykład użycia funkcji isa?
1(isa? String  Object)                           ; => true
2(isa? Class   Object)                           ; => true
3(isa? Integer Number)                           ; => true
4(isa? clojure.lang.Keyword java.lang.Runnable)  ; => true
5
6(derive clojure.lang.Associative ::kolekcja)
7(isa?   clojure.lang.Associative ::kolekcja)
8; => true
(isa? String Object) ; => true (isa? Class Object) ; => true (isa? Integer Number) ; => true (isa? clojure.lang.Keyword java.lang.Runnable) ; => true (derive clojure.lang.Associative ::kolekcja) (isa? clojure.lang.Associative ::kolekcja) ; => true

Zwróćmy uwagę, że isa? umożliwia też badanie relacji typów obiektowych do (omówionych dalej) typów znacznikowych, gdy zostały one określone hierarchią. Funkcji można używać również w odniesieniu do typów znacznikowych.

Nadklasy i interfejsy, supers

Funkcja supers służy do sprawdzania nadklas (zwanych też superklasami, ang. superclasses) oraz interfejsów podanej klasy.

Użycie:

  • (supers klasa).

Funkcja zwraca zbiór zawierający bezpośrednią i wszystkie pośrednie nadklasy, a także wszystkie interfejsy, które implementuje podana jako argument klasa lub podany jako argument interfejs.

Przykłady użycia funkcji supers
 1(contains? (supers Long) Number)               ; => true
 2(contains? (supers Long) Object)               ; => true
 3(contains? (supers Long) java.io.Serializable) ; => true
 4
 5(contains? (supers clojure.lang.IFn)
 6           java.util.concurrent.Callable)      ; => true
 7
 8(supers nil)
 9; => nil
10
11(contains? (supers java.lang.Boolean) Object)     ; => true
12(contains? (supers (class :a)) clojure.lang.IFn) ; => true
(contains? (supers Long) Number) ; => true (contains? (supers Long) Object) ; => true (contains? (supers Long) java.io.Serializable) ; => true (contains? (supers clojure.lang.IFn) java.util.concurrent.Callable) ; => true (supers nil) ; => nil (contains? (supers java.lang.Boolean) Object) ; => true (contains? (supers (class :a)) clojure.lang.IFn) ; => true

Pełne zbiory mogą różnić się między wydaniami Javy, gdy standardowe klasy zyskują nowe interfejsy. Test powinien potwierdzać potrzebną relację, a nie zamrażać cały wydrukowany zbiór.

Nadklasa i interfejsy, bases

Funkcja bases służy do sprawdzania nadklasy oraz bezpośrednich interfejsów podanej klasy.

Użycie:

  • (bases klasa).

Funkcja zwraca sekwencję zawierającą klasę bazową, a także wszystkie bezpośrednie interfejsy, które implementuje podana jako argument klasa (mogąca również być interfejsem).

Przykłady użycia funkcji bases
 1(contains? (set (bases Long)) Number)     ; => true
 2(contains? (set (bases Long)) Comparable) ; => true
 3
 4(contains? (set (bases clojure.lang.IFn))
 5           java.util.concurrent.Callable) ; => true
 6
 7(bases nil)
 8; => nil
 9
10(contains? (set (bases java.lang.Boolean)) Object)     ; => true
11(contains? (set (bases (class :a))) clojure.lang.IFn) ; => true
(contains? (set (bases Long)) Number) ; => true (contains? (set (bases Long)) Comparable) ; => true (contains? (set (bases clojure.lang.IFn)) java.util.concurrent.Callable) ; => true (bases nil) ; => nil (contains? (set (bases java.lang.Boolean)) Object) ; => true (contains? (set (bases (class :a))) clojure.lang.IFn) ; => true
Nadtypy, parents

Funkcja parents pozwala sprawdzić, jakie są nadtypy podanego typu danych (klasy lub interfejsu klasy).

Użycie:

  • (parents klasa).

Jako argument należy przekazać nazwę klasy, a wartością zwracaną jest zbiór zawierający jej klasę bazową oraz wszystkie bezpośrednio implementowane przez klasę interfejsy i wszystkie bezpośrednie nadtypy bazujące na znacznikach, jeżeli wytworzono tego typu relacje. Istnieje również wariant tej funkcji, który obsługuje te ostatnie.

Jeżeli podany typ nie ma nadtypów, zwracaną wartością jest nil.

Przykład użycia funkcji parents
 1(parents Object)
 2; => nil
 3
 4(contains? (parents String) CharSequence)      ; => true
 5(contains? (parents Integer) Number)           ; => true
 6(contains? (parents clojure.lang.Symbol)
 7           clojure.lang.Named)                 ; => true
 8
 9(derive clojure.lang.Associative ::kolekcja)
10(contains? (parents clojure.lang.Associative) ::kolekcja)
11; => true
(parents Object) ; => nil (contains? (parents String) CharSequence) ; => true (contains? (parents Integer) Number) ; => true (contains? (parents clojure.lang.Symbol) clojure.lang.Named) ; => true (derive clojure.lang.Associative ::kolekcja) (contains? (parents clojure.lang.Associative) ::kolekcja) ; => true
Wszystkie nadtypy, ancestors

Funkcja ancestors pozwala sprawdzić, jakie są wszystkie bezpośrednie i pośrednie klasy macierzyste podanej klasy lub interfejsu, a także wszystkie implementowane przez nią interfejsy. Uwzględniane są również ewentualne typy bazujące na znacznikach, jeżeli są nadtypami podanej klasy.

Użycie:

  • (ancestors klasa).

Przyjmowanym argumentem jest klasa lub interfejs, których nadtypy mają być sprawdzone.

Funkcja zwraca zbiór zawierający wszystkie bezpośrednie i pośrednie nadklasy, wszystkie implementowane interfejsy oraz nadtypy bazujące na znacznikach podanej klasy. Istnieje również wariant tej funkcji, który operuje na znacznikowych typach doraźnych.

Jeżeli podana klasa nie ma nadklas, zwracaną wartością jest nil.

Przykład użycia funkcji ancestors
1(ancestors Object)
2; => nil
3
4(contains? (ancestors String) CharSequence) ; => true
5(contains? (ancestors Integer) Object)      ; => true
6
7(derive clojure.lang.Associative ::kolekcja)
8(contains? (ancestors clojure.lang.Associative) ::kolekcja)
9; => true
(ancestors Object) ; => nil (contains? (ancestors String) CharSequence) ; => true (contains? (ancestors Integer) Object) ; => true (derive clojure.lang.Associative ::kolekcja) (contains? (ancestors clojure.lang.Associative) ::kolekcja) ; => true

System typów podstawowych

Typy obiektowe to nie jedyna rodzina typów dostępna w Clojure. W niektórych przypadkach będziemy mogli korzystać również z tzw. typów podstawowych. One również pochodzą z platformy gospodarza.

Podstawowe typy danych (ang. primitive data types) to takie typy, które są wbudowane w język i pełnią funkcję podstawowych jednostek, z których tworzone mogą być typy złożone.

Na przykład obiekt może być wyposażony w atrybuty, które nie są obiektami, lecz właśnie typami podstawowymi, nie zdefiniowanymi w żadnych klasach.

Typami podstawowymi JVM istotnymi dla współpracy z Clojure są:

  • boolean – wartości logiczne truefalse (rozmiar pamięci zależy od kontekstu);
  •    byte – bajty (8-bitowe, zakres: -128 do 127);
  •    char – jednostki kodowe UTF-16 (16-bitowe, zakres: 0 do 65535);
  •   short – krótkie liczby całkowite (16-bitowe, zakres: -32768 do 32767);
  •     int – liczby całkowite (32-bitowe, zakres: -231 do 231-1);
  •    long – liczby całkowite długie (64-bitowe, zakres: -263 do 263-1);
  •   float – liczby zmiennoprzecinkowe (32-bitowe, zgodne z IEEE 754);
  •  double – liczby zmiennoprzecinkowe podwójnej precyzji (64-bitowe, IEEE 754).

Typy opakowane

Wartości prymitywne przekraczające granicę obsługującą wyłącznie obiekty są reprezentowane przez obiekty opakowujące. Konwersję tę nazywamy opakowywaniem (ang. boxing) i może zostać wstawiona automatycznie. W lokalnych wartościach prymitywnych, arytmetyce, tablicach i wywołaniach hosta kompilator może zachować wartości nieopakowane.

Spójrzmy, jakie typy obiektowe będą używane do opakowywania typów podstawowych:

  • booleanjava.lang.Boolean;
  •    bytejava.lang.Byte;
  •    charjava.lang.Character;
  •   shortjava.lang.Short;
  •     intjava.lang.Integer;
  •    longjava.lang.Long;
  •   floatjava.lang.Float;
  •  doublejava.lang.Double.

Na granicy obiektowej, takiej jak wynik REPL, literał 123 występuje jako java.lang.Long. Podczas skompilowanej arytmetyki ta sama wartość może przechodzić przez lokalne zmienne i instrukcje typu prymitywnego long. Sama składnia źródłowa nie wymusza jednej reprezentacji w każdym punkcie.

Opakowywanie pozwala wartościom przekraczać granice obiektowych API i trafiać do zwykłych trwałych kolekcji. Klasy opakowujące są zwykłymi klasami Javy, lecz na ogół końcowymi, opakowanie nie oznacza więc, że aplikacja może po nich dziedziczyć. Koszt alokacji, pośrednictwa i pamięci może mieć znaczenie w gorących ścieżkach numerycznych.

W pewnych warunkach Clojure może bezpośrednio używać danych prymitywnych. Przydaje się to w ciasnych pętlach numerycznych i operacjach na tablicach prymitywnych. Opakowywanie może przydzielać obiekty, choć pamięci podręczne, analiza ucieczki i optymalizacje JIT sprawiają, że jedna iteracja kodu źródłowego nie gwarantuje jednej alokacji na stercie.

Poza typami opakowanymi możemy w wybranych kontekstach uzyskać dostęp do typów rozpakowanych (ang. unboxed types). Ścieżki prymitywne omijają metody obiektowe i mogą zmniejszyć koszt opakowywania, lecz wpływ na wydajność należy mierzyć w rzeczywistej ścieżce wywołania.

Kompilator Clojure obsługuje ścieżki prymitywne w wybranych kontekstach. Funkcje koercji mogą ustanawiać lokalne wartości prymitywne, a sugerowanie typu pomaga wybrać sygnatury prymitywne i wywołania hosta. Podpowiedzi są informacją dla generatora kodu, nie asercją uruchomieniową.

Obsługa typów podstawowych możliwa jest w wielu kontekstach, do których należą:

Doraźny system typów

System typów języka Clojure jest elastycznyrozszerzalny. Oprócz klasy czasu wykonania każdej wartości JVM poza nil możemy dołączać aplikacyjne znaczniki typu pod kluczem metadanych :type do wartości, które obsługują metadane.

Dodatkowe, niezależne od platformy typy mogą być używane do rozpoznawania struktur danych, które mają znaczenie w kontekście przyjętej logiki aplikacji, ale także podczas tworzenia niektórych polimorficznych operacji, które zostaną omówione później.

Hierarchiczność

System typów stworzony na bazie własnych oznaczeń możemy nazywać znacznikowym systemem typów lub doraźnym systemem hierarchicznym (ang. ad hoc hierarchy system). Jest on hierarchiczny, tzn. umożliwia tworzenie relacji między typami, a więc wyróżnianie nadtypów i podtypów. Możemy tworzyć wiele różnych hierarchii lub korzystać z domyślnej hierarchii globalnej.

Zgodnie z przyjętą nomenklaturą bezpośredni nadtyp danego typu znacznikowego nazwiemy też:

  • rodzicem (ang. parent),
  • typem macierzystym (ang. parent type),
  • typem bazowym (ang. base type),
  • bezpośrednim przodkiem (ang. direct ancestor).

Z kolei bezpośredni podtyp:

  • dzieckiem (ang. child),
  • typem potomnym (ang. child type),
  • typem pochodnym (ang. derived type),
  • bezpośrednim potomkiem (ang. direct descendant).

Za podtypy będziemy również uważali wszystkie typy potomne, zwane potomkami (ang. descendants) lub typami pochodnymi (ang. derived types), a za nadtypy wszystkie typy nadrzędne, zwane przodkami (ang. ancestors) lub typami bazowymi (ang. base types).

Obsługa w Clojure

Oznaczanie wartości typami

Użycie:

  • ^{:type znacznik} wartość,
  • '^{:type znacznik} wartość.

Znaczniki typów są zwykle słowami kluczowymi albo symbolami z dookreśloną przestrzenią nazw, zwłaszcza gdy uczestniczą w hierarchii globalnej. Oznaczane wartości muszą obsługiwać metadane, np. symbole, kolekcje Clojure, Vary albo wybrane obiekty referencyjne.

Przykład oznaczania typu danych
1(def Paweł
2  ^{:type ::Człowiek} {:imię     "Paweł"
3                       :nazwisko "Wilk"
4                       :płeć     :m})
5
6(type Paweł)
7; => :user/Człowiek
(def Paweł ^{:type ::Człowiek} {:imię "Paweł" :nazwisko "Wilk" :płeć :m}) (type Paweł) ; => :user/Człowiek
Tworzenie hierarchii, make-hierarchy

Funkcja make-hierarchy pozwala tworzyć własne hierarchie typów bazujących na oznaczeniach.

Użycie:

  • (make-hierarchy).

Funkcja nie przyjmuje argumentów, a wartością zwracaną jest mapa, w której przechowywana będzie informacja o hierarchicznych relacjach typów bazujących na oznaczeniach.

Przykład użycia funkcji make-hierarchy
(def h (make-hierarchy))
(def h (make-hierarchy))
Derywacja typów, derive

Funkcja derive pozwala dla danej hierarchii typów oznaczonych ustanowić relację przodek–potomek między dwoma typami.

Hierarchie mogą wiązać własne znaczniki ze sobą, a także wyprowadzać klasy hosta z własnych znaczników. Multimetoda może dzięki temu traktować kilka skądinąd niezwiązanych klas hosta jako elementy jednej kategorii aplikacyjnej.

Użycie:

  • (derive            typ przodek),
  • (derive hierarchia typ przodek).

W podstawowym wariancie funkcja przyjmuje dwa argumenty. Pierwszym powinien być typ wyrażony symbolem (z dookreśloną przestrzenią nazw), słowem kluczowym (z dookreśloną przestrzenią nazw) lub klasą. Wartością drugiego przekazywanego argumentu powinien być typ, który ma być względem niego nadrzędny (być jego nadtypem), wyrażony symbolem lub kluczem. Zmiana zostanie wprowadzona w globalnej hierarchii typów oznaczonych, a zwróconą wartością będzie nil.

W wersji trójargumentowej pierwszym argumentem powinna być mapa hierarchii, a wywołanie nie spowoduje powstania efektu ubocznego w postaci jej modyfikacji, lecz zwróci zaktualizowany obiekt.

Przykłady użycia funkcji derive
 1;; własna hierarchia:
 2
 3(def h
 4  (-> (make-hierarchy)
 5      (derive ::kot       ::zwierzę)
 6      (derive ::pies      ::zwierzę)
 7      (derive ::owczarek  ::pies)
 8      (derive ::pudel     ::pies)
 9      (derive clojure.lang.Associative ::owczarek)
10      (derive clojure.lang.Associative ::pudel)))
11
12h
13; => {:parents
14; =>  {:user/kot      #{:user/zwierzę},
15; =>   :user/pies     #{:user/zwierzę},
16; =>   :user/owczarek #{:user/pies},
17; =>   :user/pudel    #{:user/pies},
18; =>   clojure.lang.Associative #{:user/pudel :user/owczarek}},
19; =>  :ancestors
20; =>  {:user/kot      #{:user/zwierzę},
21; =>   :user/pies     #{:user/zwierzę},
22; =>   :user/owczarek #{:user/pies :user/zwierzę},
23; =>   :user/pudel    #{:user/pies :user/zwierzę},
24; =>   clojure.lang.Associative #{:user/pudel :user/owczarek :user/pies :user/zwierzę}},
25; =>  :descendants
26; =>  {:user/zwierzę  #{:user/pies :user/owczarek :user/kot :user/pudel clojure.lang.Associative},
27; =>   :user/pies     #{:user/owczarek :user/pudel clojure.lang.Associative},
28; =>   :user/owczarek #{clojure.lang.Associative},
29; =>   :user/pudel    #{clojure.lang.Associative}}}
30
31;; globalna hierarchia:
32
33(derive ::kot       ::zwierzę)
34(derive ::pies      ::zwierzę)
35(derive ::owczarek  ::pies)
36(derive ::pudel     ::pies)
37(derive clojure.lang.Associative ::owczarek)
38(derive clojure.lang.Associative ::pudel)
39; => nil
;; własna hierarchia: (def h (-> (make-hierarchy) (derive ::kot ::zwierzę) (derive ::pies ::zwierzę) (derive ::owczarek ::pies) (derive ::pudel ::pies) (derive clojure.lang.Associative ::owczarek) (derive clojure.lang.Associative ::pudel))) h ; => {:parents ; => {:user/kot #{:user/zwierzę}, ; => :user/pies #{:user/zwierzę}, ; => :user/owczarek #{:user/pies}, ; => :user/pudel #{:user/pies}, ; => clojure.lang.Associative #{:user/pudel :user/owczarek}}, ; => :ancestors ; => {:user/kot #{:user/zwierzę}, ; => :user/pies #{:user/zwierzę}, ; => :user/owczarek #{:user/pies :user/zwierzę}, ; => :user/pudel #{:user/pies :user/zwierzę}, ; => clojure.lang.Associative #{:user/pudel :user/owczarek :user/pies :user/zwierzę}}, ; => :descendants ; => {:user/zwierzę #{:user/pies :user/owczarek :user/kot :user/pudel clojure.lang.Associative}, ; => :user/pies #{:user/owczarek :user/pudel clojure.lang.Associative}, ; => :user/owczarek #{clojure.lang.Associative}, ; => :user/pudel #{clojure.lang.Associative}}} ;; globalna hierarchia: (derive ::kot ::zwierzę) (derive ::pies ::zwierzę) (derive ::owczarek ::pies) (derive ::pudel ::pies) (derive clojure.lang.Associative ::owczarek) (derive clojure.lang.Associative ::pudel) ; => nil
Usuwanie derywacji typów, underive

Funkcja underive pozwala dla danej hierarchii typów oznaczonych usunąć wskazaną relację przodek–potomek między dwoma typami.

Użycie:

  • (underive            typ przodek),
  • (underive hierarchia typ przodek).

W podstawowym wariancie funkcja przyjmuje dwa argumenty. Pierwszym powinien być typ wyrażony symbolem (z dookreśloną przestrzenią nazw), słowem kluczowym (z dookreśloną przestrzenią nazw) lub klasą. Wartością drugiego przekazywanego argumentu powinien być typ, który jest względem niego nadrzędny, wyrażony symbolem lub kluczem. Zmiana zostanie wprowadzona w globalnej hierarchii typów oznaczonych, a zwróconą wartością będzie nil.

W wersji trójargumentowej pierwszym argumentem powinna być mapa hierarchii, a wywołanie nie spowoduje powstania efektu ubocznego w postaci jej modyfikacji, lecz zwróci zaktualizowany obiekt.

Przykłady użycia funkcji underive
 1;; własna hierarchia:
 2
 3(def h
 4  (-> (make-hierarchy)
 5      (derive ::kot  ::zwierzę)
 6      (derive ::pies ::zwierzę)))
 7
 8h
 9; => {:parents {:user/kot #{:user/zwierzę}, :user/pies #{:user/zwierzę}},
10; =>  :ancestors {:user/kot #{:user/zwierzę}, :user/pies #{:user/zwierzę}},
11; =>  :descendants {:user/zwierzę #{:user/pies :user/kot}}}
12
13(alter-var-root (var h) underive ::kot ::zwierzę)
14; => {:ancestors {:user/pies #{:user/zwierzę}}
15; =>  :descendants {:user/zwierzę #{:user/pies}}
16; =>  :parents {:user/pies #{:user/zwierzę}}}
17
18;; globalna hierarchia:
19
20(derive   ::kot  ::zwierzę)
21(derive   ::pies ::zwierzę)
22(underive ::kot  ::zwierzę)
23; => nil
24
25@#'clojure.core/global-hierarchy
26; => {:ancestors {:user/pies #{:user/zwierzę}}
27; =>  :descendants {:user/zwierzę #{:user/pies}}
28; =>  :parents {:user/pies #{:user/zwierzę}}}
;; własna hierarchia: (def h (-> (make-hierarchy) (derive ::kot ::zwierzę) (derive ::pies ::zwierzę))) h ; => {:parents {:user/kot #{:user/zwierzę}, :user/pies #{:user/zwierzę}}, ; => :ancestors {:user/kot #{:user/zwierzę}, :user/pies #{:user/zwierzę}}, ; => :descendants {:user/zwierzę #{:user/pies :user/kot}}} (alter-var-root (var h) underive ::kot ::zwierzę) ; => {:ancestors {:user/pies #{:user/zwierzę}} ; => :descendants {:user/zwierzę #{:user/pies}} ; => :parents {:user/pies #{:user/zwierzę}}} ;; globalna hierarchia: (derive ::kot ::zwierzę) (derive ::pies ::zwierzę) (underive ::kot ::zwierzę) ; => nil @#'clojure.core/global-hierarchy ; => {:ancestors {:user/pies #{:user/zwierzę}} ; => :descendants {:user/zwierzę #{:user/pies}} ; => :parents {:user/pies #{:user/zwierzę}}}
Predykat dziedziczenia, isa?

Funkcja isa? pozwala sprawdzić, czy podany typ (znacznikowy bądź obiektowy) jest bezpośrednim lub pośrednim podtypem innego podanego typu.

Użycie:

  • (isa?            potomek rodzic),
  • (isa? hierarchia potomek rodzic),

Pierwszym przyjmowanym argumentem jest znacznik typu potomnego lub obiektowy typ systemu gospodarza (klasa), a drugim znacznik typu macierzystego.

W wariancie trójargumentowym pierwszym argumentem powinna być mapa definiująca hierarchię typów.

Wartością zwracaną będzie true, jeżeli podany typ pochodny jest bezpośrednim lub pośrednim potomkiem typu macierzystego. W przeciwnym razie zwracaną wartością będzie false.

Przykład użycia funkcji isa?
 1(derive ::kot       ::zwierzę)
 2(derive ::pies      ::zwierzę)
 3(derive ::owczarek  ::pies)
 4(derive ::pudel     ::pies)
 5
 6(isa? ::kot      ::zwierzę)  ; => true
 7(isa? ::pudel    ::pies)     ; => true
 8(isa? ::kot      ::pies)     ; => false
 9(isa? ::owczarek ::kot)      ; => false
10
11(derive clojure.lang.Associative ::kolekcja)
12(isa?   clojure.lang.Associative ::kolekcja)
13; => true
(derive ::kot ::zwierzę) (derive ::pies ::zwierzę) (derive ::owczarek ::pies) (derive ::pudel ::pies) (isa? ::kot ::zwierzę) ; => true (isa? ::pudel ::pies) ; => true (isa? ::kot ::pies) ; => false (isa? ::owczarek ::kot) ; => false (derive clojure.lang.Associative ::kolekcja) (isa? clojure.lang.Associative ::kolekcja) ; => true

Ostatnie dwie linie przykładu ukazują przypadek użycia, w którym typ doraźny ::kolekcja zostaje oznaczony jako nadtyp obiektowego typu clojure.lang.Associative. W ten sposób jesteśmy w stanie grupować typy obiektowe i sprawdzać ich przynależność.

Zwróćmy uwagę, że funkcji można używać również w wariancie obiektowym.

Nadtypy, parents

Funkcja parents pozwala sprawdzić, jakie są bezpośrednie nadtypy podanego typu danych.

Użycie:

  • (parents            typ),
  • (parents hierarchia typ).

W wariancie jednoargumentowym należy przekazać znacznik typu, a w wariancie dwuargumentowym hierarchię określoną mapą (stworzoną z użyciem make-hierarchy) oraz znacznik typu.

Funkcja zwraca zbiór zawierający wszystkie bezpośrednie nadtypy podanego typu. Obsługiwane są również typy obiektowe: funkcja zwraca klasy bazowe i bezpośrednio implementowane interfejsy podanej klasy, a także typy bazujące na znacznikach, które pozostają w relacji rodzicielskiej względem podanych typów obiektowych systemu gospodarza.

Jeżeli podany typ nie ma nadtypów, zwracaną wartością jest nil.

Przykład użycia funkcji parents
 1(derive ::kot       ::zwierzę)
 2(derive ::pies      ::zwierzę)
 3(derive ::owczarek  ::pies)
 4(derive ::pudel     ::pies)
 5(derive clojure.lang.Associative ::kolekcja)
 6
 7(parents ::kot)       ; => #{:user/zwierzę}
 8(parents ::owczarek)  ; => #{:user/pies}
 9(parents ::kolekcja)  ; => nil
10
11(parents clojure.lang.Associative)
12; => #{:user/kolekcja
13; =>   clojure.lang.ILookup
14; =>   clojure.lang.IPersistentCollection}
(derive ::kot ::zwierzę) (derive ::pies ::zwierzę) (derive ::owczarek ::pies) (derive ::pudel ::pies) (derive clojure.lang.Associative ::kolekcja) (parents ::kot) ; => #{:user/zwierzę} (parents ::owczarek) ; => #{:user/pies} (parents ::kolekcja) ; => nil (parents clojure.lang.Associative) ; => #{:user/kolekcja ; => clojure.lang.ILookup ; => clojure.lang.IPersistentCollection}
Wszystkie podtypy, descendants

Funkcja descendants pozwala sprawdzić, jakie są bezpośrednie i pośrednie podtypy typu identyfikowanego podanym znacznikiem.

Użycie:

  • (descendants            typ),
  • (descendants hierarchia typ).

Argumentem powinien być znacznik określający badany typ, a w wariancie dwuargumentowym mapa hierarchii (stworzonej z użyciem make-hierarchy) i znacznik badanego typu.

Funkcja zwraca zbiór zawierający wszystkie bezpośrednie i pośrednie typy potomne względem podanego typu bazującego na znaczniku.

Przykład użycia funkcji descendants
 1(derive ::kot       ::zwierzę)
 2(derive ::pies      ::zwierzę)
 3(derive ::owczarek  ::pies)
 4(derive ::pudel     ::pies)
 5(derive clojure.lang.Associative ::kolekcja)
 6
 7(descendants ::kot)       ; => nil
 8(descendants ::owczarek)  ; => nil
 9(descendants ::pies)      ; => #{:user/owczarek :user/pudel}
10(descendants ::kolekcja)  ; => #{clojure.lang.Associative}
(derive ::kot ::zwierzę) (derive ::pies ::zwierzę) (derive ::owczarek ::pies) (derive ::pudel ::pies) (derive clojure.lang.Associative ::kolekcja) (descendants ::kot) ; => nil (descendants ::owczarek) ; => nil (descendants ::pies) ; => #{:user/owczarek :user/pudel} (descendants ::kolekcja) ; => #{clojure.lang.Associative}
Wszystkie nadtypy, ancestors

Funkcja ancestors pozwala sprawdzić, jakie są wszystkie bezpośrednie i pośrednie nadtypy podanego typu danych.

Użycie:

  • (ancestors            typ),
  • (ancestors hierarchia typ).

W wariancie jednoargumentowym należy przekazać znacznik typu, a w wariancie dwuargumentowym hierarchię określoną mapą (stworzoną z użyciem make-hierarchy) oraz znacznik typu.

Funkcja zwraca zbiór zawierający wszystkie bezpośrednie i pośrednie nadtypy podanego typu. Obsługiwane są również typy obiektowe: funkcja zwraca nadklasy i wszystkie implementowane interfejsy podanej klasy, a także typy bazujące na znacznikach, które pozostają w relacji rodzicielskiej względem podanych typów obiektowych systemu gospodarza.

Jeżeli podany typ nie ma nadtypów, zwracaną wartością jest nil.

Przykład użycia funkcji ancestors
 1(derive ::kot       ::zwierzę)
 2(derive ::pies      ::zwierzę)
 3(derive ::owczarek  ::pies)
 4(derive ::pudel     ::pies)
 5(derive clojure.lang.Associative ::kolekcja)
 6
 7(ancestors ::kot)       ; => #{:user/zwierzę}
 8(ancestors ::owczarek)  ; => #{:user/pies :user/zwierzę}
 9(ancestors ::kolekcja)  ; => nil
10
11(ancestors clojure.lang.Associative)
12; => #{:user/kolekcja
13; =>   clojure.lang.ILookup
14; =>   clojure.lang.IPersistentCollection
15; =>   clojure.lang.Seqable}
(derive ::kot ::zwierzę) (derive ::pies ::zwierzę) (derive ::owczarek ::pies) (derive ::pudel ::pies) (derive clojure.lang.Associative ::kolekcja) (ancestors ::kot) ; => #{:user/zwierzę} (ancestors ::owczarek) ; => #{:user/pies :user/zwierzę} (ancestors ::kolekcja) ; => nil (ancestors clojure.lang.Associative) ; => #{:user/kolekcja ; => clojure.lang.ILookup ; => clojure.lang.IPersistentCollection ; => clojure.lang.Seqable}

Operacje generyczne

Istnieją w Clojure generyczne operacje wspólne dla wszystkich systemów typów. Niektóre z nich umożliwiają nawet miksowanie typów odmiennych rodzin (np. znacznikowego i obiektowego).

Sprawdzanie typów

Badanie typu wartości, type

Funkcja type umożliwia zbadanie z jakiego typu wartością mamy do czynienia. Działa dla typów doraźnych i obiektowych.

Użycie:

  • (type wartość).

Argumentem wywołania funkcji powinna być dowolna wartość, a wartością zwracaną będzie jej typ.

Typ zostanie odczytany z metadanych wartości (klucz :type), a jeżeli się to nie powiedzie, zwrócona będzie nazwa klasy, której obiekt jest instancją.

Przykłady użycia funkcji type
 1(type 1)        ; => java.lang.Long
 2(type "abc")    ; => java.lang.String
 3(type \a)       ; => java.lang.Character
 4(type nil)      ; => nil
 5(type true)     ; => java.lang.Boolean
 6(type false)    ; => java.lang.Boolean
 7(type (fn []))  ; => klasa funkcji o nazwie zależnej od implementacji
 8(type [])       ; => clojure.lang.PersistentVector
 9(type :a)       ; => clojure.lang.Keyword
10(type 'a)       ; => clojure.lang.Symbol
11
12(type '^{:type ::Coś} a)
13; => :user/Coś
(type 1) ; => java.lang.Long (type "abc") ; => java.lang.String (type \a) ; => java.lang.Character (type nil) ; => nil (type true) ; => java.lang.Boolean (type false) ; => java.lang.Boolean (type (fn [])) ; => klasa funkcji o nazwie zależnej od implementacji (type []) ; => clojure.lang.PersistentVector (type :a) ; => clojure.lang.Keyword (type 'a) ; => clojure.lang.Symbol (type '^{:type ::Coś} a) ; => :user/Coś

Widzimy, że niektóre typy danych pochodzą bezpośrednio z Javy, a niektóre są hierarchicznymi typami specyficznymi dla Clojure. W ostatnim wyrażeniu nadaliśmy własne oznaczenie typu dla symbolu a. Zwróćmy uwagę na różnicę w wartości zwracanej przez to wyrażenie względem przykładu wywołania class.

Operacje polimorficzne

Konwersja, koercja i rzutowanie

Pewne operacje na typach danych możemy uznać za proste mechanizmy polimorficzne, gdyż pozwalają traktować wartości danego typu tak, jakby były wartościami innego. Te operacje to:

  • konwersja – tworzenie wartości nowego typu na bazie wartości innego typu;
  • rzutowanie – traktowanie wartości tak, jakby była wartością innego typu;
  • koercja – konwersja typu wartości przekazywanej jako argument funkcji.

Warto na wstępie zaznaczyć, że w praktyce niektóre z wymienionych terminów używane bywają zamiennie z uwagi na nieprecyzyjne konwencje nazewnictwa i różnice w szczegółach działania kompilatorów.

Konwersja, koercja i rzutowanie są operacjami obiektowego oraz podstawowego systemu typów platformy gospodarza.

Polimorfizm typów obiektowych

W Clojure możemy tworzyć nowe typy obiektowe, korzystając z mechanizmu tzw. rekordów oraz definiowania typów. Rekordy i typy własne mogą być następnie dopasowywane do właściwych operacji zgodnie z zasadami określonymi tzw. protokołami (ang. protocols).

Polimorfizm typów doraźnych

Hierarchiczne typy doraźne bazujące na znacznikach mogą być tworzone, a następnie używane w połączeniu z mechanizmem multimetod, aby konstruować bazujące na nich (type) lub na relacjach między nimi (isa?) polimorficzne operacje.

Wbudowane typy danych

Wbudowane typy obiektowe

Typy proste
Typy proste JVM
Typy referencyjne
Typy funkcyjne
  • clojure.lang.IFn – interfejs wywołania implementowany przez wartości funkcyjne Clojure i kilka wywoływalnych typów danych.
Typy powtórzeniowe
Typy wyjątkowe
  • clojure.lang.ExceptionInfo – informacje dot. wyjątku.
Typy warunkowe
Typy strumieniowe
Typy zakresowe
Typy sekwencyjne
  • clojure.lang.Cons – komórki cons,
  • clojure.lang.ChunkedCons – komórki cons list fragmentowanych,
  • clojure.lang.Cycle – sekwencje powtórzeniowe,
  • clojure.lang.IndexedSeq – sekwencje indeksowane,
  • clojure.lang.LazySeq – leniwe sekwencje,
  • clojure.lang.SeqEnumeration – sekwencje enumeracyjne,
  • clojure.lang.SeqIterator – sekwencje iteracyjne,
  • clojure.lang.StringSeq – sekwencja po jednostkach kodowych UTF-16 łańcucha.
Typy kolekcyjne
Typy transakcyjne
Typy literałowe

Wbudowane typy podstawowe

Typy proste

  • boolean  – wartość logiczna,
  • byte     – bajt,
  • char     – znak,
  • int      – liczba całkowita,
  • short    – liczba całkowita krótsza,
  • long      – liczba całkowita dłuższa,
  • float     – liczba zmiennoprzecinkowa,
  • double    – liczba podwójnej precyzji.
Typy tablicowe

  • booleans  – tablice wartości logicznych,
  • bytes     – tablice bajtów,
  • chars     – tablice znaków,
  • ints      – tablice liczb całkowitych,
  • shorts    – tablice liczb całkowitych krótszych,
  • longs     – tablice liczb całkowitych dłuższych,
  • floats    – tablice liczb zmiennoprzecinkowych,
  • doubles   – tablice liczb podwójnej precyzji.
Predykaty tablicowe

W standardowej bibliotece Clojure istnieje jedynie predykat bytes?:

  • (bytes? wartość) – sprawdza, czy wartość jest tablicą bajtów.

Pozostałe predykaty tablicowe (booleans?, chars?, ints?, shorts?, longs?, floats?, doubles?) nie istnieją w rdzeniu języka.

Jesteś w sekcji Poczytaj mi Clojure
Tematyka:

Taksonomie: