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łowiekiRoślina), -
określanie relacji między klasami wartości i konkretyzowanie
(np. typZwierzęi podtypSsak).
-
-
Budowanie abstrakcji:
-
rozróżnianie danych implementacyjnych (np. typy
StringiInteger), -
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.intalong, 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 niejawnie i dynamicznie 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.
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:
- „Protokoły”, rozdział XXI.
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ń.
(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ścitrue(lub użycie^:volatile-mutable) sprawi, że pole będzie mutowalne z semantyką widoczności javowegovolatile; -
:unsynchronized-mutable
– ustawienie wartościtrue(lub użycie^:unsynchronized-mutable) sprawi, że pole będzie mutowalne bez gwarancji synchronizacji ani widocznościvolatile.
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.
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.
->typ
(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 jesttrue, wygenerowana zostanie statyczna funkcjamain; -
: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 obiektamiVarwskazującymi implementacje metod; -
:load-impl-ns przełącznik
– gdytrue, 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 -.
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:
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:
- „gen-class”, ClojureDocs
- „gen-class – how it works and how to use it”, kotka.de
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.classpodczas 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.
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:
- „proxy”, ClojureDocs
- „proxy – gen-class little brother”, kotka.de
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;
reifywybieramy 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.
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,\a–java.lang.Character,true–java.lang.Boolean,123–java.lang.Long,123M–java.math.BigDecimal,123N–clojure.lang.BigInt,:a–clojure.lang.Keyword,'a–clojure.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.
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.
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ć Integer w Long.
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.
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.
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.
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.
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).
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.
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.
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 logicznetrueifalse(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:
boolean–java.lang.Boolean;byte–java.lang.Byte;char–java.lang.Character;short–java.lang.Short;int–java.lang.Integer;long–java.lang.Long;float–java.lang.Float;double–java.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żą:
- operacje arytmetyczne,
- powiązania leksykalne i parametryczne,
- powiązania w rekurencji ogonowej z użyciem
recur, - operacje tablicowe z użyciem odpowiednich form.
Doraźny system typów
System typów języka Clojure jest elastyczny i rozszerzalny. 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.
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.
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.
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.
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.
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.
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.
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.
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ą.
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
clojure.lang.BigInt– liczby całkowite nieograniczone,clojure.lang.Keyword– słowa kluczowe,clojure.lang.Ratio– ułamki,clojure.lang.Symbol– symbole.
Typy proste JVM
java.math.BigDecimal– liczby dziesiętne nieograniczone,java.math.BigInteger– liczby całkowite nieograniczone.java.lang.Boolean– wartości logiczne,java.lang.Byte– bajty,java.lang.Character– znaki,java.lang.Double– liczby zmiennoprzecinkowe podwójnej precyzji,java.lang.Float– liczby zmiennoprzecinkowe,java.lang.Integer– liczby całkowite,java.lang.Long– liczby całkowite długie,java.lang.Short– liczby całkowite krótkie.
Typy referencyjne
clojure.lang.Agent– Agenty,clojure.lang.Atom– Atomy,clojure.lang.Delay– Delay’e,- Promise’y – typ anonimowy tworzony przez
promiseideliver, clojure.lang.Ref– Refy,clojure.lang.Var– Vary,clojure.lang.Volatile– Volatile’e.
Typy funkcyjne
clojure.lang.IFn– interfejs wywołania implementowany przez wartości funkcyjne Clojure i kilka wywoływalnych typów danych.
Typy powtórzeniowe
clojure.lang.Iterate– iteracje,clojure.lang.RecordIterator– iteracje po rekordach,clojure.lang.Repeat– powtórzenia wartości.
Typy wyjątkowe
clojure.lang.ExceptionInfo– informacje dot. wyjątku.
Typy warunkowe
clojure.lang.Reduced– warunki zakończenia redukcji.
Typy strumieniowe
clojure.lang.XMLHandler– uchwyt parsera XML.
Typy zakresowe
clojure.lang.LongRange– zakresy długich liczb całkowitych,clojure.lang.Range– zakresy liczb całkowitych.
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
clojure.lang.MapEntry– element mapy (asocjacja),clojure.lang.Namespace– przestrzenie nazw,clojure.lang.PersistentArrayMap– mapy tablicowe,clojure.lang.PersistentHashMap– mapy,clojure.lang.PersistentHashSet– zbiory,clojure.lang.LazilyPersistentVector– leniwe wektory,clojure.lang.PersistentList– listy,clojure.lang.PersistentQueue– kolejki,clojure.lang.PersistentStructMap– mapy strukturalne,clojure.lang.PersistentTreeMap– mapy sortowane,clojure.lang.PersistentTreeSet– zbiory sortowane,clojure.lang.PersistentVector– wektory,clojure.lang.TransactionalHashMap– wewnętrzna transakcyjna mapa mieszająca.
Typy transakcyjne
clojure.lang.LockingTransaction– transakcje.
Typy literałowe
clojure.lang.TaggedLiteral– literały oznaczone.
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.