I.
Wprowadzenie do  języków zapytań (1)
II.
Wprowadzenie do  języków zapytań (2)
III.
Pojęcia obiektowości w bazach danych (1)
IV.
Pojęcia obiektowości w bazach danych (2)
V.
Podstawy semantyczne języków zapytań
VI.
Modele składu obiektów
VII.
Stos środowisk, rezultaty zapytań, funkcja nested
VIII.
Język SBQL (Stack-Based Query Language) (1)
IX.
X.
Dalsze własności SBQL
XI.
Operatory order by i group by
XII.
Przetwarzanie struktur nieregularnych
XIII.
Rozszerzenie języków zapytań o konstrukcje imperatywne
  Wstęp
  1. Konstrukcje deklaratywne i imperatywne
  2. Zasada korespondencji
  3. Elementarne konstrukcje językowe w językach imperatywnych
  Podsumowanie
  Zadania
XIV.
Procedury, procedury funkcyjne, metody, reguły zakresu
XV.
Parametry procedur i metod, procedury rekurencyjne, optymalizacja poprzez modyfikację zapytań

 

3. Elementarne konstrukcje językowe w językach imperatywnych

Celem tego podrozdziału jest omówienie tych konstrukcji imperatywnych, które mogą mieć znaczenie przy budowie języka programowania opartego na języku zapytań. Ponieważ konstrukcje imperatywne istnieją w tysiącach różnorodnych języków programowania, wynajdywanie nowych rozwiązań jest niepotrzebne. Będziemy raczej oceniać popularne i powszechnie używane konstrukcje, takie jak if...then...else..., while...do... itd., w celu ustalenia ich przydatności i ewentualnych modyfikacji w przypadku języka programowania opartego na zapytaniach.


3.1. Operator tworzenia obiektu

Mamy do czynienia z dwoma rodzajami takiego operatora: deklaracją obiektu, czyli utworzeniem obiektu w momencie inicjacji pewnej abstrakcji programistycznej (np. metody), lub dynamicznym utworzeniem obiektu na polecenie wydane explicite przez programistę. W tym drugim przypadku operator musi być dodatkowo parametryzowany miejscem, w którym dany obiekt ma być utworzony, oraz jego statusem trwałości. Operator ten powinien być makroskopowy, tj. wykonanie jednego zdania może utworzyć wiele obiektów. Odmiana tego operatora powinna również dawać możliwość utworzenia nowej danej elementarnej oraz nowej danej pointerowej wewnątrz określonego obiektu. Inna odmiana operatora powinna dawać możliwość utworzenia nowego modułu; w naszym ujęciu moduł jest też obiektem. Dla modeli z możliwością dynamicznego dodawania klas powinna istnieć możliwość utworzenia nowej klasy w określonym module. Generalnie, dla dowolnego elementu składu obiektów, np. metody, perspektywy, procedury, ograniczenia, trygera itd. powinna istnieć odmiana tego operatora pozwalająca na utworzenie tego elementu w określonym środowisku.

Z operatorem tworzenia obiektu musi być skojarzony operator jego usuwania: wszystko, co może być utworzone, kiedyś może być usunięte. Dotyczy to zwłaszcza obiektów bazy danych, na dowolnym poziomie agregacji. W przypadku obiektów deklarowanych jego usunięcie powinno nastąpić wraz z zamknięciem środowiska, wraz z którym został utworzony, np. w momencie wyjścia sterowania programu z procedury i zlikwidowania odpowiadającej jej sekcji na stosie ENVS. Usuwanie może być opóźnione, gdyż natychmiastowe usunięcie uniemożliwi programiście zwrócenie referencji do lokalnego obiektu jako wyniku funkcji lub metody funkcyjnej. Pozostawienie tej możliwości oznacza konieczność zaimplementowania efektywnego odśmiecacza (garbage collector), który usunie obiekt w momencie, gdy nie prowadzi do niego żadna referencja. Taki odśmiecacz jest problemem implementacyjnym i wydajnościowym. W Loqisie nie zdecydowaliśmy się na implementację takiego odśmiecacza i z tego powodu zabroniliśmy w nim zwracania referencji do lokalnych obiektów przez funkcję lub metodę (jest to kontrolowane).

Operator tworzenia nowego obiektu musi być skojarzony ze środowiskiem, w którym obiekt ma być utworzony. Takie środowisko, identyfikowane przez jego referencję, może być bezpośrednio zakomunikowane jako parametr instrukcji tworzenia (explicite lub implicite) lub może wynikać z miejsca, w którym wywołano tę instrukcję. W szczególności, jeżeli miejscem wywołania jest ciało pewnej procedury, funkcji lub metody, to domyślnie nowy obiekt jest wstawiany do jej lokalnego środowiska.

Jeżeli utworzony został nowy obiekt, to istotne jest uaktualnienie stosu ENVS poprzez wstawienie tam - w razie potrzeby - odpowiedniego bindera do nowego obiektu. Technicznie oznacza to, że dla każdego środowiska reprezentowanego przez sekcję stosu ENVS pamiętana jest także referencja (lub referencje) do środowiska (środowisk) składu obiektów, które były podstawą skonstruowania danej sekcji stosu. Następnie po utworzeniu obiektu porównuje się referencję do środowiska, wewnątrz którego został utworzony nowy obiekt, z referencjami do wszystkich środowisk reprezentowanych na stosie ENVS. Jeżeli dla danej sekcji referencje są identyczne, wówczas do tej sekcji wstawia się odpowiedni binder, Rys.72. Przykładowo, jeżeli tworzymy lub wstawiamy nowy obiekt Stan z referencją i765 do wnętrza obiektu Pracownik z identyfikatorem i9, wówczas przeglądamy kolumnę "Referencje do środowisk" stosu ENVS, następnie znajdując tam również referencję i9 dostawiamy do tej sekcji w kolumnie "Bindery" binder Stan(i765). Czynność tę wykonujemy dla każdej sekcji, która zawiera referencję do środowiska i9. Możliwe są oczywiście sekcje nie posiadające referencji do środowisk, np. sekcja ustalająca środowisko obiektu pointerowego (we wnętrzu którego nie możemy utworzyć obiektu).

70
Rys.72. Referencje do środowisk na stosie ENVS

Przy optymalizowanej wersji stosu ENVS (zaimplementowanej w systemie Loqis) w istocie pamiętana jest tylko pierwsza kolumna, jeżeli zawiera referencje do środowisk, i druga kolumna w pozostałych przypadkach. To oznacza, że operacje, tworzenia, wstawiania lub usuwania obiektu nie wymagają w większości jakichkolwiek działań na stosie ENVS.

