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.

W teorii typy danych jednoznacznie określają, jakie operacje możemy wykonywać na wartościach. W praktyce wykorzystywanie typów do tego rodzaju weryfikacji może być skomplikowane i mało skuteczne, szczególnie podczas testowania złożonych danych biznesowych aplikacji. W związku z tym poza typami – które w językach dynamicznych pomagają bardziej kompilatorowi bądź interpreterowi, niż programiście – stosuje się także dodatkowe mechanizmy testowania i walidacji danych.

Typy dostarczają kompilatorowi bądź interpreterowi informacji o tym, jak zarządzać przestrzenią pamięciową i automatycznymi konwersjami danych jednego rodzaju do danych innego. W zależności od języka programowania wiedza o typach używanych w programie będzie mniej lub bardziej eksponowana. W językach z tzw. silnym typizowaniem mamy obowiązek oznaczania danych typami, aby kompilator mógł wyłapać niezgodności między naszymi deklaracjami a rzeczywistymi zastosowaniami; np. przekazywanie do funkcji liczby, gdy wymagana jest sekwencja znakowa będzie błędem kompilacji.

Jednym z zastosowań wiedzy o typach danych może być polimorficzne optymalizowanie kodu, które polega na generowaniu wielu wersji tego samego podprogramu w zależności od rodzajów przyjmowanych obiektów wejściowych. Na przykład argumentem tworzonej funkcji może być liczba całkowita lub łańcuch znakowy. Podczas kompilacji powstaną wtedy dwa warianty funkcyjnego obiektu w zależności od typu akceptowanego wejścia. Wygenerowana zostanie też odpowiednia funkcja dyspozycyjna, której zadaniem będzie wywoływanie pasującego do typu podprogramu obsługi. Dzięki temu kompilator będzie w stanie zastosować optymalizacje związane z konkretnym rodzajem przetwarzanych wartości w każdym wariancie funkcji (np. w przeciwieństwie do łańcuchów znakowych dane numeryczne nie muszą być rygorystycznie sprawdzane pod kątem liczby elementów czy symboli terminujących).

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.

Możemy wyróżnić języki programowania, które są słabo typizowane (ang. weakly typed) i mocno typizowane (ang. strongly typed). Te ostatnie dokonują sprawdzania typów danych podczas przekazywania wartości do podprogramów (np. funkcji) czy przypisywania ich do zmiennych, natomiast pierwsze nie wprowadzają takich restrykcji, lecz programista musi być bardziej czujny, aby nie dokonywać operacji na danych, których rodzaje (typy) nie mogą być przez te operacje poprawnie obsłużone. Gdy programista o to nie zadba, wystąpią usterki podczas pracy programu, ponieważ nie dojdzie do wykrycia niekompatybilności na etapie kompilacji.

W Clojure mamy do czynienia z dynamicznym typizowaniem, czyli dynamicznym sprawdzaniem typów (ang. dynamic type-checking). Oznacza to, że sprawdzanie typów danych i kompatybilności pamięciowych obiektów pod tym względem odbywa się w czasie uruchamiania programu, a nie na etapie kompilacji.

Język Clojure to również typizowanie słabe (ang. weak typing), które polega na tym, że programista nie musi deklarować typów danych będących w użyciu. Jest to możliwe m.in. dzięki inferencji typów i wyposażeniu języka w generyczne konstrukcje, które potrafią operować na danych dowolnego rodzaju (szczególnie w przypadku kolekcji i sekwencji).

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:

  • rozróżnianie danych biznesowych (np. typy CzłowiekRoślina);
  • rozróżnianie danych implementacyjnych (np. typy StringInteger);
  • określanie relacji między klasami wartości (np. typ Zwierzę i podtyp Ssak);
  • określanie wymogów interfejsów odnośnie danych wejściowych i wyjściowych (np. typy argumentów czy wartości zwracanych przez funkcje),
  • porównywanie właściwości konstruktów służących do przechowywania danych (np. możliwych zakresów czy sposobów odzwierciedlania wartości w pamięci).

Systemy typów w Clojure

Clojure jest językiem słabo i dynamicznie typizowanym, chociaż niektóre optymalizacje mogą wykorzystywać typizowanie statyczne JVM, a wybrane konstrukcje (np. argumenty i wartości zwracane przez funkcje czy niektóre klasy wartości) możemy oznaczać typami, chociaż nie jest to obowiązkowe i nie umożliwia wprowadzenia mocnego typizowania.

