• Nie Znaleziono Wyników

Rozdział 3. Język zapytań

3.3 Drzewo atrybutów

Do opisu złożonych struktur takich jak dane przestrzenne, schemat krotki zdefiniowany przy użyciu hierarchi jest łatwiejszy zarówno do zrozumienia, jak i definiowania na nim przekształceń. Często wśród schematów krotek możemy wyróżnić fragmenty podobne. Przykładowo, jeżeli pewna grupa strumieni przekazuje informacje o lokalizacji w układzie kartezjańskim, należy spodziewać się, że do opisu położenia użyto atrybutów x, y, (z). Jeżeli krotka przekazuje komunikaty o alarmach w systemie, prawdopodobnie jej schemat zawiera atrybuty: nazwa i opis. Aby umożliwić tworzenie podtypów w języku StreamAPAS wprowadzono drzewo atrybutów przedstawiające schemat krotki, jako hierarchię z nazwanymi węzłami. W odróżnieniu do XML, drzewo atrybutów definiuje typ danych przechowywany w węzłach.

Drzewo atrybutów jest zdefiniowane, jako zagnieżdżona lista węzłów posiadających nazwy i wartości obiektowe. Węzły nieprzechowujące wartości, są

typu NONE, służą one definiowaniu grup atrybutów, gdzie sam węzeł grupy nie ma ze sobą skojarzonej wartości. Z kolei etykieta korzenia drzewa atrybutów oznacza nazwę kolekcji danych. Poniżej przedstawiono składnię wyrażeń z użyciem drzew atrybutów:

η ::= etykieta wyrażenia

$name etykieta do danych znajdującego się w przestrzeni nazw kolekcji

name etykieta węzła

A, B ::= wyrażenie arytmetyczne

true pod-drzewo dla bieżącego węzła

[ ]

A

η lokalizacja bieżącego węzła A, B kompozycja wyrażeń

name = exp przypisanie do węzła o nazwie name wartości wyrażenia

arytmetycznego exp

Od strony implementacyjnej krotka zawiera wartości atrybutów zapisane w liście indeksowanej od 0, na której zdefiniowano operacje zapisu i odczytu. Drzewo atrybutów mapuje wartości pomiędzy węzłami a komórkami listy. Aby zrozumieć działanie drzewa atrybutów, na rys. 3.2 przedstawiono przykładowy schemat krotki zawierający informacje o nazwie oraz położeniu alarmu wygenerowanego przez system monitorujący. Każdy z węzłów jest opisany przez trzy wartości: pierwsza oznacza nazwę węzła, druga jego typ, trzecia jest indeksem wskazującym na komórkę listy, gdzie przechowywana jest wartość atrybutu. Innymi słowy, jeżeli zapytanie korzysta z wartości węzła I.place.x. Kompilator pobiera wartość indeksu dla tego węzła, następnie dla każdej krotki odczytywana jest wartość elementu listy o indeksie 1. n a m e S T R I N G 0 I N O N E x D O U B L E 1 y D O U B L E 2 p la c e N O N E

Rys. 3.2. Przykładowe drzewo atrybutów

Z każdym węzłem jest skojarzona przestrzeń nazw zawierająca etykiety węzłów do niej należących. W trakcie kompilacji przestrzenie nazw są odkładane

na stosie. W danym momencie są dostępne tylko etykiety należące do bieżącej przestrzeni nazw. U spodu leży bazowa przestrzeń, która zawiera etykiety do kolekcji danych np. strumieni. Jeżeli wybierzemy kolekcję danych o nazwie I wtedy na stos zostaje odłożona przestrzeń nazw zawierająca etykiety, które opisują elementy kolekcji danych I, ponadto przestrzeń ta staje się bieżącą przestrzenią nazw. Jeżeli etykieta jest poprzedzona znakiem ‘$’, wtedy następuje skok do bazowej przestrzeni nazw.

Powróćmy do przykładu z rys. 3.2. Zarówno węzeł jak i całe poddrzewo może być operandem wyrażeń. Ścieżka do węzła składa się z etykiet obiektów oddzielonych kropką. Składnię tą zaczerpnięto z języków obiektowych, gdzie istnieje pod nazwą ‘dot-sytax’. Jeżeli chcemy pobrać wartość węzła x należy zbudować ścieżkę

I.place.x. Wyrażenie I.place.x jest poprawne pod warunkiem, że bieżącą przestrzenią

nazw jest przestrzeń bazowa. Aby uniezależnić się od bieżącej przestrzeni nazw należy podać ścieżkę: $I.place.x, ponieważ znak ‘$’ determinuje przeglądanie bazowej przestrzeni nazw. Jeżeli operandem ma być drzewo należy użyć składni [true]. Przykładowo zapis $I.place[true] oznacza drzewo o korzeniu w węźle place.

Podstawową czynnością podczas budowania drzewa atrybutów jest operacja zapisu wartości do węzła, do jej realizacji służy składnia name = exp. Jeżeli w bieżącej przestrzeni nazw nie istnieje węzeł o nazwie name, wtedy jest tworzony nowy węzeł o typie identycznym jak typ wyrażenia exp. Następnie do węzła jest zapisana wartość wyrażenia. Jeżeli w bieżącej przestrzeni nazw istnieje węzeł name, wtedy weryfikowane jest czy typ zapisany w węźle jest zgodny z typem zwracanym przez wyrażenie exp. Jeżeli istnieje niezgodność, kompilacja jest przerwana z odpowiednim komunikatem błędu. Aby w bieżącej przestrzeni nazw zbudować węzeł x o wartości 1 należy zapisać x = 1. Wyrażenia przypisania można łączyć ze sobą w większe ciągi przy użyciu składni kompozycji. Stworzenie węzłów x i y z wartościami odpowiednio