Podczas lub po utworzeniu obiektu należy zapełnić go wartościami (inicjalizacja) i ewentualnie połączyć związkami pointerowymi z innymi obiektami. Można to zrobić jednocześnie w instrukcji tworzenia obiektu lub utworzyć obiekt z wartościami przypadkowymi (domyślnymi), a następnie zaktualizować zgodnie z potrzebą. Wartości domyślne obiektu mogą być umieszczone w jego klasie.

Po utworzeniu nowego obiektu powinna być znana jego referencja w programie, który go utworzył. Referencja ta ma na celu umożliwienie wykonania dalszych czynności na utworzonym obiekcie, np. inicjalizację, wstawienie go do innego obiektu itd. Najprostszym sposobem uzyskania tej referencji jest potraktowanie instrukcji tworzenia obiektu jak funkcji z efektem ubocznym, której wynikiem właściwym jest referencja (referencje) do obiektu, zaś efektem ubocznym - utworzony obiekt.

Najprostszą formą syntaktyczną instrukcji tworzenia obiektu (zrealizowaną w nieco odmiennej wersji w systemie Loqis) jest:

zapytanie ::= instrukcja_create
instrukcja ::= instrukcja_create
instrukcja_create ::= create [gdzie] zapytanie;
gdzie ::= local | temporary | permanent

Zapytanie będące argumentem instrukcji create może zwrócić bag. Zostanie wówczas utworzonych tyle obiektów, ile elementów ma ten bag (w szczególności ani jednego, jeżeli bag jest pusty). Wynikiem instrukcji jest referencja (bag referencji) do utworzonych obiektów. Nazwy tworzonych obiektów oraz ich podobiektów są określone przez odpowiednie bindery będące wynikiem tego zapytania. Dla wszystkich referencji zwróconych przez to zapytanie jest wykonywana automatycznie dereferencja. W ten sposób można utworzyć dowolnie złożony obiekt (obiekty), o dowolnej liczbie poziomów hierarchii.

Miejsce tworzenia obiektu jest ustalone przez kwalifikator gdzie. Kwalifikator local (lub domyślnie brak kwalifikatora) powoduje, że obiekt jest tworzony w środowisku lokalnym (procedury, funkcji, metody lub bloku), w którym została wykonana ta instrukcja. Kwalifikator temporary oznacza, że obiekty są tworzone dla danej sesji użytkownika; obiekty te są dostępne podczas trwania sesji i są automatycznie usuwane po jej zamknięciu. Kwalifikator permanent oznacza, że obiekty są tworzone w bazie danych jako obiekty korzeniowe. Możliwe jest oczywiście zastosowanie bardziej wyrafinowanego systemu, w którym programista ma możliwość precyzyjnego wskazania, gdzie należy wstawić utworzone obiekty. W systemie Loqis przyjęliśmy, że takie możliwości ma instrukcja insert, przesuwająca obiekt do wnętrza dowolnie wybranego innego obiektu.

Poprzez dodanie nowej prostej konstrukcji do SBQL można również w ten sposób tworzyć obiekty pointerowe. Załóżmy składnię

zapytanie ::= ref zapytanie

i przyjmijmy, że w każdej sytuacji (nie tylko związanej z create) ref q zwraca referencję r (lub wiele referencji) zwróconą przez zapytanie q, która nie podlega dereferencji. Technicznie, referencja r jest opatrzona specjalną flagą, która zabrania użycia dereferencji i zabrania automatycznego wywołania procedury, funkcji lub metody poprzez referencję r z tą flagą. Jeżeli instrukcja create napotka referencję z taką flagą, wówczas tworzy obiekt pointerowy.

P13.1.

Patrz Rys.55. Instrukcja create tworzy w trwałym składzie obiekt pracownika Kowalskiego, po zatrudnieniu go w dziale marketingu.

create permanent (
     2453456 as NrP,
     "Kowalski" as Nazwisko
     "analityk" as Stan,
     2350 as Zar,
     (ref Dział where Nazwa = "Marketing") as PracujeW
) as Prac


P13.2.

Instrukcja create tworzy lokalnie od zera do dowolnej liczby obiektów pointerowych z nazwą analityk, prowadzących do obiektów Prac. Następnie instrukcja for each każdemu w ten sposób wyselekcjonowanemu analitykowi podnosi zarobek o 100.

create (ref Prac where Stan = "analityk") as analityk;
for each analityk.Prac do Zar := Zar +100;

Kolejnym zagadnieniem, które należy rozstrzygnąć przy tworzeniu nowego obiektu jest podłączenie go do klasy. Można to zrobić poprzez dodatkową składnię związaną z instrukcją create, np.:

instrukcja_create ::= create [gdzie] zapytanie as member of nazwaKlasy

Przyjmujemy tu, że wszystkie klasy są bytami pierwszej kategorii dostępnymi do wiązania w środowisku, w którym wydana została ta instrukcja. Skutkiem jest wytworzenie specyficznego związku pointerowego pomiędzy utworzonym obiektem a jego bezpośrednią klasą, który jest używany przez procedurę eval dla załadowania binderów do wnętrza klasy wraz z binderami do wnętrza obiektu, tak jak to było objaśnione dla modeli M1 i M2.

W niektórych systemach do utworzenia nowego obiektu używa się specjalnej metody o wyróżnionej nazwie, zwykle new. Metoda ta jest predefiniowana i parametryzowana nazwą klasy. Obiekt tworzony jest w tym samym środowisku, w którym taka metoda jest wywołana. Istnieje spora liczba różnych decyzji dotyczących tej metody, w szczególności:

  • Metoda new może być predefiniowana i obligatoryjna dla każdej klasy, lub może być implementowana przez programistę na normalnych zasadach.

  • Metoda new może tworzyć obiekt predefiniowany, zawsze taki sam, który następnie programista musi zainicjować, lub może być parametryzowana wartościami inicjalizującymi.

  • Metoda new może korzystać z wartości domyślnych zapisanych wewnątrz klasy. Te wartości nie muszą być identyczne z wartościami domyślnymi dziedziczonymi przez obiekt w sytuacji, gdy brakuje mu pewnych atrybutów.