System typów języka Clojure składa się z trzech podsystemów:

  • obiektowego systemu typów platformy gospodarza,
  • podstawowego systemu typów platformy gospodarza,
  • doraźnego, hierarchicznego systemu typów.

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, będziemy nazywali instancją (ang. instance) lub egzemplarzem pewnej klasy.

Obiekty składają się ze zmiennych, w których przechowywane są ich dane, a także z odwołań do zdefiniowanych w klasach funkcji (zwanych metodami), które służą do operowania na zmiennych.

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 instancjach egzemplarze klas (obiekty) przechowują dane;

  • definicji funkcji składowych (ang. member functions), zwanych też metodami (ang. methods), które są operacjami, jakie można wykonywać na danych egzemplarza.

Klasa przypomina znaną z języka C konstrukcję zwaną strukturą (ang. structure), ale oprócz pól ma jeszcze funkcje, które pozwalają na nich operować. Egzemplarz klasy to – kontynuując analogię – zmienna, której typ określono konkretną strukturą. W praktyce mechanizmy programowania zorientowanego obiektowo można zaimplementować w języku C, jednak kody źródłowe takich programów byłyby dłuższe niż odpowiedniki w językach z wbudowaną obsługą klas i obiektów.

Dziedziczenie

Sposobem na dodawanie nowych metod lub pól do istniejących klas jest dziedziczenie (ang. inheritance), czyli tworzenie klas pochodnych. 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 dokonywać rozszerzania istniejących o nowe lub ich modyfikowania.

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ż interfejsy, w przeciwieństwie do klas bazowych, nie definiują metod, a tylko zapowiadają konieczność ich wystąpienia.

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).

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, a każda zadeklarowana w interfejsie metoda musi być zdefiniowana w klasie. Co więcej, klasy mogą być później rozszerzane o obsługę nowych interfejsów. Jest to jeden z mechanizmów dynamicznego polimorfizmu.

Przykład interfejsu w Javie
1
2
3
4
5
6
7
8
9
interface IPoliczalne {
  public Long zlicz();
}