1 i 0 sprowadza się do zapisu x = 1,y = 0. Na zakończenie chcąc zbudować węzeł place

zawierający wcześniej zdefiniowane węzły x i y, należy skorzystać ze składni lokalizacji: place[x=1,y=0]. Jeżeli do lokalizacji zastosujemy składnię ‘dot-sytax’, wtedy otrzymamy zapis: place.x=1, place.y=0. Ze względu na złożoność zapisu pierwsza wersja jest czytelniejsza. Warto zauważyć, że po zdefiniowaniu węzła można użyć go w następujących później wyrażeniach. Jako przykład zbudujmy drzewo atrybutów d,

które zawiera węzły x=2, y=1 oraz wartość średnią zapisaną w węźle avg: d[x=2,y=1,

avg=(x+y)/2].

W językach programowania typy danych dzielimy na proste oraz złożone, czyli struktury zbudowane z typów podstawowych. Podczas kompilacji każda etykieta zmiennej jest uzupełniana o typ. Następnie po naniesieniu typów na drzewo rozbioru składniowego sprawdzana jest poprawność semantyczna programu. W językach programowania szeroko stosowane są typy nazwane. Rozwiązanie to polega na tym, że każda struktura złożona posiada swoją nazwę jednoznacznie ją identyfikującą. Takie podejście upraszcza konstrukcję kompilatora, ponieważ weryfikacja zgodności typów jest realizowana poprzez zgodność nazw typów. Z drugiej strony może zaistnieć sytuacja, że w programie będą istniały dwie identyczne struktury z nazwami A i B. Pomimo to funkcja posiadająca argument typu A, nie może zostać wywołana z argumentem typu B. Aby wyliczyć wartość funkcji dla argumentu typu B, należy go wpierw skonwertować do typu A. Postać definicji typów złożonych ma wpływ na organizację i czytelność kodu źródłowego oraz narzuca sposób korzystania ze wzorców projektowe. W językach programowania korzystamy z typów nazwanych, dlatego ogromne znaczenie ma zdefiniowanie współdzielonych typów podczas tworzenia aplikacji w zespole wieloosobowym. W przeciwnym wypadku stworzone oddzielnie fragmenty programu będą musiały intensywnie korzystać z konwersji typów. Aby przyspieszyć tworzenie aplikacji konieczne jest korzystanie z gotowych komponentów takich jak: biblioteki, technologie i platformy. W tym momencie program może posiadać wiele typów oznaczających podobne struktury, co skutkuje mniejszą czytelnością kodu oraz wieloma konwersjami typów. Relacyjne bazy danych są zorientowane na obsługę ograniczonej liczby typów. Inne struktury danych należy zapisać w postaci binarnej. Wiąże się to z ograniczeniem, że nie można na binarnym typie definiować warunków zapytania. Taka konstrukcja bazy danych sprawia, że tworzenie nowych typów danych przez użytkownika jest mało atrakcyjne w odniesieniu do rozwiązań oferowanych w językach programowania. Zgodnie z wymaganiem 1) strumieniową bazę danych należy postrzegać jako platformę, do której można dołączyć własne operatory w celu uzupełnienia jej funkcjonalności. Dodawanie nowych operatorów bez możliwości konstruowania własnych typów jest rozwiązaniem nie pełnym. W odróżnieniu do typów nazwanych, w języku StreamAPAS zastosowano podobieństwo strukturalne. Idea polega na tym,

że kompilator sprawdza czy w danych drzewie atrybutów znajdują się elementy wymagane przez funkcję. Przyjmijmy, że chcemy zdefiniować typ opisujący punkt w przestrzeni 2D. Korzystając z podobieństwa strukturalnego taki typ można zdefiniować na kilka sposobów. Przykładowo takim typem jest drzewo atrybutów, którego dwa pierwsze pod-węzły są typu Integer. Dopuszczalna jest również definicja, że jest to drzewo atrybutów z pod-węzłami typy Integer o nazwach: x i y. Warto zauważyć, że obie definicje dopuszczają istnienie drzewa atrybutów z większą liczbą węzłów. Dla tak stworzonej definicji, drzewo o korzeniu w węźle place na rys. 3.2 jest zgodne z definicją punktu 2D. Od strony implementacyjnej, podobieństwo semantyczne jest implementowane przez użytkownika w języku Java. Pozwala to na zdefiniowanie bardziej złożonych zasad. Przykładowo funkcja dopuszcza tylko struktury z dwoma danymi typu Integer, gdyż trzy typy Integer oznaczałyby punkt w trój-wymiarze.

Podsumowując, posługiwanie się drzewem atrybutów przez użytkownika jest niewiele trudniejsze niż definiowanie prostych wyrażeń w SQL. Składnia drzewa atrybutów umożliwia użytkownikowi definiowanie struktury danych oraz planu obliczeń w jednym miejscu. Sprawia to, że zapis jest zwięzły i czytelny. Dzięki temu, że operandem może być fragment drzewa atrybutów, można uniknąć potrzeby podawania długiej listy atrybutów, tak jak to ma miejsce w języku SQL. Dużym ograniczeniem w swobodnym korzystaniu z gotowych bibliotek programistycznych jest mnogość podobnych typów. W języku StreamAPAS zastosowano drzewo atrybutów oraz podobieństwo strukturalne. Takie połączenie sprawia, że pojedyncze drzewo atrybutów może być postrzegane, jako instancja wielu typów. To nowatorskie podejście ma na celu otworzyć język zapytań na dodawanie nowej funkcjonalności a ograniczyć trudności związane z zarządzanie wieloma typami. Inną korzyścią tego podejścia jest zmniejszenie liczby funkcji, które konwertują typy danych, ponieważ nie ma konieczności konwertować danych strukturalnie podobnych.