Języki i Środowiska Programowania Baz Danych | |||||||||||||||||||||||||||||||||||||
|
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.
|
P13.1. | Patrz Rys.55. Instrukcja create tworzy w trwałym składzie obiekt pracownika Kowalskiego, po zatrudnieniu go w dziale marketingu. |
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. |
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.:
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:
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.
Operator podstawienia możemy zrealizować w klasycznym wariancie.
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:
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 q1 i q2 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 q1 i q2 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 q1 i q2 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:
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: |
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": |
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: |
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: |
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): |
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 , { |
to rezultat dereferencji deref( i9 ) ma postać następującej struktury binderów:
P13.9. | struct{ Nazwisko( "Barski" ), |
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.
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:
lub
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: |
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. |
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, temporary i permanent, 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ć:
P13.13. | Utwórz obiekt ostatniaAktualizacja z bieżącą datą i wstaw kopię tego obiektu do wnętrza wszystkich obiektów Prac. |
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:
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. |
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:
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ć: Dla wszystkich mało zarabiających pracowników w średnim wieku zwiększ zarobek o 100: |
Podstawienia na wiekŚredni i mał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:
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.
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.
Istnieje kilka tego rodzaju operatorów, dobrze znanych z języków programowania.
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.
Najbardziej popularny jest operator
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:
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ą:
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:
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ą:
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++:
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.
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:
Wykonanie tego operatora jest identyczne jak w przypadku operatorów niealgebraicznych. Semantyka konstrukcji for each q do p jest następująca:
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:
|
P13.17. | Dla wszystkich obiektów Prac wstaw podobiekt kierownik zawierający nazwisko szefa.
|
P13.18. | Dla wszystkich działów zlokalizowanych w Olsztynie posortowanych według nazw wydrukuj nazwę, nazwisko szefa oraz liczbę pracowników. |
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 getNext i getPrior 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.