class Napis extends String implements IPoliczalne {
  public Long zlicz() {
    return(new Long(super.length()));
  }
}
interface IPoliczalne { public Long zlicz(); } class Napis extends String implements IPoliczalne { public Long zlicz() { return(new Long(super.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ę znaków 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 zwykle nie będziemy musieli bezpośrednio zajmować się interfejsami systemu gospodarza, chyba że zechcemy dokonywać optymalizacji z wykorzystaniem operacji polimorficznych.

Definiowanie interfejsów, definterface

W Clojure możemy dynamicznie tworzyć nowe interfejsy Javy. Definicje zawierać mogą sygnatury metod, które będą musiały zostać zdefiniowane w każdej klasie implementującej interfejs.

Warto nadmienić, że w Clojure nie zaleca się bezpośredniego tworzenia interfejsów, ponieważ istnieje mechanizm zwany protokołami, który zastępuje je i wzbogaca.

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 [& argument…]) lub
  • (^typ nazwa-metody [& argument…]).

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

Generowanie interfejsów, gen-interface

Do niskopoziomowego generowania interfejsów Javy służy makro gen-interface. Sprawia ono, że w pamięci generowany jest kod bajtowy JVM dla interfejsu o podanej nazwie, która powinna być pełną nazwą pakietu Javy. Dodatkowo, jeżeli program jest kompilowany (w trybie AOT), w katalogu systemu plikowego, którego nazwa znajduje się w zmiennej dynamicznej *compile-path* umieszczony zostanie plik z rozszerzeniem class.

Użycie:

  • (gen-interface & opcja…).

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

Protokoły

Odpowiednikiem interfejsów Javy w Clojure są tzw. protokoły (ang. protocols), opisane w dalszej części. Bazują one na interfejsach, ale zawierają dodatkowe struktury danych i mechanizmy obsługi typowe dla Clojure.

Jedynymi sytuacjami, kiedy zechcemy wybrać interfejsy zamiast protokołów, będą te, gdzie ze względów wydajnościowych zdecydujemy, aby deklarowane funkcje przyjmowały jako argumenty lub zwracały jako wartości dane typów podstawowych.

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 tzw. problem 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
1
2
3
4
5
6
7
8
9
(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 pozwala definiować obiektowe typy danych. Jego użycie sprawia, że zostanie dynamicznie wygenerowana kompilowana do kodu bajtowego klasa definiująca typ danych, wraz z zawierającym ją pakietem Javy o nazwie takiej samej, jak nazwa bieżącej przestrzeni nazw.

W fazie pełnej kompilacji programu (jeżeli do niej dojdzie) nazwa klasy z dołączoną z przodu kropką i nazwą pakietu utworzą nazwę pliku, do której dodane będzie rozszerzenie class. Do pliku wpisana będzie definicja klasy i umieszczony on zostanie w katalogu o nazwie ścieżkowej określonej dynamiczną zmienną specjalną *compile-path* (domyślnie: classes).

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.

Do instancji stworzonego typu będzie można dynamicznie dodawać nowe pola i ich wartości. Tego rodzaju pola nazywamy polami rozszerzeniowymi (ang. extension fields).

Użycie:

  • (deftype nazwa pole… & opcja… & 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 zostanie oznaczone jako mutowalny obiekt ulotny (modyfikator volatile Javy);

  • :unsynchronized-mutable
    – ustawienie wartości true (lub użycie :unsynchronized-mutable) sprawi, że pole zostanie oznaczone jako obiekt mutowalny.

Wartości pól definiowanych typów są – podobnie jak inne wartości w Clojure – niemutowalne, ale zastosowanie powyższych metadanych może to zmienić. Należy korzystać z nich tylko wtedy, gdy naprawdę jest taka potrzeba i potrafimy obsłużyć ewentualne sytuacje graniczne, np. związane z obsługą współbieżności.

Uwaga: Nazwami pól nie mogą być __meta oraz __extmap – są to symbole zarezerwowane, które służą do wewnętrznej obsługi metadanych i pól rozszerzeniowych.

Po nazwach pól możemy podać opcje (obecnie nieobsługiwane) i tzw. specyfikacje (ang. skr. specs).

Każda specyfikacja powinna składać się z nazwy protokołu lub interfejsu platformy gospodarza, po której może (ale nie musi) pojawić się jedna lub więcej definicji metod w postaci:

  • (nazwa-metody [this & argument…] ciało).

Możemy i powinniśmy zdefiniować metody, które zostały zadeklarowane w protokołach lub interfejsach. Dodatkowo możemy umieszczać przeciążone wersje metod klasy Object.

Pierwszym przyjmowanym argumentem w przypadku definiowania metod zadeklarowanych w interfejsach powinien być obiekt instancji bieżącej (odpowiednik this z Javy). Podczas ich bezpośredniego wywoływania (z wykorzystaniem mechanizmów odwoływania się do obiektów platformy gospodarza) będzie on przekazywany automatycznie i nie należy podawać go wprost.

W odniesieniu do argumentów i wartości zwracanych przez wszystkie metody możliwe jest zastosowanie sugerowania typów.

W ciałach metod jesteśmy w stanie odwoływać się do definiowanej klasy (i w związku z tym do jej klasowych metod) przez podawanie jej symbolicznej nazwy.

Uwaga: Definicje metod nie zamykają w ciałach powiązań z leksykalnego otoczenia ich definicji. Mamy dostęp wyłącznie do zadeklarowanych pól.

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.

Uwaga: Nie należy tworzyć instancji typów przez bezpośrednie wywoływanie konstruktorów obiektu. Lepiej skorzystać z funkcji języka Clojure, która służy do tego celu: ->typ.

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

Przykład użycia makra deftype
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
(deftype Osoba [imię nazwisko wiek])
; => user.Osoba

;; tworzenie rekordu typu Osoba przez wywołanie konstruktora
;;
(def Paweł (Osoba. "Paweł" "Wilk" 18))

;; tworzenie rekordu typu 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"
(deftype Osoba [imię nazwisko wiek]) ; => user.Osoba ;; tworzenie rekordu typu Osoba przez wywołanie konstruktora ;; (def Paweł (Osoba. "Paweł" "Wilk" 18)) ;; tworzenie rekordu typu 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

Tworzyć obiekty zdefiniowanych samodzielnie typów możemy (i powinniśmy!) nie tylko bezpośrednio, odwołując się do konstruktora powstałego w efekcie wywołania deftype, lecz również z użyciem generowanej podczas definiowania nowego typu funkcji ->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
2
3
4
(deftype Osoba [imię nazwisko wiek])

(->Osoba "Paweł" "Wilk" 18)
; => #object[user.Osoba 0x53dda21f "user.Osoba@53dda21f"]
(deftype Osoba [imię nazwisko wiek]) (->Osoba "Paweł" "Wilk" 18) ; => #object[user.Osoba 0x53dda21f "user.Osoba@53dda21f"]

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).

Do instancji typu rekordowego (zwanych rekordami) można odwoływać się z użyciem funkcji operujących na mapach. Każde zadeklarowane w klasie pole jest w egzemplarzu rekordu uwidaczniane jako klucz wirtualnej mapy.

Do obiektów powstałych na bazie zdefiniowanego typu można też dynamicznie dodawać nowe pola wraz z wartościami, nawet jeżeli nie zostały one zadeklarowane w momencie tworzenia typu. Tego rodzaju pola nazywamy polami rozszerzeniowymi (ang. extension fields).

Użycie:

  • (defrecord nazwa pole… & opcja… & 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).

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

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.: (def -main [] ("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, która stanie się konstruktorem;

  • :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 {:get nazwa :set nazwa} …}
    – generuje gettery i settery dla chronionych metod nadklasy;

  • :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, implementacja statycznego inicjalizatora klasy poszukiwana będzie w funkcji z tej samej przestrzeni nazw.

Warto pamiętać, że nowa klasa nie będzie wyposażona w metody, które należy zdefiniować z użyciem funkcji w jednej przestrzeni nazw programu. Będą one dynamicznie wywoływane, gdy pojawią się odwołania do metod klasy o takich samych nazwach (z ustalonym przedrostkiem lub domyślnym przedrostkiem -).

Przykład użycia gen-class
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
(ns pl.randomseed.Człowiek)

;; tworzymy klasę Javy
;;
(gen-class
 :name         pl.randomseed.Człowiek
 :main         true
 :prefix       -człowiek-)

;; tworzymy implementacje metod
;;
(defn -człowiek-Wita [this]
  (str "Hejka " this))

(defn -człowiek-main [kto]
  (-człowiek-Wita kto))
(ns pl.randomseed.Człowiek) ;; tworzymy klasę Javy ;; (gen-class :name pl.randomseed.Człowiek :main true :prefix -człowiek-) ;; tworzymy implementacje metod ;; (defn -człowiek-Wita [this] (str "Hejka " this)) (defn -człowiek-main [kto] (-człowiek-Wita kto))

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

SH
1
2
3
4
5
6
7
8
9
mkdir -p /tmp/gen-class-przykład/src/pl/randomseed
mkdir /tmp/gen-class-przykład/classes
cd /tmp/gen-class-przykład

# edycja pliku
edit src/pl/randomseed/Człowiek.clj

# kompilacja
clj -e "(compile 'pl.randomseed.Człowiek)"
mkdir -p /tmp/gen-class-przykład/src/pl/randomseed mkdir /tmp/gen-class-przykład/classes cd /tmp/gen-class-przykład # edycja pliku edit src/pl/randomseed/Człowiek.clj # kompilacja clj -e "(compile 'pl.randomseed.Człowiek)"

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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
;; 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.

Gdybyśmy jednak w naszym przykładzie spróbowali wykonać jakąś operację arytmetyczną na wartości x, zgłoszony zostanie wyjątek Javy UnsupportedOperationException, ponieważ w obiekcie pośredniczącym zostaną uwidocznione wyłącznie zdefiniowane przez nas metody.

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 pozwala nam na reifikację, czyli konkretyzowanie interfejsów Javy i protokołów Clojure. Jest to starszy brat makra proxy i w stosunku do niego bardziej polecana konstrukcja.

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 są faktycznymi metodami anonimowej klasy, a nie dynamicznymi odwołaniami do funkcyjnych obiektów poza klasą. W związku z tym wywołania metod są szybsze – nie zachodzi konieczność rozdzielania z użyciem specjalnej mapy przyporządkowującej nazwy metod do funkcyjnych obiektów.

  • 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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
(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

Gdy w Clojure korzystamy z jakiejś wartości, będzie ona miała zawsze przypisany obiektowy typ danych, pochodzący z platformy gospodarza. Typ ten będzie odwzorowany w postaci klas z przestrzeni nazw java (najczęściej java.lang w przypadku JVM) lub dodany przez klasy zdefiniowane w przestrzeni clojure.lang.

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
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
(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 []))  ; => user$eval10652$fn__10653
(class [])       ; => clojure.lang.PersistentVector
(class :a)       ; => clojure.lang.Keyword
(class 'a)       ; => clojure.lang.Symbol

(class '^{:type ::Coś} a)
; => 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 [])) ; => user$eval10652$fn__10653 (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 nadtypó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 nadtypów (jedną z klas nadrzędnych lub interfejsów), zwracana jest podana wartość. W przeciwnym przypadku zgłaszany jest wyjątek java.lang.ClassCastException.

Przykłady użycia funkcji cast
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
(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 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

Zauważmy, że nazwa tej funkcji może być nieco myląca, ponieważ nie mamy do czynienia z rzutowaniem – widać to dobrze w ostatnim wyrażeniu przykładu.

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
2
3
4
5
6
7
(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
(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
2
3
4
5
6
7
8
9
(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
(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 znacznik typu potomnego, a drugim znacznik typu macierzystego.

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.

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

Przykład użycia funkcji isa?
1
2
3
4
5
6
7
8
(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
(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
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
(supers Long)
; => #{java.io.Serializable
; =>   java.lang.Comparable
; =>   java.lang.Number
; =>   java.lang.Object}

(supers clojure.lang.IFn)
; => #{java.lang.Runnable
; =>   java.util.concurrent.Callable}

(supers nil)
; => nil

(supers java.lang.Boolean)
; => #{java.io.Serializable
; =>   java.lang.Comparable
; =>   java.lang.Object}

(supers (class :a))
; => #{clojure.lang.IFn
; =>   clojure.lang.IHashEq
; =>   clojure.lang.Named
; =>   java.io.Serializable
; =>   java.lang.Comparable
; =>   java.lang.Object
; =>   java.lang.Runnable
; =>   java.util.concurrent.Callable}
(supers Long) ; => #{java.io.Serializable ; => java.lang.Comparable ; => java.lang.Number ; => java.lang.Object} (supers clojure.lang.IFn) ; => #{java.lang.Runnable ; => java.util.concurrent.Callable} (supers nil) ; => nil (supers java.lang.Boolean) ; => #{java.io.Serializable ; => java.lang.Comparable ; => java.lang.Object} (supers (class :a)) ; => #{clojure.lang.IFn ; => clojure.lang.IHashEq ; => clojure.lang.Named ; => java.io.Serializable ; => java.lang.Comparable ; => java.lang.Object ; => java.lang.Runnable ; => java.util.concurrent.Callable}

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 zbiór zawierający 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
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
(bases Long)
; => #{java.lang.Comparable
; =>   java.lang.Number}

(bases clojure.lang.IFn)
; => #{java.lang.Runnable
; =>   java.util.concurrent.Callable}

(bases nil)
; => nil

(bases java.lang.Boolean)
; => #{java.io.Serializable
; =>   java.lang.Comparable
; =>   java.lang.Object}

(bases (class :a))
; => #{clojure.lang.IFn
; =>   clojure.lang.IHashEq
; =>   clojure.lang.Named
; =>   java.io.Serializable
; =>   java.lang.Comparable}
(bases Long) ; => #{java.lang.Comparable ; => java.lang.Number} (bases clojure.lang.IFn) ; => #{java.lang.Runnable ; => java.util.concurrent.Callable} (bases nil) ; => nil (bases java.lang.Boolean) ; => #{java.io.Serializable ; => java.lang.Comparable ; => java.lang.Object} (bases (class :a)) ; => #{clojure.lang.IFn ; => clojure.lang.IHashEq ; => clojure.lang.Named ; => java.io.Serializable ; => java.lang.Comparable}

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
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
(parents Object)
; => nil

(parents String)
; => #{java.io.Serializable
; =>   java.lang.CharSequence
; =>   java.lang.Comparable
; =>   java.lang.Object}

(parents Integer)
; => #{java.lang.Comparable
; =>   java.lang.Number}

(parents clojure.lang.Symbol)
; => #{class clojure.lang.AFn
; =>   interface clojure.lang.IHashEq
; =>   interface clojure.lang.IObj
; =>   interface clojure.lang.Named
; =>   interface java.io.Serializable
; =>   interface java.lang.Comparable}

(derive clojure.lang.Associative ::kolekcja)
(parents clojure.lang.Associative)
; => #{:user/kolekcja
; =>   clojure.lang.ILookup
; =>   clojure.lang.IPersistentCollection}
(parents Object) ; => nil (parents String) ; => #{java.io.Serializable ; => java.lang.CharSequence ; => java.lang.Comparable ; => java.lang.Object} (parents Integer) ; => #{java.lang.Comparable ; => java.lang.Number} (parents clojure.lang.Symbol) ; => #{class clojure.lang.AFn ; => interface clojure.lang.IHashEq ; => interface clojure.lang.IObj ; => interface clojure.lang.Named ; => interface java.io.Serializable ; => interface java.lang.Comparable} (derive clojure.lang.Associative ::kolekcja) (parents clojure.lang.Associative) ; => #{:user/kolekcja ; => clojure.lang.ILookup ; => clojure.lang.IPersistentCollection}

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
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
(ancestors Object)
; => nil

(ancestors String)
; => #{java.io.Serializable
; =>   java.lang.CharSequence
; =>   java.lang.Comparable
; =>   java.lang.Object}