Wątpliwości budzi przypisanie metody new do klasy tworzonego obiektu. W modelach M1-M3 nie ma takiej możliwości, gdyż tam metody są inwariantami obiektów dziedziczonymi przez każdy obiekt tej klasy, natomiast metoda new nie może być w ten sposób dziedziczona. Zatem wprowadzenie metody new wiąże się z nowym modelem składu. Metoda new może być rozumiana jako metoda operująca na środowisku, w którym jest wywołana i posiadająca parametr w postaci klasy, lub odwrotnie, może być uważana za metodę należącą do metaklasy (klasy, której instancjami są klasy) i parametryzowana lokalnym środowiskiem. W zależności od punktu widzenia, własności semantyczne metody i jej użycia mogą być nieco inne. Inny punkt widzenia zakłada, że metoda ta należy do klasy potęgowej (power-set-of-class), tj. takiej klasy A, której instancjami są obiekty zawierające kolekcje obiektów klasy B. W takim przypadku metoda ta nie wnosi nic nowego i może być normalnie zdefiniowana przez programistę, o ile sobie tego życzy. Metoda ta w swoim wnętrzu musi wtedy używać któregoś z wariantów instrukcji create. W tym wykładzie przyjmiemy ostatni punkt widzenia.


3.2. Operator podstawienia

 Operator podstawienia możemy zrealizować w klasycznym wariancie.

instrukcja ::= zapytanie := zapytanie

Podstawienie ma postać l-value := r-value gdzie l-value (lewa wartość) jest  wyrażeniem zwracającym referencję, zaś r-value (prawa wartość) jest wyrażeniem zwracającym wartość (powstałą po ewentualnej dereferencji). Semantyka tego operatora jest standardowa: dokonuje zamiany bieżącej wartości obiektu posiadającego identyfikator zwrócony przez l-value na wartość określoną przez r-value. Istotą tego operatora (w stosunku np. do złożenia operatorów usuwania i tworzenia) jest to, że nie zmienia identyfikatora obiektu, na który podstawia się nową wartość. Operator tego typu jest zrealizowany w SQL w postaci klauzuli update. Nie będziemy jednak odwoływać się do przyjętej tam składni, gdyż naszym zdaniem jest ona kontrowersyjna.

Operator podstawienia w wersji języka zapytań prowadzi do konieczności ustalenia następujących aspektów:

  • Czy ma być makroskopowy, tj. czy l-valuer-value mogą być określane przez zapytania zwracające bagi lub sekwencje?

  • Czy możliwe będzie podstawienie na obiekt złożony?

  • Czy możliwe będzie podstawienie na obiekt pointerowy?


W systemie Loqis i w tym wykładzie przyjęliśmy, że odpowiedzi na te pytania muszą być pozytywne, inaczej złamana będzie zasada korespondencji.

Jeżeli chodzi o pierwsze pytanie, przyjęcie składni q1 := q2 jest nieakceptowalne w sytuacji, gdy q1q2 zwracają bagi, ponieważ nie będzie wiadomo, która wartość ma być podstawiona na którą referencję. Taka składnia jest również nieakceptowalna w sytuacji, gdy q1q2 zwracają sekwencje, gdyż zmusza to programistę do starannej kontroli, czy rozmiar sekwencji i wzajemne dopasowanie poszczególnych ich elementów są w pełni spójne. W sytuacji, gdy mamy do czynienia z nieregularnymi danymi, taka kontrola staje się dużym utrapieniem. Składnia ta jest również niekorzystna z tego powodu, że w wielu przypadkach zmusza programistę do powtarzania kodu (np. warunku where) w q1q2 oraz zmusza system do podwójnej ewaluacji tego kodu. Są to powody, dla której takiej konstrukcji nie będziemy proponować.

Pozostają dwa rozwiązania:

  • Nie dopuszczamy podstawienia makroskopowego: w konstrukcji  q1 := q2 zapytanie q1 musi zwrócić dokładnie jedną referencję, zaś zapytanie q2 musi zwrócić dokładnie jedną wartość. Makroskopowość podstawienia będzie osiągnięta poprzez konstrukcję for each.

  • Wprowadzamy nowy operator update z parametrem będącym pojedynczym zapytaniem. Zapytanie to zwraca bag struktur dwuelementowych, gdzie pierwszy element każdej struktury jest traktowany jako l-value, zaś drugi element jest traktowany jako r-value. Rozwiązanie to zostało zaimplementowane w systemie Loqis.


Zaletą drugiego rozwiązania jest to, że całość danych do podstawienia może być przesłana jako jeden parametr procedury/metody oraz zwrócona jako pojedynczy wynik procedury/metody. Zaletą pierwszego rozwiązania jest syntaktyczna zgodność z dotychczasowymi, dobrze rozpoznanymi konstrukcjami języków programowania, takich jak Pascal, C/C++, Java itd.

P13.3.

Patrz Rys.55. Podwyższ o 100 zarobek wszystkim programistom:

for each Prac where Stan = "programista" do Zar := Zar + 100;

Alternatywnie:

update
(Prac where Stan = "programista") . (Zar, Zar + 100);

Druga forma wydaje się mniej naturalna, ale naszym zdaniem jest to tylko pierwsze wrażenie. W systemie Loqis używana była chętniej niż for each.

P13.4.

Podwyższ o 100 zarobek wszystkim programistom i zmień im stanowisko na "inżynier":

for each Prac where Stan = "programista" do {
   Zar := Zar + 100;
   Stan := "inżynier";
};

Forma alternatywna wymaga wprowadzenia bagów z typologicznie niekompatybilnymi strukturami:

update (Prac where Stan = "programista") . bag((Zar, Zar + 100), (Stan, "inżynier"));

Podstawienie na daną pointerową można zdefiniować za pomocą wprowadzonego już kwalifikatora ref, który informuje, że chodzi o referencję.

P13.5.

Przenieś wszystkich programistów do działu Nowaka:

for each Prac where Stan = "programista" do
PracujeW := ref Dział where (Szef.Prac.Nazwisko) = "Nowak";

Alternatywnie:

update
(Prac where Stan = "programista") .
(PracujeW, ref (Dział where (Szef.Prac.Nazwisko) = "Nowak"));

Podstawienie na obiekt złożony przypomina instrukcję create. Identyfikator obiektu, na który się podstawia, nie ulega zmianie. Wszystkie podobiekty tego obiektu są usuwane, następnie tworzone są nowe podobiekty na podstawie prawej strony podstawienia. Powinna ona zawierać strukturę z binderami; bindery wyznaczają nazwy tworzonych podobiektów.

P13.6.

Wstaw nowe dane Nowaka:

(Prac where Nazwisko = "Nowak") :=  (
     4536 as NrP,
     "Nowak" as Nazwisko,
     "analityk" as Stan,
     2500 as Zar,
     (   "Radom" as Miasto,
          "Nowa" as Ulica,
          76 as NrDomu) as Adres,
     ( ref Dział where Nazwa = "Magazyn") as PracujeW );

Alternatywna forma podstawienia wygląda podobnie. Można byłoby w tym miejscu zastanowić się nad alternatywną postacią operatora as, która umożliwiałaby częściej spotykaną formę składniową, w której nazwy danych są zapisywane przed ich wartościami. Np. zapytanie q as n można zapisać n(q). Takie rozwiązanie zostało przyjęte w Loqisie. W tym przypadku ostatni przykład zyskałby bardziej czytelną postać syntaktyczną:

P13.7.

Wstaw nowe dane Nowaka (q as n zmieniono na n(q):

(Prac where Nazwisko = "Nowak") := (
     NrP(4536),
     Nazwisko("Nowak"),
     Stan("analityk"),
     Zar(2500),
     Adres( Miasto("Radom"), Ulica("Nowa"),  NrDomu(76) ),
     PracujeW( ref Dział where Nazwa = "Magazyn"));

W Loqisie na dany obiekt można podstawić zawartość innego obiektu. Drobnym problemem technicznym jest definicja dereferencji dla złożonego obiektu. Przykład P13.7 wskazuje pomysł na taką definicję. Struktura binderów znajdująca się w przykładzie P13.7 po prawej stronie podstawienia może być uważana za rezultat dereferencji zastosowanej do referencji do obiektu Nowaka. Przykładowo, jeżeli mamy obiekt:

P13.8.

< i9 , Prac , {  
     < i10, Nazwisko, "Barski" >,
     < i11, Zar, 900 >,
     < i12, Adres, {
          < i13, Miasto, "Radom" >,
          < i14, Ulica, "Wolska" >,
          < i15, NrDomu, 12 > } >,
     < i16, PracujeW,  i22 > } >

to rezultat dereferencji deref( i9 ) ma postać następującej struktury binderów:

P13.9.

struct{ Nazwisko( "Barski" ),
     Zar( 900 ), Adres( struct{Miasto("Radom"),
                                         Ulica("Wolska"), NrDomu(12)},
     PracujeW ( ref i22 ) }         

gdzie ref jest wprowadzoną uprzednio flagą nadawaną przez operator ref. Dzięki takim definicjom jedynym skutkiem podstawienia:

P13.10.

(Prac where Nazwisko="Nec") := (Prac where Nazwisko="Nec")        

jest to, że podobiekty obiektu Barskiego mogą zmienić swoje identyfikatory.

Niektóre języki wprowadzają odmiany operatora podstawienia w postaci   x += y (co znaczy x := x+y) i x -= y (co znaczy x := x - y) itd. Semantyka tych operatorów jest oczywista, wobec czego ich wprowadzenie zależy wyłącznie od chęci i wyczucia twórcy konkretnej implementacji języka.


3.3. Operator wstawiania obiektu

Operator ten powinien umożliwiać wstawianie makroskopowe, w stylu klauzuli insert języka SQL. Jego istotą jest przesuwanie istniejących obiektów do środka innego obiektu bez zmiany identyfikatorów przestawianych obiektów. Operator powinien działać na wszystkich typach obiektów, włączając obiekty złożone, klasy, metody, perspektywy itd. Dla modelu M2 powinna istnieć odmiana tego operatora pozwalająca podłączyć do istniejącej roli nową rolę. Instrukcja powinna również pozostawić w stanie spójnym stos środowiskowy.

Składnia tego operatora może być następująca:

instrukcja ::= zapytanie :< zapytanie
instrukcja ::= gdzie :< zapytanie

lub

instrukcja ::= insert zapytanie

Ponieważ semantycznie operator ten bardzo przypomina podstawienie, warto zadbać o to, aby formy syntaktyczne tych dwóch konstrukcji były podobne. Wybrane przez nas formy są zgodne: operator :< jest analogiczny do :=, zaś słowo kluczowe insert jest analogiczne do update. Działanie tego operatora jest następujące. W konstrukcji q1 :< q2 zapytanie q1 musi zwrócić pojedynczą referencję, zaś q2 - jedną referencję lub bag referencji. Obiekty z referencjami zwróconymi przez q2 są przesuwane (bez żadnych zmian i kopii) do obiektu, którego referencja jest zwrócona przez q1. W konstrukcji insert q zapytanie q musi zwrócić strukturę dwuelementową lub bag struktur dwuelementowych, gdzie w każdej strukturze obydwa elementy są referencjami. Pierwsza z nich jest referencją do obiektu, do którego się wstawia, zaś druga do obiektu, który się wstawia, podobnie jak w konstrukcji q1 :< q2. Jeżeli operator próbuje wstawić dany obiekt do swojego własnego wnętrza, wówczas jest to błąd wykonania.

P13.11.

Jeżeli dowolny obiekt analityka nie ma atrybutu Zar, to wstaw mu do środka taką daną z wartością 1000. Dwie alternatywne formy:

for each (Prac where Stan = "analityk" and count(Zar) = 0) as p do {  p :< create 1000 as Zar; }

for each (Prac where Stan = "analityk" and count(Zar) = 0) as p do { insert( p, create 1000 as Zar ); }

P13.12.

Dla wszystkich obiektów pracowników wstaw podobiekt zarabiał z wartością będącą konkatenacją aktualnej daty i aktualnego zarobku danego pracownika. Date jest zmienną środowiskową dostępną na ENVS, (string) jest operatorem zamiany dowolnego typu na string, o jest operatorem konkatenacji stringów.

insert (Prac as p) . ( p, create ((string)Date o (string)(p.Zar)) as zarabiał);

Podane przykłady pokazują, że nie jest potrzebna instrukcja wstawiająca do środka obiektu obiekt określony przez zapytanie (w szczególności, obiekt z podstawionymi literałami), gdyż ten efekt daje połączenie instrukcji wstawiania i tworzenia obiektu. W tym przypadku nie jest oczywiście istotne środowisko, w którym dany obiekt się tworzy. Wymaga to pewnego założenia odnośnie do trwałości obiektu, które jest dość proste: obiekt ma status trwałości wynikający wyłącznie ze środowiska, w którym jest ulokowany. Inaczej mówiąc, status trwałości nie jest atrybutem obiektu ani jego identyfikatora, ani też środowiska, w którym został utworzony. Przesuniecie obiektu bazy danych do środowiska lokalnego oznacza, że ten obiekt przestaje być trwały,  i odwrotnie, bez zmiany identyfikatora. Takie założenie zostało zrealizowane w systemie Loqis. Może  być jednak zbyt rewolucyjne i trudne do zrealizowania przy założeniu tradycyjnych technik implementacyjnych, w których jest wyraźnie zaznaczona granica pomiędzy środowiskiem trwałym (dyskiem) a środowiskiem nietrwałym (pamięcią operacyjną). W Loqisie bez większego trudu udało się tę granicę uczynić przezroczystą dla programisty.

Docelowym obiektem operacji przesuwania może być baza danych na najwyższym poziomie hierarchii, środowisko obiektów czasowych sesji użytkownika oraz środowiska lokalne danej procedury. Jak dotąd, nie ma odpowiedniej składni pozwalającej odzyskać identyfikatory tych środowisk. Przypomnimy, że taką składnię wprowadziliśmy przy okazji instrukcji create, opcja gdzie. Jest to oczywiście jedno z wielu rozwiązań syntaktycznych, które tu można wykorzystać. Przyjęcie tego rozwiązania oznacza, że w instrukcji q1 :< q2 zamiast q1 można wstawić słowa kluczowe local, temporarypermanent, które są wiązane odpowiednio do identyfikatora lokalnego środowiska aktualnie wykonywanej procedury, identyfikatora środowiska sesji oraz identyfikatora całej bazy danych. Można oczywiście przyjąć, że w każdej sytuacji słowa kluczowe  są wiązane do wymienionych wyżej identyfikatorów, co oznaczałoby, że są  również dopuszczalne w alternatywnej formie insert.

Niekiedy programiście zależy na tym, aby do środka obiektu wstawić kopię innego obiektu. Skopiowany obiekt oraz wszystkie jego podobiekty będą oczywiście posiadać nowe identyfikatory. Oryginalny obiekt pozostaje niezmieniony. Odpowiednia składnia może mieć następującą postać:

instrukcja ::= insert copy zapytanie
P13.13.

Utwórz obiekt ostatniaAktualizacja z bieżącą datą i wstaw kopię tego obiektu do wnętrza wszystkich obiektów Prac.

create Date as ostatniaAktualizacja;
insert copy (Prac, ostatniaAktualizacja);
delete ostatniaAktualizacja;        

 

 

 



3.4. Operator usuwania obiektu

Operator powinien umożliwiać usuwanie makroskopowe, w stylu klauzuli delete języka SQL. Dotyczy obiektów tworzonych (nie deklarowanych) przez programistę i oznacza usunięcie obiektu z danego środowiska. Przyjmuje się zwykle, że obiekty deklarowane nie podlegają operacji usuwania explicite przez programistę. Są automatycznie usuwane w momencie, gdy sterowanie opuści dane lokalne środowisko (blok, procedurę, metodę, funkcję, perspektywę). Jak poprzednio, operator usuwania powinien dotyczyć wszystkich rodzajów obiektów, w tym obiektów atomowych, pointerowych, złożonych, klas, metod, perspektyw itd. Składnia tego operatora jest następująca:

instrukcja ::= delete zapytanie

Zapytanie będące argumentem operatora delete powinno zwrócić bag referencji (można to uogólnić na dowolny bag lub strukturę danych zawierającą referencje, ewentualnie występujące w zagnieżdżonych binderach). Obecność referencji do obiektów deklarowanych należy traktować jako błąd. Instrukcja powinna również pozostawić w stanie spójnym stos środowiskowy.

P13.14.

Usuń "Sopot" z lokacji działu marketingu.

delete (Dział where Nazwa = "Marketing").
                        (Lokacja as x where x = "Sopot").x

Niektórzy autorzy lansują pogląd, że operator usuwania nie powinien być w ogóle dostępny programiście, gdyż może prowadzić do tzw. zwisających pointerów (dangling pointers), czyli pointerów prowadzących do śmieci lub do przypadkowych nowo utworzonych obiektów. Programista ma za zadanie usunąć wszelkie pointery prowadzące do obiektu,  a wtedy obiekt stanie się niedostępny, wobec czego będzie automatycznie usunięty przez odśmiecacz. Ten pogląd wynika z bardzo specyficznego rozumienia pojęcia pointera, takiego, który obowiązuje w C/C++. Jest również konsekwencją niedoceniania roli modelowania pojęciowego w procesie tworzenia oprogramowania. Jest to pogląd powierzchowny co najmniej z następujących powodów:

  • Jeżeli obiekty pointerowe są dobrze zaimplementowane, to nie może nigdy powstać sytuacja, że pointer prowadzi do nieistniejącego lub fałszywego obiektu. Takie rozwiązanie przyjęto w systemie Loqis, gdzie każdy pointer jest skojarzony z bliźniaczym pointerem zwrotnym. W momencie usunięcia obiektu pointery zwrotne umożliwiają dotarcie do pointerów prowadzących do obiektu i usunięcia ich. Straty pamięci na pointery zwrotne są raczej minimalne.

  • Jeżeli programista musi usunąć wszystkie pointery prowadzące do obiektu, to jego wiedza o danych i związkach tych danych z dziedziną przedmiotową musi być nieporównywalnie większa niż wiedza o samym obiekcie, który ma usunąć. Jego pojęciowy obraz danych musi ulec radykalnemu rozszerzeniu. Zmuszanie w ten sposób programisty - z powodów czysto technicznych - do panowania nad znacznie bardziej rozległą dziedziną modelowania pojęciowego jest koncepcyjną bzdurą.

  • Jeżeli programista zapomni usunąć chociaż jeden pointer, wówczas obiekt, który w świadomości programisty jest już usunięty, pozostanie i będzie uczestniczył w przetwarzaniu. Prowadzi to do błędów słabo wykrywalnych, znacznie groźniejszych niż zwisający pointer (który można wykryć znacznie łatwiej).

  • Programista może nie mieć dostępu do niektórych pointerów wskutek jego ograniczonej perspektywy (view) lub może nie mieć uprawnień do tego dostępu, wobec czego usunięcie wszystkich pointerów stanie się niemożliwe i przez to niemożliwe będzie usunięcie obiektu.

  • W naszej koncepcji nie dopuszczamy wartości null, zatem usunięcie pointera do obiektu oznacza konieczność zaimplementowania tejże operacji delete, tyle że na obiekcie pointerowym. Złamana więc zostaje zasada relatywizmu. Jak programista ma usunąć np. pewien obiekt będący elementem kolekcji, do którego nie prowadzi żaden pointer? (Patrz powyższy przykład.)

  • Podane wyżej argumenty wyraźnie pokazują, że pomysł braku operacji usuwania obiektu explicite jest koncepcyjnym chwastem, który niestety trudno wyrwać ze świadomości specjalistów wywodzących się ze społeczności języków programowania. W społeczności baz danych operacja delete ma dostatecznie dobry autorytet i nie potrzebuje specjalnych uzasadnień.


3.5. Deklaracja i zastosowanie makrosa

Wszelkie operacje związane z tym operatorem są wykonywane podczas kompilacji. W klasycznych językach programowania użyteczność makrosów jest wysoka. Makros jest to nazwany fragment tekstu. Użycie nazwy makrosa jest równoważne użyciu tego tekstu. W przypadku języków zapytań udogodnienie to oznaczałoby konieczność umieszczenia pewnej puli makrosów w trwałym składzie obiektów. Muszą być specjalne środki administracyjne do zarządzania tą pulą (wstawiania lub usuwania makrosów) oraz specjalne środki programistyczne, za pomocą których programista miałby możliwość podłączenia określonego fragmentu tej puli do swojego programu.

Technicznie można to rozwiązać na wiele sposobów. Niżej zaprezentujemy jeden z nich, przy czym nie będziemy zajmować się częścią administracyjną, tj. sposobem, przy pomocy którego programista bądź administrator będzie wstawiał i usuwał makrosy z tej puli. Do tego celu system musi posiadać specjalny interfejs, w dużym stopniu niezależny od definiowanego przez nas języka.

Pula makrosów będzie zrealizowana dwupoziomowo. Na pierwszym poziomie są grupy makrosów związane z konkretną dziedziną aplikacyjną lub aspektem programistycznym. Na drugim poziomie są konkretne makrosy odnoszące się do danej dziedziny lub aspektu. Programista ma do dyspozycji klauzulę include, której argumentem jest nazwa grupy makrosów. Program źródłowy może zawierać dowolną liczbę takich klauzul. Działanie klauzul dotyczy tylko danego tekstu źródłowego; nie przenosi się na inne teksty. Klauzule te nie przechodzą do drzewa syntaktycznego.

P13.15.

Niech pula makrosów ma postać:

kategoriaWieknastolatek( wiek<20 ),
                      młody( wiek>20 and wiek<30 ),
                      wiekŚredni( wiek>30 and wiek<66 ),
                      emeryt( wiek>66 ) );

kategoriaZar( małoZarabia( Zar<1000),
                   średnioZarabia(Zar>1000 and Zar<4000),
                   dużoZarabia(Zar>4000) );

Dla wszystkich mało zarabiających pracowników w średnim wieku zwiększ zarobek o 100:

include kategoriaWiek;
include kategoriaZar;
for each Prac where wiekŚredni and małoZarabia do Zar:= Zar +100;

Podstawienia na wiekŚrednimałoZarabia odbywają się na poziomie tekstu programu, zatem nie maja skutków dla semantyki. Jak widać z podanego przykładu, makrosy mogą być bardzo użyteczne z punktu widzenia modelowania pojęciowego i organizacji programu.

3.6. Operator zmiany nazwy obiektu

W klasycznych językach programowania nazwa obiektu jest drugiej kategorii programistycznej, wobec czego jej zmiana podczas czasu wykonania jest niemożliwa. W większości systemów baz danych nazwy obiektów są pierwszej kategorii, zatem możliwa jest zmiana tej nazwy, pod warunkiem, że ta zmiana jest dopuszczalna w schemacie danych. W niektórych sytuacjach nazwa obiektu przenosi stan obiektu biznesowego, zatem zmiana nazwy, bez zmiany identyfikatora, jest sposobem na zmianę tego stanu. Przykładowo, obiekt o nazwie Pracownik może zmienić swoją nazwę na Emeryt, przez co (w modelu M0) automatycznie staje się elementem innej kolekcji. W modelu M1 ta zmiana może dodatkowo wymagać podłączenia tego obiektu do innej klasy. W modelu M2 w tej sytuacji można powołać nową rolę. Instrukcja zmiany nazwy obiektu powinna pozostawić w stanie spójnym stos środowiskowy.

Zmiana nazwy obiektu może być także potraktowana jako technika programistyczna. Polega ona na tym, że programista tworzy pewne obiekty z nazwą pomocniczą, wykonuje na nich wszelkie niezbędne operacje i w momencie, gdy są już "dojrzałe", zmienia im nazwę, włączając je w ten sposób do docelowej kolekcji.  Zysk polega na tym, że programista operuje na znacznie mniejszej kolekcji, zatem operacje są szybsze, zaś program jest mniej błędogenny. Ponadto istnieją procesy biznesowe, w których włączenie do docelowej kolekcji obiektów "niedojrzałych" jest niewskazane lub niedopuszczalne.

Forma syntaktyczna operatora może być następująca:

instrukcja ::= rename zapytanie to zapytanie

Zakładamy, że pierwsze zapytanie zwraca referencje, drugie zaś dokładnie jeden string lub referencję do stringu. String ten jest traktowany jako nowa nazwa nadawana dla obiektów wskazanych przez pierwsze zapytanie. Możliwe są oczywiście inne odmiany syntaktyczne tego operatora, w szczególności odmiana podobna do operatora update.


3.7. Operatory dotyczące zdarzeń

Zdarzenia i bloki obsługi zdarzeń są nowymi pojęciami programistycznymi w stosunku do pojęć już wprowadzonych. Koncepcja ta zakłada podział pewnego problemu  na część "normalną", wynikającą z naturalnego myślenia projektanta lub programisty, oraz część "wyjątkową", zachodzącą wtedy, gdy sytuacja nie pasuje do normalnej. Ten podział problemu jest umowny, ponieważ analizując dowolny problem biznesowy do nas należy decyzja, co w nim jest "normalne", a co "wyjątkowe". W szczególności, programowanie zdarzeniowe (event-driven programming) traktuje takie wyjątkowe sytuacje jak normalną technikę programistyczną.

Podział na sytuacje normalne i wyjątkowe ma odbicie w organizacji sterowania programu, które zachodzi zgodnie z zadanym algorytmem aż do momentu, gdy w środowisku pojawi się wyjątek. Wówczas sterowanie jest przekazywane do specjalnego bloku obsługi tego wyjątku. W ramach tej obsługi programista może podjąć decyzję, czy sterowanie ma wrócić tam, gdzie było poprzednio, czy też ma być przekazane do innego fragmentu programu. Popularną koncepcją jest zwinięcie stosu środowiskowego aż do miejsca, w którym znajduje się obsługa wyjątku, co oznacza wyjście ze wszystkich procedur, funkcji i metod, aż do tego miejsca. Nie do końca jest pewne, czy ta koncepcja jest we wszystkich sytuacjach spójna.

Literatura na temat rozwiązań w zakresie zdarzeń i ich obsługi jest dość obszerna. Ponieważ nie wydaje się, aby języki zapytań wnosiły do tego tematu nową jakość, w tym wykładzie ograniczymy się tylko do powyższej sygnalizacji zagadnienia.


3.8. Operatory sterujące

Istnieje kilka tego rodzaju operatorów, dobrze znanych z języków programowania.

  • Operator warunkowy:
instrukcja ::= if zapytanie then instrukcje else instrukcje
instrukcja ::= if zapytanie then instrukcje
instrukcje ::= instrukcja | instrukcja; instrukcje
  • Zapytanie powinno zwrócić wartość boolowską. Przypomnimy, że operator ten zdefiniowaliśmy także jako składnik języka zapytań, w postaci  zapytanie ::= if zapytanie then zapytanie else zapytanie.

  • Operator case umożliwia wybór sterowania programu spomiędzy dowolnej liczby możliwości. Typowa składnia tego operatora jest następująca:
instrukcja ::= case zapytanie do etykietyInstrukcje
instrukcja ::= case zapytanie do etykietyInstrukcje else instrukcje
etykietyInstrukcje::= etykietaInstrukcja {, etykietaInstrukcja}
etykietaInstrukcja::= etykieta : instrukcja
etykieta ::= literał

Obliczane jest zapytanie; jeżeli zwróci ono wartość równą etykieta, wówczas wykonywane są opatrzone tą etykietą instrukcje. Jeżeli wartość zwrócona przez zapytanie nie jest wartością jakiejkolwiek etykiety, wówczas wykonywane są instrukcje po else. Etykiety są literałami, czyli ciągami znaków pisanymi bezpośrednio przez programistę, kompilowanymi statycznie i  interpretowanymi jako stringi lub liczby. Możliwy jest również bardziej uniwersalny (ale mniej czytelny) wariant, w którym etykiety są zapytaniami obliczanymi podczas czasu wykonania. 


3.9. Operatory pętli

Najbardziej popularny jest operator

instrukcja ::= while warunek do instrukcje

Operator ten powtarza wyliczenie wyrażenia <warunek> i wykonanie bloku <ciąg instrukcji> aż do momentu, gdy <warunek> będzie nieprawdziwy. W szczególności, jeżeli <warunek> jest na samym początku nieprawdziwy, wówczas <ciąg instrukcji> nie jest wykonywany ani razu. Odmianą tego operatora jest instrukcja repeat:

instrukcja ::= repeat instrukcje until warunek

Najpierw następuje wykonanie bloku <ciąg instrukcji>, następnie wyliczenie wyrażenia <warunek>; jeżeli jest prawdziwy, to sterowanie jest przekazywane dalej, w przeciwnym przypadku powtarzane jest wykonanie bloku <ciąg instrukcji>, z ponownym wyliczeniem wyrażenia <warunek>; itd. Modyfikacja tych operatorów polega na wprowadzeniu specjalnych instrukcji break (lub exit), które mogą być użyte wewnątrz bloku <ciąg instrukcji> i których wykonanie powoduje przekazanie sterowania do instrukcji, znajdującego się za tą konstrukcją. W języku C stosowana jest także instrukcja continue oznaczająca skok do końca ciągu instrukcji.

Obecność konstrukcji break jest przyczyną innej konstrukcji pętli ze składnią:

instrukcja ::= loop instrukcje

gdzie <ciąg instrukcji> jest powtarzany tak długo, aż napotkana zostanie instrukcja break.

Operator pętli może mieć znaczenie dla języka programowania opartego na języku zapytań. W systemie Loqis zdecydowaliśmy się na instrukcję loop:

instrukcja ::= loop instrukcje
instrukcja ::= break

Odmianą operatora pętli jest instrukcja for. Intencją tego operatora jest powtórzenie wykonania pewnego programu dla kolejnych wartości tzw. zmiennej iteracyjnej, przyjmującej wartości z pewnego przedziału numerycznego, np. ze składnią:

instrukcja ::= for zmienna := wartość początkowa
until wartość końcowa do instrukcje

W tej składni początkowa i końcowa wartość tej zmiennej iteracyjnej jest określona z góry przez programistę, zaś przyrost tej zmiennej w poszczególnych cyklach iteracyjnych wynosi 1. Składnia ta została uogólniona na przypadek, kiedy zarówno początkowa, jak i końcowa wartość zmiennej jest określona przez dowolne wyrażenie, zaś przyrost tej zmiennej jest również określony przez wyrażenie. Klasyczna konstrukcja jest znana z języków C i C++:

instrukcja ::= for(zmienna = wyrażenie początkowe; warunek zakończenia pętli; instrukcja przyrostu zmiennej)
                       { instrukcje }

Na początku oblicza się wyrażenie początkowe i podstawia się na zmienna. Przed każdym wejściem w wykonanie bloku instrukcje oblicza się warunek zakończenia pętli. Jeżeli jest fałszywy, to instrukcja for jest zakończona; jeżeli jest prawdziwy, to wchodzi się w instrukcje, zaś po ich zakończeniu wykonuje się instrukcja przyrostu zmiennej, i cykl się powtarza. Dodatkowo zakończenie tego cyklu umożliwia instrukcja break.

Operator for może być łatwo zastąpiony operatorem loop, wobec czego w systemie Loqis nie zdecydowaliśmy się na jego wprowadzenie.


3.10. Operator for each

Intencja tego operatora jest nieco różna od operatora for. Chodzi w nim o wykonanie ciągu instrukcji dla wszystkich elementów pewnej kolekcji, przy czym ta kolekcja jest określona przez zapytanie, zaś element tej kolekcji parametryzuje wykonywany ciąg instrukcji. Istnieje wiele wariantów syntaktycznych tego operatora. W przypadku podejścia stosowego jest naturalne, że parametryzacja ciągu instrukcji następuje poprzez stos środowiskowy. Składnia tego operatora jest następująca:

instrukcja ::= for each zapytanie do instrukcje

Wykonanie tego operatora jest identyczne jak w przypadku operatorów niealgebraicznych. Semantyka konstrukcji for each q do p jest następująca:

  • Najpierw ewaluowane jest q, które powinno zwrócić bag na QRES.

  • Dla każdego elementu r należącego do wyniku zapytania q należy wykonać następujące kroki:

    • Oblicz nested(r) i włóż powstałe w ten sposób bindery jako nową sekcję na wierzchołku ENVS.

    • Jeżeli r jest referencją do obiektu podłączonego do klas, to stos powinien zawierać sekcje tych klas tak, jak wyjaśnialiśmy poprzednio. Jeżeli r jest referencją do roli, to powinny na stosie pojawić się również sekcje jej nadról.

    • Dla tak zorganizowanego ENVS wykonaj instrukcje.

    • Zdejmij ze stosu wszystkie sekcje włożone w poprzednich krokach.

  • Usuń rezultat q z QRES.


Specjalna zmienna iteracyjna nie jest dla tego operatora potrzebna, gdyż można ją powołać jak zwykle operatorem as w ramach zapytania umieszczonego po for each.

P13.16.

Dla wszystkich pracowników zwiększ zarobek o 100, zaś programistom zmień stanowisko na inżynier:

for each Prac do {
     Zar := Zar + 100;
     if Stan = "programista"  then Stan := "inżynier";}

To samo z wprowadzeniem "zmiennej iteracyjnej" (w ramach normalnego zapytania Prac as p):

for each Prac as p do {
     p.Zar := p.Zar + 100;
     if p.Stan = "programista"  then p.Stan := "inżynier";}

 

P13.17.

Dla wszystkich obiektów Prac wstaw podobiekt kierownik zawierający nazwisko szefa.

for each Prac as p join (p.pracujeW.Dział.Szef.Prac) as s do
     insert p, create s.Nazwisko as kierownik;

Powołane zostały w ten sposób dwie "zmienne iteracyjne" (jak poprzednio, w ramach normalnego zapytania).

 

P13.18.

Dla wszystkich działów zlokalizowanych w Olsztynie posortowanych według nazw wydrukuj nazwę, nazwisko szefa  oraz liczbę pracowników.

for each (Dział where "Olsztyn" Í  Lokacja) order by Nazwa do
{    print( "Nazwa działu: " o Nazwa o ", ");
     tab( 60 );
     print ( "Nazwisko szefa: " o (Szef.Prac.Nazwiskoo ", " );
     tab( 120 );
     print( "Liczba pracowników: " o count(Zatrudnia) );
     nowaLinia(); }

"Zmienna iteracyjna" okazała się niepotrzebna. Dla eleganckiego druku użyliśmy dwóch procedur bibliotecznych: tab przesuwa karetkę druku do odpowiedniej pozycji, nowaLinia zmienia linię.


3.11. Iteratory niższego poziomu

Występują w postaci spójnej rodziny funkcji lub metod, niekiedy hermetyzowanej w postaci klasy lub szablonu. Służą do sekwencyjnego przetwarzania (element po elemencie) danej kolekcji. Typowym przykładem są kursory znane z języka SQL lub iteratory znane z C++. Klasyczny zestaw takich operatorów jest następujący (z dokładnością do nazw operatorów): getFirst (ustaw pierwszy element kolekcji jako element bieżący), getNext (ustaw  następny element kolekcji jako element bieżący), getPrior (ustaw poprzedni element kolekcji jako element bieżący) oraz pewien mechanizm (np. w postaci wyjątku NoMoreElements) dla stwierdzenia, że przetwarzanie całej kolekcji jest zakończone. Zwykle takie metody zwracają referencję do bieżącego elementu, która dalej jest przetwarzana za pomocą standardowych metod (nie dotyczy to SQL). W większości przypadków iteratory niższego poziomu można zastąpić konstrukcją for each. Konstrukcja ta jest jednak czasami za mało elastyczna i uniwersalna. Istnieją zadania, które nie da się łatwo zrealizować za pomocą for each; klasycznym przykładem jest algorytm zlania (merging) dwóch posortowanych zbiorów w jeden posortowany zbiór.

Iteratory niskiego poziomu wymagają porządku w przetwarzanym zbiorze. Może to być oczywiście porządek dowolny w przypadku bagów, ale zdeterminowany w tym sensie, że każde uruchomienie iteratora w stosunku do tego samego zbioru zwraca tę samą kolejność elementów. Dla sekwencji porządek obiegu elementów przez iterator jest zdeterminowany tą sekwencją.

Problem z iteratorami niższego poziomu polega na tym, że wymagają one wprowadzenia niejawnego składnika stanu, zwanego "wskaźnikiem bieżącym" (currency indicator). Powstaje pytanie, gdzie taki stan ma być pamiętany. W większości rozwiązań (m.in. w SQL i C++) sposób pamiętania stanu nie jest dobry, gdyż prowadzi do trudności z zagnieżdżonym wywoływaniem iteratorów (kursorów), uniemożliwiając m.in. pisanie programów rekurencyjnych. Według doświadczeń autora jest to szczególnie dokuczliwe w zagnieżdżonym SQL (przy profesjonalnym programowaniu, gdzie kontrolowanie zagnieżdżeń i rekurencji jest dość utrudnione). Rozwiązaniem, które nie wymaga specjalnych środków do przechowywania stanu, jest przyjęcie założenia, że funkcje getNextgetPrior mają zawsze parametr w postaci referencji do elementu, w stosunku do którego pobierany jest następny lub poprzedni element. Jeżeli taki parametr jest odłożony na lokalnej zmiennej, wówczas ta zmienna przechowuje stan iteratora, a wobec jej lokalności nie występują problemy z rekurencją.

Pewnym problemem przy definiowaniu semantyki iteratorów niskiego poziomu jest wykonywanie z ich pomocą operacji usuwania lub wstawiania nowych obiektów do kolekcji. Np. jeżeli usuniemy obiekt za pomocą referencji uzyskanej przez getNext, to powstaje pytanie, gdzie będzie ustawiony wskaźnik bieżący. Złe zaprojektowanie lub nieuważne korzystanie z takich operatorów może spowodować, że pewne obiekty nie będą przetworzone lub będą przetworzone dwa razy.

Iteratory niskiego poziomu, oparte na rozbudowanym systemie wskaźników bieżących, były podstawą koncepcji sieciowego modelu danych i języka DML opracowanego w latach 70-tych przez komitet DBTG CODASYL. Koncepcja ta została totalnie skompromitowana podczas gorącej debaty z autorami i zwolennikiem tworzonego wówczas modelu relacyjnego. Przetwarzanie w stylu języka zapytań, w szczególności algebry relacji, uznano za znacznie lepszą alternatywę. Było w tym nieco racji, ale w większości argumentacja zwolenników modelu relacyjnego była demagogią (w której matematyka odegrała dość niechlubną rolę). Twórcy SQL postąpili rozsądnie: nie przejmując się argumentacją relacyjnych guru wprowadzili kursory do zagnieżdżonego SQL, wracając w ten sposób do skrytykowanych koncepcji CODASYL-u. Twórcom i zwolennikom modelu nie pozostało nic innego tylko chwalić to osiągnięcie w konstrukcji SQL (z pewnymi wyjątkami, np. Ch.Date). Termin "wskaźnik bieżący" został zapomniany, gdyż SQL go ukrył, jak się okazało niesłusznie, ponieważ w ten sposób utrudnił realizację niektórych zadań.

W tym wykładzie nie będziemy zajmować się iteratorami niskiego poziomu, przyjmując, że 99% zadań można wykonać bez ich pomocy. Osoby, którym odpowiada ten styl programowania, mogą opracować ich definicje i implementacje (zwykle bardzo proste) zgodnie z własnym inżynierskim wyczuciem.

Copyrights © 2006 PJWSTK
Materiały zostały opracowane w PJWSTK w projekcie współfinansowanym ze środków EFS.