(ancestors Integer)
; => #{java.io.Serializable
; =>   java.lang.Comparable
; =>   java.lang.Number
; =>   java.lang.Object}

(derive clojure.lang.Associative ::kolekcja)
(ancestors clojure.lang.Associative)
; => #{:user/kolekcja
; =>   clojure.lang.ILookup
; =>   clojure.lang.IPersistentCollection
; =>   clojure.lang.Seqable}
(ancestors Object) ; => nil (ancestors String) ; => #{java.io.Serializable ; => java.lang.CharSequence ; => java.lang.Comparable ; => java.lang.Object} (ancestors Integer) ; => #{java.io.Serializable ; => java.lang.Comparable ; => java.lang.Number ; => java.lang.Object} (derive clojure.lang.Associative ::kolekcja) (ancestors clojure.lang.Associative) ; => #{:user/kolekcja ; => clojure.lang.ILookup ; => clojure.lang.IPersistentCollection ; => clojure.lang.Seqable}

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.

W przypadku języka Clojure i JVM typami podstawowymi będą:

  • boolean – wartości logiczne (8- do 32-bitowe, wartości: true lub false);
  •    byte – bajty (8-bitowe, zakres: -128 do 127);
  •    char – znaki alfabetu Unicode (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

Zazwyczaj wartości typów podstawowych w Javie (a więc i w Clojure) nie są reprezentowane bezpośrednio, lecz automatycznie umieszczane w specjalnych obiektach. Podstawowe typy wchodzące w skład takich obiektów nazywamy typami opakowanymi (ang. boxed types), a proces ich obiektowego reprezentowania automatycznym opakowywaniem (ang. autoboxing).

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.

Kiedy więc w programie widzimy zapis 123 to naprawdę mamy do czynienia z typem java.lang.Long. Wewnątrz obiektu tego typu znajduje się pole typu podstawowego (long) przechowujące właściwą wartość. Poza tym w klasie java.lang.Long znajdziemy funkcje składowe, czyli metody, które pozwalają dokonywać na instancjach różnych operacji.

Dzięki opakowywaniu możemy korzystać z wartości typów podstawowych tak samo, jak z innych obiektów (np. wywoływać pewne metody, umieszczać w kolekcjach, przekazywać jako argumenty itp.), a także operować na samych typach (np. tworzyć względem nich klasy pochodne, rozszerzać definiujące je klasy itp.). Minusem jest jednak wydajność przetwarzania i zajmowana przestrzeń pamięciowa.

W pewnych warunkach możliwe jest bezpośrednie korzystanie z danych typów podstawowych z pominięciem mechanizmu opakowywania. Przydaje się to np. w pętlach, które wykonują wiele iteracji, za każdym razem zmieniając jakąś numeryczną wartość. Gdyby zamiast zmiennej typu int, skorzystać ze zmiennej referencyjnej odnoszącej się do obiektu klasy Integer, to każdy przebieg pętli oznaczałby tworzenie nowej instancji odkładanej na stertę, a następnie niszczonej przez mechanizmy odśmiecania pamięci.

Poza korzystaniem z typów opakowanych możemy uzyskiwać bezpośredni dostęp do typów rozpakowanych (ang. unboxed types). Będą to takie typy podstawowe, których dane, mimo że z reguły opakowywane w obiektach, w pewnych sytuacjach użytkowane są bezpośrednio. Tracimy wtedy oczywiście wiele mechanizmów obiektowych, lecz zyskujemy na prędkości realizowania obliczeń i zdolności kompilatora do przeprowadzania optymalizacji.

W Clojure typy podstawowe platformy gospodarza obsługiwane są z użyciem obiektów opakowujących, jednak możemy wykorzystać tzw. sugerowanie typu bądź rzutowanie, aby w pewnych warunkach przeprowadzać operacje na wartościach typów rozpakowanych.

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. Poza tym, że każda wartość ma przypisany typ danych określony przez platformę gospodarza (np. JVM), możemy tworzyć własne dodatkowe oznaczenia typowe, korzystając z metadanych, a konkretnie z klucza :type – oczywiście dla tych struktur, które pozwalają dodawać 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ść.

Identyfikatorami oznaczeń typu powinny być słowa kluczowe lub symbole (w formach stałych) z dookreślonymi przestrzeniami nazw. Z kolei wartościami powinny być struktury danych, które obsługują metadane (np. symbole, kolekcje, zmienne globalne, niektóre obiekty referencyjne).

Przykład oznaczania typu danych
1
2
3
4
5
6
7
(def Paweł
  ^{:type ::Człowiek} {:imię     "Paweł"
                       :nazwisko "Wilk"
                       :płeć     :m})

(type Paweł)
; => :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.

W hierarchiach możemy wytwarzać relacje nie tylko między typami bazującymi na oznaczeniach własnych, ale też między typami własnymi a wbudowanymi. Typy wbudowane mogą być podtypami typów znacznikowych. Dzięki temu możemy na przykład wskazać, że tworzony typ reprezentowany może być określonymi strukturami danych.

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
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
;; 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}},
; =>  :descendants
; =>  {:user/zwierzę  #{:user/pies :user/owczarek :user/kot :user/pudel},
; =>   :user/pies     #{:user/owczarek :user/pudel},
; =>   :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
;; 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}}, ; => :descendants ; => {:user/zwierzę #{:user/pies :user/owczarek :user/kot :user/pudel}, ; => :user/pies #{:user/owczarek :user/pudel}, ; => :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
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
;; 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ę}}}
;; 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 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, 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
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
(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
(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

Zwróćmy uwagę, że isa? umożliwia też badanie relacji typów obiektowych do znacznikowych, gdy zostały one określone hierarchią. 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
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
(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}
(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
 2
 3
 4
 5
 6
 7
 8
 9
10
(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}
(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
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
(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}
(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
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
(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 []))  ; => user$eval10652$fn__10653
(type [])       ; => clojure.lang.PersistentVector
(type :a)       ; => clojure.lang.Keyword
(type 'a)       ; => clojure.lang.Symbol

(type '^{:type ::Coś} a)
; => :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 [])) ; => user$eval10652$fn__10653 (type []) ; => clojure.lang.PersistentVector (type :a) ; => clojure.lang.Keyword (type 'a) ; => clojure.lang.Symbol (type '^{:type ::Coś} a) ; => :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 (ang. records) 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
Typy powtórzeniowe
Typy wyjątkowe
  • clojure.lang.ExceptionInfo – informacje dot. wyjątku.
Typy warunkowe
Typy strumieniowe
Typy zakresowe
Typy sekwencyjne
Typy kolekcyjne
Typy transakcyjne
Typy literałowe

Wbudowane typy podstawowe

Typy proste JVM
  • long   – liczby całkowite długie,
  • double – liczby zmiennoprzecinkowe podwójnej precyzji.
Typy złożone JVM
  • ints    – tablice liczb całkowitych,
  • longs   – tablice liczb całkowitych dłuższych,
  • floats  – tablice liczb zmiennoprzecinkowych,
  • doubles – tablice liczb zmiennoprzecinkowych podwójnej precyzji.
Predykaty typów złożonych JVM
  • (ints?    wartość) – czy tablice liczb całkowitych,
  • (longs?   wartość) – czy tablice liczb całkowitych dłuższych,
  • (floats?  wartość) – czy tablice liczb zmiennoprzecinkowych,
  • (doubles? wartość) – czy tablice liczb zmiennoprzecinkowych podwójnej precyzji.
Jesteś w sekcji .
Tematyka:

Taksonomie: