113
POLITECHNIKA ŚLĄSKA W GLIWICACH WYDZIAŁ AUTOMATYKI, ELEKTORNIKI I INFORMATYKI INSTYTUT INFORMATYKI PRACA MAGISTERSKA Eksperymentalny język programowania bardzo wysokiego poziomu wersja 1.3, build 26. autor: Michał Czardybon prowadzący: dr inż. Przemysław Szmal Gliwice 2005

Tylko książka (pdf)

Embed Size (px)

Citation preview

Page 1: Tylko książka (pdf)

POLITECHNIKA ŚLĄSKA W GLIWICACHWYDZIAŁ AUTOMATYKI, ELEKTORNIKI I INFORMATYKI

INSTYTUT INFORMATYKI

PRACA MAGISTERSKAEksperymentalny język programowania bardzo wysokiego poziomu

wersja 1.3, build 26.

autor: Michał Czardybonprowadzący: dr inż. Przemysław Szmal

Gliwice 2005

Page 2: Tylko książka (pdf)
Page 3: Tylko książka (pdf)

Three Languages for the Scientists under the sky,

Seven for the Designers and their files of art,

Nine for the IT-Specialists doomed not to comply,

One for the Mathematicians and the abstract craft

In the mind of the Programmer where the Ideas lie.

One Language to rule them all,

One Language to find them,

One Language to bring them all

And in the darkness bind them,

In the mind of the Programmer where the Ideas lie.

Page 4: Tylko książka (pdf)
Page 5: Tylko książka (pdf)

Spis treści

Wstęp 7Przewodnik po treści pracy 8

1 Wprowadzenie1.1 Podstawowe pojęcia 9

Informacja, dane, opis................................................................................................... 9Język.............................................................................................................................. 9Abstrakcja....................................................................................................................10Język programowania [bardzo] wysokiego poziomu.................................................. 10Meta-........................................................................................................................... 11Obiekt, typ, podtyp...................................................................................................... 11Zmienna, wartość........................................................................................................ 11Funkcja, procedura, podprogram, metoda................................................................... 12

1.2 Wymagania dla języka programowania 13

2 Wybrane zagadnienia programowania wysokiego poziomu2.1 Dziedziczenie 14

Struktura obiektu w C++............................................................................................. 14Struktura obiektu w Javie i C#.................................................................................... 15Czy można się obejść bez dziedziczenia implementacji?........................................... 16Zmiana typu argumentów w typach pochodnych........................................................ 17

2.2 Hermetyzacja 19Hermetyzacja a ukrywanie implementacji...................................................................19Poziomy hermetyzacji................................................................................................. 19Hermetyzacja w języku Harpoon.................................................................................19

2.3 Język opisu danych 22Wprowadzenie.............................................................................................................22Elementy opisu............................................................................................................ 22Bazy danych.................................................................................................................26

2.4 Konstruktory 27Konstruktory klasyczne............................................................................................... 27Nowe spojrzenie na konstruktory................................................................................ 27

2.5 Polimorfizm 29Czym jest polimorfizm?.............................................................................................. 29Polimorfizm strukturalny.............................................................................................29Unie obiektowe............................................................................................................30

2.6 Niektóre zasady projektowania obiektowego 34Atomowość..................................................................................................................34Podział na moduły....................................................................................................... 35Pełne oddzielenie interfejsów od implementacji.........................................................36

2.7 Prawa własności, aliasing i zarządzanie pamięcią 37Sieć obiektów.............................................................................................................. 37Logiczna hierarchia danych.........................................................................................38Aliasing....................................................................................................................... 38

Page 6: Tylko książka (pdf)

Zarządzanie pamięcią.................................................................................................. 40Uwagi dodatkowe........................................................................................................41Inne prace.................................................................................................................... 43Problemy......................................................................................................................44Korzyści.......................................................................................................................46

2.8 Współprogramy 47Przykład motywujący.................................................................................................. 47Współprogramy........................................................................................................... 48Generatory................................................................................................................... 49Leniwe struktury danych............................................................................................. 50Implementacja quasi-wyjątków................................................................................... 50

2.9 Operatory 51Ułatwienie składniowe dla.......................................................................................... 51Składnia....................................................................................................................... 51

3 Język programowania Harpoon3.1 Przegląd cech 52

Charakterystyczne cechy języka Harpoon................................................................... 52Konstrukcje nieobecne................................................................................................ 52

3.2 Zagadnienia składniowe 54Struktura leksykalna.................................................................................................... 54Konwencje składniowe – przedstawienie i uzasadnienie............................................57

3.3 Instrukcje i wyrażenia 61Instrukcje sterujące...................................................................................................... 61Wyrażenia....................................................................................................................62

3.4 Typy danych 66Wstęp...........................................................................................................................66Typy podstawowe........................................................................................................66Interfejsy......................................................................................................................67Klasy............................................................................................................................69Metody inicjujące........................................................................................................ 70Właściwości, rekordy, struktury i krotki..................................................................... 71Unie............................................................................................................................. 73Przecięcia.....................................................................................................................75Typy wyliczeniowe i obiekty symboliczne..................................................................75Typ typów....................................................................................................................76Typy procedur..............................................................................................................77Rzutowanie..................................................................................................................77Typy i podprogramy generyczne................................................................................. 78

3.5 Zmienne 80Deklaracje....................................................................................................................80Tryby dostępu.............................................................................................................. 80Dyscypliny...................................................................................................................81Instrukcja kopiowania................................................................................................. 81

3.6 Współprogramy 823.7 Organizacja dużych programów 83

Moduły........................................................................................................................ 83Przestrzenie nazw........................................................................................................ 83

Page 7: Tylko książka (pdf)

4 Realizacja4.1 Kompilator i interpreter 84

Wstęp...........................................................................................................................84Biblioteka sd3..............................................................................................................85Analizator składniowy.................................................................................................86Generator kodu pośredniego........................................................................................90Interpreter.................................................................................................................... 91

4.2 Instrukcja obsługi narzędzi 964.3 Przykładowe programy 97

Wyznaczanie największego wspólnego dzielnika.......................................................97Generowanie liczb pierwszych....................................................................................97Przykład użycia argumentów strukturowych...............................................................98Przykład użycia współprogramów...............................................................................98Funkcja „print” – przykład użycia tablic..................................................................... 99

4.4 Testowanie i uruchamianie 100Ogólne zasady testowania i uruchamiania.................................................................100Przebieg testowania i uruchamiania.......................................................................... 101Wyniki testów szybkościowych................................................................................ 102

5 Uwagi końcoweLiteratura 104

Załączniki 106Struktura drzewa składniowego języka Harpoon--....................................................106

Page 8: Tylko książka (pdf)
Page 9: Tylko książka (pdf)

WstępNie minęło jeszcze pół wieku od powstania pierwszego języka programowania

wysokiego poziomu, ale postęp, który dokonał się w tej dziedzinie do dziś jestznaczący. W tym czasie powstało wiele języków, z których niektóre zostałyzapomniane, niektóre pozostały domeną wąskich grup użytkowników, innewyewoluowały w języki wyższych generacji. Pomijając wyspecjalizowane językiprzeznaczone dla wąskich grup zadań, istnieje obecnie kilka-kilkanaście językówprogramowania o szerokich w skali świata gronach użytkowników. Jedne z nich sąlepsze, inne gorsze, ale najczęściej zależy to od zadania, do którego są stosowane.Jedno jest pewne – nie doczekaliśmy się jeszcze języka, który zdobyłby takądominację, jaką zdobył język matematyki, języka, który byłby wystarczająco dobrydla znaczącej większości zastosowań. Należy liczyć się z tym, że stworzenie językaidealnego do wszystkich zastosowań nie jest możliwe, tak jak istnieją wąskiedziedziny matematyki, które stosują odmienne od standardowej notacje (np. w fizycekwantowej). Z drugiej jednak strony nie ulega wątpliwości, że sytuacja, w której coroku powstaje wiele nowych języków programowania i wciąż inny z nich zyskujedużą popularność, jest przejściowa i wynika z młodego wieku tej dyscyplinynaukowej. Ostatecznie kiedyś będzie musiała nastąpić większa stabilizacja z dwoma,może trzema językami pokrywającymi oczekiwania prawie wszystkichprogramistów, aczkolwiek zapewne długo jeszcze na taki stan trzeba będziepoczekać.

Niniejsza praca jest jedną z prób stworzenia języka następnej generacji,dającego programistom więcej mocy i stawiającego mniej ograniczeń. Cechąodróżniającą ten projekt od wielu innych jest to, że nie jest on próbą przekształceniaistniejącego języka tudzież prostą próbą połączenia cech dwóch lub więcej języków;jest próbą stworzenia czegoś nowego zupełnie od podstaw, aczkolwiek przy wzięciupod uwagę całego dotychczasowego doświadczenia w tej dziedzinie. Każdakonstrukcja i każda własność, choćby wydawała się już dobrze okrzepniętymstandardem, została zweryfikowana pod kątem swojej rzeczywistej wartości, amyślenie ukierunkowane na przyzwyczajenia współczesnych programistów nie miałow tym procesie swojego miejsca (z wyjątkiem nieuniknionego wpływu preferencjiautora tej pracy).

Pod względem ideologicznym oparto się na założeniu, że programiści sątwórcami, czasem bardziej artystami niż inżynierami, a język programowania jestjęzykiem wyrażania ich myśli [12]. Informatyka jest pełna niestandardowychproblemów, w których rozwiązywaniu najbardziej istotnym czynnikiem jestpomysłowość twórcy. Stąd dobry język programowania nie powinien być zbioremgotowych rozwiązań, ale elastycznym narzędziem, dzięki któremu możliwe jesttworzenie rzeczy pięknych.

Page 10: Tylko książka (pdf)

Przewodnik po treści pracyPracę rozpoczyna krótki rozdział wprowadzający, w którym sprecyzowano

stosowaną terminologię i przedstawiono kryteria, według których w następnychrozdziałach oceniane są języki programowania i poszczególne ich konstrukcje.

Rozdział drugi zawiera teoretyczne omówienie wybranych zagadnieńzwiązanych z tematyką programowania na wysokim i bardzo wysokim poziomieabstrakcji. Dla każdego z problemów przedstawiono charakterystykę, standardowe ialternatywne sposoby rozwiązywania oraz próbę rozstrzygnięcia o tym, jakierozwiązanie byłoby, przy obecnym stanie wiedzy na ten temat, optymalne dla językaprogramowania tworzonego od podstaw.

W rozdziale trzeci przedstawiono projekt eksperymentalnego językaprogramowania Harpoon, który stanowi próbę połączenia w spójną całość wnioskówzawartych w rozdziale drugim. W pierwszym podrozdziale wyliczono ogólne cechytego języka, a w kolejnych opisano: (2) zagadnienia składniowe, (3) instrukcjei wyrażenia, (4) typy danych, (5) zmienne, (6) współprogramy, oraz (7) zasadyorganizację dużych programów. Na szczególną uwagę zasługują podrozdziały od 4.do 6., ze względu na to, że to one stanowią najbardziej o innowacyjnościprezentowanego języka programowania. Dokładne uzasadnienie dla decyzjipodjętych w sprawach prezentowanych w tym rozdziale podane jest tylko wtedy, gdyodpowiednie wnioski nie wynikają z treści rozdziału poprzedniego.

Rozdział czwarty zawiera omówienie zbioru programów, które składają się nakompilator i środowisko uruchomieniowe znacznie uproszczonego języka Harpoon--.Przedstawiono w nim kolejno programy: analizatora składniowego, generatora kodupośredniego oraz interpretera, a także sposoby współpracy między nimi. Obokprezentacji ww. narzędzi, zawarto omówienia przyjętych (często bardzoniestandardowych) rozwiązań dla kilku trudnych problemów implementacyjnychzwiązanych z tworzeniem kompilatorów, więc – w opinii autora – prezentowanatreść powinna stanowić ciekawe źródło pomysłów dla programistów realizującychpodobne projekty. W kolejnych podrozdziałach zawarto także kilka przykładowychprogramów oraz informacje o przebiegu i wynikach testowania i uruchamiania.

Page 11: Tylko książka (pdf)

1 Wprowadzenie 9

1 Wprowadzenie1.1 Podstawowe pojęcia

W teoretycznych rozważaniach na temat języków programowania pojawia siękilka pojęć, które ze względu na zróżnicowane znaczenie w różnych kontekstachwymagają uściślenia. Są to: informacja, dane, opis, język, język opisu danych,abstrakcja, język programowania wysokiego poziomu, meta-, obiekt, typ, zmienna,wartość, funkcja, procedura, podprogram, metoda. W tym podrozdzialeprzedstawione zostały znaczenia, w jakich są one używane w niniejszej pracy. Zewzględu na bardzo podstawową naturę omawianych pojęć przedstawione definicje,mimo dbałości o precyzję, pozostają nieformalne, a czasami nawet mają charakterbardziej filozoficzny niż ścisły.

Informacja, dane, opisPodstawowym i niedefiniowalnym pojęciem w informatyce jest informacja

(synonim wiedzy). Jest to pojęcie abstrakcyjne, określa byt niezależny od konkretnejreprezentacji. Ta sama informacja może być reprezentowana w różny sposób.W kontekście informacji zawartej na pewnym nośniku fizycznym lub w urządzeniu,która może być przetwarzana w sposób mechaniczny, zwykło się używać określenia„dane”. Z drugiej strony, w przypadku informacji zrozumiałej dla człowieka lubprzynajmniej dającej się przez istotę rozumną zinterpretować i dotyczącej czegoświększego, niebanalnego, używa się terminu „opis”. W szczególności opisaminazywane są dane w postaci tekstowej, takie jak np. opis sceny dla programurenderującego obrazy trójwymiarowe. Mówimy tu o opisie bo stworzył to człowiek,ale są to jednocześnie dane wejściowe dla programu albo – najbardziej ogólnie – jestto informacja przekazywana komputerowi przez człowieka.

Pojęcia informacji, danych i opisu są więc synonimami. Należy jednakinterpretować je ostrożnie, ponieważ tymi samymi terminami określa się równieżobiekty, które w najczystszym znaczeniu informacją nie są, a jedynie informacjęzawierają, np. „Czytając książki pomijam opisy przyrody.” zamiast „Czytając książkipomijam fragmenty zawierające opisy przyrody.” albo „Skasuj te dane!” zamiast„Skasuj pliki zawierające te dane!”.

Jeszcze bardziej sytuację komplikują informacje (pełne) o innych informacjach,na przykład „opisy danych”. Właściwie należałoby przyjąć, że informacja (1)o informacji (2) jest dokładnie tym samym co informacja (2), ponieważ zawieradokładnie tę samą (tę i tylko tę) informację co informacja (2). Niemniej jednakużywanie rozszerzonych form w rodzaju „opisów danych” może być uzasadnione,ponieważ sugeruje, dla kogo przeznaczona (zrozumiała) jest informacja na kolejnychetapach przetwarzania – „opis” wskazuje na nastawienia na człowieka, natomiast„dane” są dla maszyny. Należy jednak pamiętać, że są to rozróżnienia nieformalne.

JęzykJeżeli informacja ma być przetwarzana mechanicznie (dane) lub rozumiana

przez człowieka (opis), to muszą istnieć dla niej zbiory reguł określającedopuszczalną formę (składnię) oraz znaczenie (semantykę). W kontekście językówprogramowania te dwa zbiory zasad wyznaczają abstrakcyjny obiekt nazywanyjęzykiem (inaczej niż w teorii języków formalnych, w której istotne są jedyniezasady określające składnię).

Page 12: Tylko książka (pdf)

10 Rozdział 1 Wprowadzenie

Mając sprecyzowane odpowiednie pojęcia podstawowe można przystąpić dorozszyfrowania ważnego z punktu widzenia niniejszej pracy pojęcia „języka opisudanych”. Według przyjętych wcześniej wniosków oznacza ono język służący doreprezentowania informacji bliskiej człowiekowi (bo „opis”) o informacjiprzeznaczonej dla komputera (bo „dane”). Ponadto przyjęta forma określeniainformacji sugeruje występowanie etapu przetwarzania informacji z postaci „opisu”na postać „danych”. Jednakże jeśli spojrzeć na pochodzenie omawianego terminu,okaże się, że jest on tłumaczeniem angielskiego określenia „data descriptionlanguage”, które bywa uznawane za synonim „data definition language”. Tymczasemto drugie pojęcie oznacza metajęzyk służący do opisywania struktury danych, jakimjest na przykład XML Schema. Należy więc zachowywać ostrożność w posługiwaniusię tymi terminami.

AbstrakcjaWszystko, co pozwala na prowadzenie rozumowania bez dbałości o szczegóły

przy jednoczesnym zachowaniu całkowitej precyzji, nazywamy abstrakcją. Może tobyć mechanizm, sposób, obiekt, ale precyzyjne sformułowanie odpowiedniejdefinicji jest bardzo trudne, a może nawet niemożliwe, ponieważ pojęcie abstrakcjisamo w sobie jest abstrakcyjne. Pojęcie to jest bardzo szerokie, w kontekście samychtylko języków programowania stosowane jest na różnych poziomach i dla zupełnieróżnych ich aspektów – np. język wysokiego poziomu jest abstrakcją pozwalającą nanie przejmowanie się architekturą maszyny, natomiast funkcja jest abstrakcjąpozwalającą na analizowanie pewnych zależności w oderwaniu od konkretnychwartości argumentów.

Najciekawsze abstrakcje powstają po odnalezieniu w wielu różnych teoriachanalogii i wyekstrahowaniu z nich największej wspólnej części, przez co otrzymujesię „czyste” pojęcia, które nie mają swoich odpowiedników w świecie rzeczywistym,ale mających duże znaczenie teoretyczne. Chyba najdonioślejszym przykładem takiejabstrakcji jest koncepcja liczb naturalnych.

Język programowania [bardzo] wysokiego poziomuJak zauważa David L. Parnas w artykule „O modnym powiedzonku – struktura

hierarchiczna” [16], nie jest jasne, do jakiego rodzaju hierarchii odnosi się pojęcie„języka [bardzo] wysokiego poziomu”.

Otóż, w najbardziej ogólnym znaczeniu odnosi się ono do hierarchiiwyznaczonej przez abstrakcyjność, która jednak, jak już wcześniej wspomniano,również jest pojęciem bardzo niejednoznacznym.

W węższym znaczeniu pojęcie „języka wysokiego poziomu” jest używane dookreślenia języków niezależnych od konkretnej maszyny (procesora), ale wciążopartych na modelu stanu maszyny i na dobrze określonej w czasie sekwencjioperacji ten stan zmieniających (są to tzw. języki imperatywne). Mianem „językówbardzo wysokiego poziomu” określa się natomiast języki deklaratywne, takie jakSQL czy Prolog, w których programista abstrahuje od pojęć stanu i kolejnościobliczeń.

Użycie terminu „języka bardzo wysokiego poziomu” w tytule niniejszej pracymoże być więc traktowane jako stwierdzenie, że język programowania będący jejprzedmiotem jest językiem deklaratywnym. W rzeczywistości nie jest to jednakprawdą – prezentowany język wciąż dużo bardziej przypomina języki imperatywne

Page 13: Tylko książka (pdf)

1.1 Podstawowe pojęcia 11

niż deklaratywne, ale dodanie słowa „bardzo” jest wyrazem przekonania autora, żepozwala on programować w stylu deklaratywnym dużo łatwiej niż jest to możliwe winnych językach imperatywnych.

Meta-Rozumowanie jest procesem myślowym polegającym na budowaniu wiedzy

o świecie rzeczywistym (lub przynajmniej o odpowiednim Matriksie) na podstawieinformacji dostarczanej przez zmysły. Zdolność tę posiadają jednak nawet zwierzęta.O wyższości ludzi nad innymi stworzeniami decyduje to, że oprócz rozumowaniao świecie rzeczywistym potrafią przeprowadzać rozumowania o rozumowaniach,a nawet, czego przykładem jest ten tekst, rozumowania o dowolnych ciągachrozumowań nad rozumowaniami. Na pewnym poziomie rozumowania posługujemysię pewnym zbiorem pojęć. Te same pojęcia mogą jednak występować również nainnych poziomach rozumowania (np. w rozumowaniach nad rozumowaniami) i ichznaczenie jest tam, wbrew pozorom, odmienne. Aby więc wprowadzić odpowiednierozróżnienie nazw używa się przedrostka meta- do oznaczenia pojęć właściwych dlanadrzędnego poziomu rozumowania. Pojęcia będące bezpośrednim przedmiotemrozumowania nazywamy obiektowymi. Dla przykładu język służący do opisywaniainnych języków nazywamy metajęzykiem, a języki przez niego definiowane językamiobiektowymi (nieszczęśliwa zbieżność z terminem „język programowaniaobiektowego”).

Obiekt, typ, podtypObiekt jest pojęciem pojawiającym się (inaczej niż np. „zmienna”) zarówno

w świecie rzeczywistym jak i w programach komputerowych. W tym drugimprzypadku oznacza elementy procesu, które mają określoną tożsamość (adres), stan(dane) oraz zbiór operacji (metod). W założeniu powinny one odzwierciedlać obiektyfizyczne lub abstrakcyjne ze świata modelowanego.

Z pojęciem obiektu związane jest pojęcie typu. Typ jest pewnym zbioremwłasności obiektu. W praktyce terminem tym określane są zbiory własnościwystarczająco ogólne, aby objąć nimi więcej niż jeden obiekt i w więcej niż jednejchwili czasu. Dokładniej, znaczenie tego pojęcia wraz ze znaczeniem relacji „byciapodtypem” wyznacza zasada LSP [15] (ang. Liskov Substitution Printicple):

Niech q(x) będzie pewnym (istotnym z punktu widzenia wpływu na właściwyprzebieg procesu1) predykatem spełnionym dla wszystkich obiektów x typu T.Wtedy predykat q(y) musi być także spełniony dla wszystkich obiektów y o typieS, który jest podtypem T.W obiektowych językach programowania typy modelowane są za pomocą klas

oraz interfejsów, dla których wspólnym określeniem jest niestety słowo „typ”w znaczeniu odmiennym od powyżej używanego znaczenia ogólnego. O tym czysłowo to użyte jest w znaczeniu ogólnym czy w sensie klasy lub interfejsu decydujekontekst wypowiedzi. Podobnie modelem dla relacji bycia podtypem (w sensieogólnym) jest mechanizm dziedziczenia, który także określany może być mianemmechanizmu tworzącego podtypy (w sensie klas lub interfejsów pochodnych).

Zmienna, wartośćW programie komputerowym musi istnieć możliwość odwoływania się do

1 Nieistotne są na przykład predykaty oparte na identyfikacji sposobu implementacji.

Page 14: Tylko książka (pdf)

12 Rozdział 1 Wprowadzenie

danych. W językach niskiego poziomu dokonuje się tego poprzez wskazanie ichadresów. W językach wysokiego poziomu z danymi wiąże się nazwy, co znacznieułatwia programowanie. Złączenia nazwy i danych określane są mianem zmiennych(lub stałych, jeśli dane są niemodyfikowalne). Zmienne mają dwa atrybuty: nazwęi wartość, gdzie „wartość” jest terminem używanym dla danych w kontekście pewnejzmiennej.

„Wartość” ma jednak także drugie znaczenie. W niektórych językachprogramowania określa elementy procesu, którym ze względu na ich małą złożonośćodmawia się miana obiektów. Istnieje również pojęcie „semantyki wartości”, któredotyczy wartości i obiektów, do których nie tworzy się dowiązań i które zawszeprzekazuje przez wartość (klonowanie).

Funkcja, procedura, podprogram, metodaW różnych językach programowania pojęcia funkcji, procedury i podprogramu

mają nieco inne znaczenie. W języku C znaczą to samo, w Pascalu już co innego –funkcje i procedury są podprogramami, ale te pierwsze zwracają wartości, a drugienie. Pojęcie metody dotyczy podprogramów związanych z obiektami.

W niniejszej pracy pojęcie funkcji zarezerwowano dla abstrakcjimatematycznych oraz realizujących je podprogramów bez efektów ubocznych,a „procedurę” utożsamia się z podprogramem nie związanym z obiektami.

Page 15: Tylko książka (pdf)

1.2 Wymagania dla języka programowania 13

1.2 Wymagania dla języka programowaniaRóżne języki programowania stosowane są dla różnych zastosowań, więc tak

samo różne mogą być wymagania przed nimi stawiane. Język będący przedmiotemniniejszej pracy ma jednak jasno określony cel – jest językiem ogólnegoprzeznaczenia. Jest to podstawą wszystkich przedstawionych dalej wywodów –poszczególne mechanizmy oceniane są pod kątem ich zastosowania w językuuniwersalnym. Aby jeszcze dokładniej określić cele, poniżej przedstawiono cechy,jakie projektowany język ma za zadanie mieć. Można się z nimi zgadzać lub nie, alenie zmienia to faktu, że w niniejszej pracy stanowią one założenia wstępne.

1. Ekspresywność. Język programowania służy do sterowania komputerem, ale conajmniej równie ważne jest to, że jest językiem służącym do myślenia.Programiści muszą mieć możliwość wyrażania dowolnie złożonych koncepcji,budowania abstrakcji wysokiego poziomu.

2. Precyzja wyrażeń. Program komputerowy niesie informację potrzebnąkompilatorowi do wygenerowania kodu wykonywalnego na pewnej maszynie. Tojednak nie wszystko – formalne wyrażanie intencji programisty umożliwia takżekontrolę spójności programu, w tym kontrolę typów, oraz wykonywanieautomatycznych optymalizacji. Im większa precyzja języka, tym więcej tegorodzaju pracy mogą wykonać odpowiednie narzędzia.

3. Lokalność wyrażeń. Tworzenie złożonych systemów wymaga stosowania zasady„dziel i rządź”, ponieważ człowiek nie jest w stanie myśleć o zbyt wielu rzeczachjednocześnie. Język programowania powinien uwzględniać ten aspekt ludzkiegoumysłu i pozwalać na dzielenie programów na moduły o jasno określonychzadaniach i całkowicie niezależnych od reszty programu implementacjach.Niespełnienie tego warunku (na poziomie języka lub projektu) prowadzi doponadliniowego wzrostu złożoności programów wraz z ich rozmiarami.

4. Prostota. Trudność wykonywanego zadania powinna zawsze wynikać z jego cechwłasnych, ale nigdy ze złożoności używanych narzędzi. Skomplikowane narzędziasprawdzają się w zadaniach dobrze zbadanych i powtarzalnych, ale takiew programowaniu są mało istotne. W programowaniu znaczenie ma kreatywnośćprogramisty i to na nim spoczywa odpowiedzialność za znajdowanieodpowiednich rozwiązań, a język programowania powinien jedynie umożliwiaćmu ich realizację.

5. Efektywność. W dążeniu do ideału języka prostego i wysoce ekspresywnego niemożna pominąć faktu, że tworzone w nim programy muszą być wykonywaneszybko i przy użyciu jak najmniejszej pamięci. Dobrze byłoby, gdyby żadenmechanizm języka nie powodował utraty wydajności w stosunku do wydajnościodpowiednich konstrukcji z języka C.

6. Zapewnienie standardów. Programy są prawie zawsze tworzone przez wieluprogramistów (w końcu korzystamy z bibliotek), więc ważne również jestistnienie odpowiednich konwencji konstruowania programów, tak aby kodyprogramów były zrozumiałe nie tylko dla ich twórców. Wymaganie toprzeciwdziała nadmiernemu upraszczaniu języka.

Page 16: Tylko książka (pdf)

14 Rozdział 2 Wybrane zagadnienia programowania wysokiego poziomu

2 Wybrane zagadnienia programowania wysokiegopoziomu

W tym rozdziale przedstawiono dziewięć luźno ze sobą powiązanychproblemów związanych z programowaniem na wysokim poziomie abstrakcji. Obokomówień poszczególnych zagadnień przedstawiono analizy, które mogą miejscamimieć dość subiektywny charakter, oraz propozycje częściowo lub całościowo nowychrozwiązań mających w zamierzeniu autora stanowić o innowacyjnościproponowanego języka.

2.1 DziedziczenieDziedziczenie jest jednym z podstawowych pojęć obiektowego paradygmatu

programowania. Nie jest to jednak termin jednoznaczny – kryją się pod nim conajmniej dwie bardzo odmienne koncepcje:

1. dziedziczenie implementacji polegające na tworzeniu nowej klasy napodstawie już istniejącej, poprzez rozszerzenie listy metod lub zmiennychstanu, lub poprzez zmienianie implementacji wybranych metod,

2. dziedziczenie interfejsu polegające na tworzeniu nowego typu, któregoobiekty, ze względu na zgodność listy metod i ich sygnatur, mogą być użytew miejscach, gdzie wymagane są obiekty typów bazowych.

W języku C++ dla obu tych przypadków dostępny jest jeden wspólnymechanizm klas, przez co omawiane rozróżnienie może nie być dostrzegane.Problem ten został zauważony przez twórców języka Java, i wprowadzili oni osobnekonstrukcje dla dziedziczenia interfejsów i dziedziczenia implementacji. Ogólnie,zadaniem projektanta systemu typów dla języka programowania jest dostarczenieprogramistom takich konstrukcji, które możliwie najlepiej przybliżałybywieloaspektową naturę ogólnej relacji bycia podtypem (patrz: Obiekt, typ, podtyp wewstępie). Zadanie to jest o tyle trudne, że oprócz wymagań teoretycznych, istniejątakże bardzo ważne wymagania związane z koniecznością zapewnienia wysokiejwydajności programów wynikowych, a także z praktyczną nieodzownościązachowania względnej prostoty. W niniejszym podrozdziale przedstawiono różnemożliwości zawarcia w tej kwestii kompromisu między teorią i praktyką.

Struktura obiektu w C++Jednym z podstawowych wymagań, jakie stawiał przed sobą twórca języka

C++ [19], było uzyskanie wydajności programów wynikowych równie wysokiej jakprogramów pisanych w języku C. Miało to bardzo duży wpływ na to, w jaki sposóbidea obiektowości została zaadaptowana dla potrzeb tego języka.

Dziedziczenie jednokrotne (bez rozróżnienia między dziedziczeniem interfejsua dziedziczeniem implementacji) dało się w dość prosty sposób zrealizować bezutraty efektywności. Przyjęto, że wszystkie obiekty w pełni korzystającez dziedziczenia będą przekazywane przez wskaźnik, adresy metod odpowiednich dlakonkretnych obiektów będą przechowywane w tablicy (v-table), której wskaźnikznajduje się w nagłówku każdego obiektu, a w pozostałej części znajdą się zmiennestanu, ułożone w ten sposób, że zmienne pochodzące z klasy podstawowej znajdą sięprzed (nad) zmiennymi klasy pochodnej (patrz rysunek 2.1.1). W ten sposóbosiągnięto własność kompatybilności obiektów klas pochodnych z obiektami klaspodstawowych, dzięki której możliwe stało się zrealizowanie w efektywny sposób

Page 17: Tylko książka (pdf)

2.1 Dziedziczenie 15

teoretycznej koncepcji dziedziczenia jednobazowego.

Rysunek 2.1.1. Struktura obiektu w języku C++

Tablica metod wirtualnych (v-table) znajdująca się w nagłówku każdegoobiektu pełni zasadniczą rolę w oddzieleniu interfejsu od implementacji. Metodyobiektów nie są wywoływane poprzez bezpośredni skok, ale występuje tutaj etappośredni pobrania adresu metody z v-table. Adres ten znajduje się w komórce, którejprzesunięcie wyznaczane jest na etapie kompilacji na podstawie definicji klasy.Dzięki temu ten sam kod klienta może być stosowany dla obiektów, które w różnysposób implementują ten sam interfejs. Co prawda wywołanie metody trwa dłużej niżzwykłej funkcji, ale ostateczna wydajność programu napisanego w C++ może byćnawet większa niż odpowiedniego programu napisanego w C, ponieważ wywołaniametod za pośrednictwem tablicy metod wirtualnych odpowiadają zazwyczajinstrukcjom switch w języku C, a nie bezpośrednim wywołaniom funkcji.

Niestety w tak prosty sposób nie udało się zrealizować dziedziczeniawielokrotnego, a rozwiązanie, jakie zostało przyjęte, jest powszechnie krytykowane.Obecnie przeważa pogląd, że wielokrotnego dziedziczenia implementacji należyunikać, gdyż komplikacje nim spowodowane nie równoważą wątpliwych korzyści.Z tego też powodu autor tej pracy nie zagłębiał się w szczegóły tego rozwiązania i wkonsekwencji nie zostało one tutaj omówione. Znacznie ciekawsze z praktycznegopunktu widzenia wielokrotne dziedziczenie interfejsów zostało omówione w punkciepoświęconym strukturze obiektów w językach Java i C#.

Mogłoby się wydawać, że w zakresie dziedziczenia jednokrotnego znalezionorozwiązanie idealne. Trzeba sobie jednak zdawać sprawę, że ma ono jednak swojeograniczenia – m.in. obiekt nie może zmieniać swojego typu w czasie działaniaprogramu, a metody są zawsze wybierane na podstawie tylko jednego obiektu this,podczas gdy zdarzają się sytuacje, gdy również obiekty argumentów powinny miećwpływ na wybór odpowiedniej metody.

Struktura obiektu w Javie i C#Twórcy języka Java postawili sobie za cel utworzenie lepszego C++, ze

szczególnym naciskiem na większą prostotę. Wiązało się to z dużymi zmianamiw zakresie dziedziczenia. Po pierwsze w miejsce pojedynczej koncepcji klaswprowadzono dwa pojęcia: klasy i interfejsu. Klasy mogą dziedziczyć implementacjeod jednej klasy podstawowej tak, jak ma to miejsce w C++ oraz dowolną liczbę„czystych” interfejsów, dla których jednak muszą zapewnić całościowąimplementację. Interfejsy mogą dziedziczyć (rozszerzać) dowolną liczbę innychinterfejsów.

wskaźnik obiektu wskaźnik v-table

zmienne stanuklasy podstawowej

zmienne stanuklasy pochodnej

adresy metodklasy podstawowej

adresy metodklasy pochodnej

Page 18: Tylko książka (pdf)

16 Rozdział 2 Wybrane zagadnienia programowania wysokiego poziomu

Na poziomie implementacji schemat ten został zrealizowany poprzez dodaniedo nagłówka każdego obiektu nowego elementu – wskaźnika na tablicę interfejsów(i-table), której elementami są wskaźniki na tablice metod wirtualnychodpowiadające poszczególnym interfejsom [28] (patrz rysunek 2.1.2). W efekcie czaswywoływania metod jeszcze bardziej się wydłużył, ale dotyczy to jedynie metoddziedziczonych z interfejsów; metody dziedziczone z klas mogą być wywoływanerównie szybko jak w języku C++. Dodatkowym problemem jest to, że implementacjatablic interfejsów nie jest taka prosta jak tablic metod wirtualnych, ponieważprzesunięcie w i-table wyznacza się na podstawie globalnego identyfikatorainterfejsu bez wiedzy o tym, jakie inne interfejsy implementuje dana klasa. Aby więctablice te nie miały monstrualnych rozmiarów, wykorzystuje się fakt, że są onebardzo rzadkie i łączy wiele tablic w jedną, jeśli tylko nie powoduje to konfliktów.

Rysunek 2.1.2. Struktura obiektu w językach Java oraz C#

Czy można się obejść bez dziedziczenia implementacji?Jednokrotne dziedziczenie implementacji jest powszechnie stosowaną

techniką. W tym punkcie poddano jednak w wątpliwość jej niezbędność, a nawetwskazano, że język pozbawiony tej funkcji mógłby przewyższać języki, w którychjest ona dostępna.

Głównym celem stosowania dziedziczenia implementacji jest powtórne użyciekodu. Cel ten sam w sobie jest bardzo szczytny, aczkolwiek może być realizowanyinną drogą. Rozważmy język nie oferujący dziedziczenia implementacji, a jedyniedziedziczenie interfejsu. Programista używający tego języka, w sytuacjiodpowiadającej zastosowaniu dziedziczenia implementacji, mógłby przyjąćnastępujące rozwiązanie:

• utworzyć klasę dziedziczącą tylko interfejs innej klasy,

• w klasie dziedziczącej utworzyć (przez wartość) zmienną stanu X, którejtypem jest klasa dziedziczona,

wskaźnik obiektu wskaźnik v-table

zmienne stanuklasy podstawowej

zmienne stanuklasy pochodnej

adresy metodklasy podstawowej

adresy metodklasy pochodnej

wskaźnik i-table

wskaźnik v-tabledla interfejsu #1

wskaźnik v-tabledla interfejsu #2

wskaźnik v-tabledla interfejsu #3

Page 19: Tylko książka (pdf)

2.1 Dziedziczenie 17

• zaimplementować wszystkie dziedziczone metody tak, aby realizowałyproste przekierowanie odpowiedniego komunikatu do zmiennej stanu X,

• albo lepiej: skorzystać z oferowanej przez język deklaracji zlecającej tobanalne zadanie kompilatorowi.

Wspomniane ułatwienia składniowe mogłyby wyglądać tak, jak w poniższymprzykładzie:

Typowe rozwiązanie w języku C++:int find(Object* obj) { return core.find(obj); }

Najprostsze ułatwienie składniowe:int find(Object* obj) --> core;

W ten sposób można by realizować wielodziedziczenie implementacji, a nawet,po małej modyfikacji, istniałaby możliwość dynamicznego zmieniania obiektuodpowiadającego klasie dziedziczonej. Pewne wątpliwości może budzić kwestiawydajności – faktycznie, ze względu na to, że wszystkie metody stają się metodamiinterfejsowymi (dostępnymi poprzez dwa przekierowania: i-table oraz v-table),wygenerowanie równie szybkiego kodu, jak dla programu napisanego w językuz dziedziczeniem implementacji może być nieco trudniejsze, ale nie wydaje się tobyć przeszkodą nie do pokonania.

Pomijając kwestie wydajnościowe, optymistycznie traktując je jako problemyczysto techniczne, uzyskujemy język prostszy i bardziej elastyczny od językaz dziedziczeniem implementacji. Prezentowana tutaj technika nie jest jednak wcalenowatorska – znana jest od dawna pod nazwą delegacji i jest często polecana jakoalternatywa dla dziedziczenia (patrz np. wzorzec Dekorator w [10]). Niemniejjednak, ze względu na brak możliwości automatycznego przekierowywaniakomunikatóww popularnych językach programowania, używanie dziedziczenia często bywaznacznie wygodniejsze.

Zmiana typu argumentów w typach pochodnychW językach C++, Java oraz C# metody w klasach i interfejsach pochodnych

muszą mieć dokładnie takie same sygnatury (w tym typy argumentów i wartościzwracanych) jak odpowiadające im metody w typach bazowych. Ze względu nawymaganie zgodności hierarchii typów z relacją bycia podtypem niedopuszczalnejest, aby w typie pochodnym typ pewnego argumentu był szerszy niż w typiebazowym, ale zdarzają się sytuacje, w których zasadne bywa zawężenie typuargumentu lub typu wartości zwracanej w klasie lub interfejsie pochodnym.

Przykład:

Ogólny framework gier planszowych (w języku C++) zawiera m.in. klasyabstrakcyjne State oraz Move oznaczające odpowiednio stan gry oraz akcjęmożliwą do wykonania przez gracza. Ponadto istnieje metoda

void State::perform(Move& move)

która powoduje zmianę stanu gry poprzez wykonanie wskazanego ruchu.

Na bazie tego frameworku zbudowano program do gry w szachy, który zawieraklasy Board implementującą interfejs State oraz ChessMove implementującąinterfejs Move. Metoda perform w tym programie może wyglądać następująco:

Page 20: Tylko książka (pdf)

18 Rozdział 2 Wybrane zagadnienia programowania wysokiego poziomu

void Board::perform(Move& move)

ale oczywiste jest, że powinna mieć raczej postać następującą:void Board::perform(ChessMove& move)

Dlaczego coś takiego nie jest możliwe w językach C++, Java i C#? Powodemzapewne jest brak możliwości pełnej statycznej weryfikacji typów w takichprzypadkach, ale jednak zawsze istnieje możliwość sprawdzeniadynamicznego. Ponadto dopuszczalność zawężania typów argumentów maistotny wpływ na zasady rozstrzygania przeciążania metod, co również możebyć przyczyną braku tej własności w popularnych językach. Mogłoby siębowiem zdarzyć, że w omawianym przykładzie w klasie State znalazłaby sięmetoda perform o argumencie typu ChessMove (teoretycznie, bo z punktuwidzenia tego przykładu nie ma to sensu). Pojawiłoby się wtedy pytanie czymetoda perform w klasie Board przesłania jedną czy obie metody performz klasy State? Problem ten trudno byłoby jednoznacznie rozstrzygnąć, gdyżmożna sobie wyobrazić różne sytuacje przemawiające raz za jedną, a raz zadrugą możliwością.

Trudność taka występuje jednak tylko wtedy, gdy rozpatrywane sąprzesłonięcia metod podczas dziedziczenia implementacji. Jeśli rozważymy tenproblem w kontekście samego tylko dziedziczenia interfejsu, uzyskamy dodatkowyczynnik rozstrzygający – wszystkie metody interfejsowe muszą znaleźć swojąimplementację i w tej sytuacji metoda w typie pochodnym będzie implementacją obumetody z interfejsu bazowego, chyba że w typie pochodnym zadeklarowane zostanąobie wersje metod. Pozostaje jeszcze kwestia wpływu proponowanej własności namechanizm delegacji, który pomyślany jest jako alternatywa dla dziedziczeniaimplementacji, ale i tutaj problemy się nie pojawiają, ponieważ, inaczej niż ma tomiejsce w przypadku dziedziczenia implementacji, w przypadku delegacji istniejemożliwość precyzyjnego wskazania, który komunikat przekazywany jest do któregoobiektu.

Podsumowując, jeśli tylko w powyższym teoretycznym rozumowaniu niepominięto jakiegoś istotnego czynnika, możliwość zawężania typów argumentóww dziedziczonych metodach może sprawiać istotne problemy w przypadkudziedziczenia implementacji przy jednoczesnej dopuszczalności przeciążaniemmetod, natomiast w przypadku dziedziczenia interfejsów podobne problemy niewystępują, co składnia do wniosku, że w tych okolicznościach wprowadzenieomawianej własności może istotnie poprawiać ekspresywność językaprogramowania.

Page 21: Tylko książka (pdf)

2.2 Hermetyzacja 19

2.2 Hermetyzacja

Hermetyzacja a ukrywanie implementacjiW popularnych językach programowania obiektowego, takich jak C++ i Java,

składowe klas mogą być publiczne, chronione lub prywatne. Zadaniem tychatrybutów jest ograniczanie dostępności pewnych elementów na zewnątrz klasy.Z powodu dostępności takich ograniczeń na poziomie klasy mówi się, że klasa jestjednostką hermetyzacji (kapsułkowania), co oznacza, że klasa dostarcza pewnychserwisów, które realizuje w sposób niewiadomy dla klienta. Hermetyzacja napoziomie klasy nie jest jednak jedynym możliwym rozwiązaniem – np. w języku Cstosuje się ukrywanie implementacji na poziomie modułu.

Warto przy tym zwrócić uwagę, że istnieje zasadnicza różnica międzyukrywaniem implementacji (lub ogólniej informacji) a hermetyzacją [17].Z pierwszym przypadkiem mamy do czynienia w języku C – gdyby programistazerknął do plików implementacyjnych, zamiast tylko do nagłówków, kompilator niezabroniłby mu z tej wiedzy skorzystać. W przypadku prywatnych elementów klasyw języku C++ tajna wiedza na nic się nie zda, ponieważ kompilator i tak nie pozwoliskorzystać z tego, co nie jest udostępnione. Nazywamy to hermetyzacją lubkapsułkowaniem.

Jakie rozwiązanie jest właściwe? Zgodnie z powszechnie przyjętą opinią, pełnahermetyzacja wydaje się być lepsza od prostego ukrywania implementacji, ponieważdokładniej oddaje intencje programisty (skoro coś ukrył, to nie po to aby można siębyło do tego dostać).

Poziomy hermetyzacjiPrzyjmując wyższość hermetyzacji nad ukrywaniem informacji, pozostaje

jeszcze do rozważenia kwestia, na jakim poziomie powinno się to odbywać – napoziomie typu (np. klasy) czy na poziomie modułu (lub pakietu)? Zacznijmy odprzedstawienia postulatów:

• Jednostka kapsułkowania powinna być na tyle mała, aby niezależneskładniki programu nie miały dostępu do informacji o implementacji innychskładników.

• Jednostka kapsułkowania powinna być na tyle duża, aby składnikiprogramu, które zależą od tych samych detali implementacyjnych (np. klasykolekcji i ich iteratorów) miały dostęp do odpowiedniej informacji.

• Jednostka kapsułkowania powinna być na tyle mała, aby programista byłw stanie zrozumieć ją jako całość.

W C++ przyjęto hermetyzację na poziomie klasy wspomaganą małoeleganckimi deklaracjami zaprzyjaźnienia. Krokiem do przodu było dodanie w Javie„domyślnego” modyfikatora dostępu, który powoduje, że dany element klasy jestdostępny dla wszystkich klas z tego samego pakietu. Jest to rozwiązanie spełniającewymienione powyżej postulaty, ale dość skomplikowane.

Hermetyzacja w języku HarpoonDla języka Harpoon zaproponowano znacznie prostsze podejście – tylko prosta

hermetyzacja na poziomie modułu – element albo jest dostępny na zewnątrz modułualbo tylko w jego obrębie. Ponadto, kierując się zasadą, że kod źródłowy powinien

Page 22: Tylko książka (pdf)

20 Rozdział 2 Wybrane zagadnienia programowania wysokiego poziomu

sam w sobie być dokumentacją, wrócono do dwuplikowego modelu modułu z językaC/C++. Tym sposobem niepotrzebne są nawet słowa kluczowe public i private,ponieważ to, co jest publiczne, jest po prostu umieszczone w pliku nagłówkowym,a to, co jest prywatne, w pliku implementacyjnym. Oczywiście, takie rozwiązanie niemogłoby być wprost przyjęte dla języków C++ i Java, w których zwykło sięumieszczać zarówno informacje o interfejsie, jak i o implementacji w jednej definicjiklasy. W języku Harpoon przyjęto zasadę, że publiczny interfejs każdej klasy jestzdefiniowany osobno (jako interfejs) i umieszczony w pliku nagłówkowym wrazz jedynie deklaracją klasy (nazwa + lista interfejsów), natomiast definicja klasy jestumieszczona w pliku implementacyjnym. Takie podejście powoduje jednak jedenbardzo poważny problem – deklaracje konstruktorów, które są częścią definicji klasy,a nie interfejsu stałyby się niedostępne. Problem ten rozwiązano w sposób równieniekonwencjonalny poprzez ustalenie, że konstruktory będą częścią interfejsówi będą mogły być dziedziczone. Jako, że nie mogą one dłużej mieć nazwy tożsamejz nazwą klasy, nadano im jedną uniwersalną nazwę metody „init”. Szczegóły tegorozwiązania przedstawiono w podrozdziale poświęconym konstruktorom.Przykład

C++ Harpoon w składni C++class CFile{public: CFile(const char* file_name); void open(const char* file_name); void close(); Terminal peek(int k = 0); Terminal get(); void pop(); Location location();private: void feed(); FILE* f; Location loc; deque<Terminal> buf;};

Plik nagłówkowy:interface IFile{ void init(const char* file_name); void open(const char* file_name); void close(); Terminal peek(int k = 0); Terminal get(); void pop(); Location location();};class CFile implements IFile;

Plik implementacyjny:class CFile implements IFile{ void init(const char* file_name); void open(const char* file_name); void close(); Terminal peek(int k); Terminal get(); void pop(); Location location(); void feed(); FILE* f; Location loc; deque<Terminal> buf;};

Deklaracje metod w pliku implementacyjnym zostały powtórzone, ale wydajesię, że nie jest to konieczne – można przyjąć, że deklaracje elementówdziedziczonych nie powtórzone w typie pochodnym są uzupełniane automatycznieprzez kompilator.

Symulowanie publicznych i prywatnych elementów klasy nie jest żadnymproblemem – wystarczy, że w jednym module będzie jedna klasa i jeden interfejs,aby otrzymać mechanizm równoważny hermetyzacji na poziomie klasy. Ażebynatomiast uzyskać odpowiednik dostępu „domyślnego” z Javy (na poziomie pakietu),

Page 23: Tylko książka (pdf)

2.2 Hermetyzacja 21

należy umieścić zależne od siebie klasy w jednym pliku. Większym problemem jestwskazanie zamiennika dla dostępu „chronionego” (ang. protected). Tego trybu nie dasię emulować w Harpoonie, ale nie ma w tym nic złego, gdy weźmie się pod uwagę,że jest on stosowany jedynie w dziedziczeniu implementacji (o czym świadczy naprzykład brak możliwości tworzenia chronionych elementów dla interfejsóww Javie), a tymczasem w proponowanym modelu obiektowości dopuszcza się jedyniedziedziczenie interfejsów.

Dodatkowym uproszczeniem wynikającym z hermetyzacji na poziomie modułujest brak potrzeby istnienia metod i pół statycznych, gdyż mogą one z powodzeniembyć zastąpione przez normalne procedury i zmienne globalne umieszczone w tymsamym module co klasa. Przed występowaniem konfliktów nazw takich statycznychelementów modułu chroni mechanizm przestrzeni nazw o strukturze odpowiadającejhierarchii modułów.

Przedstawiona propozycja hermetyzacji jest, jak wykazano powyżej, jedynieminimalnie mniej ekspresywna niż w Javie, ale za to:

• jest bardzo prosta (co najmniej o 4 słowa kluczowe mniej: public,protected, private, static),

• oddziela formalną dokumentację od właściwego programu w sposób lepszyniż w C++, ponieważ nie powoduje umieszczania w nagłówkach żadnychszczegółów implementacyjnych (części prywatne klas w C++),

• promuje ścisłe oddzielanie interfejsów od implementacji.

Page 24: Tylko książka (pdf)

22 Rozdział 2 Wybrane zagadnienia programowania wysokiego poziomu

2.3 Język opisu danych

WprowadzenieWiele popularnych języków programowania zapewnia szeroki repertuar

konstrukcji służących do tworzenia typów i przetwarzania bardzo złożonych danych,jednak wsparcie dla zapisywania samych danych w kodzie źródłowym jestnajczęściej zupełnie prymitywne. Dla przykładu w języku C++ można znaleźćjedynie konstrukcje odpowiednie dla zapisu stałych numerycznych, znakowychi napisowych. Istnieje także odpowiednia notacja dla tablic, ale może być onastosowana tylko podczas inicjacji.

Czyż nie byłoby właściwe, gdyby obiekty dowolnych kolekcji można byłozainicjować wyrażeniami wprost wyliczającymi ich elementy? Na przykład tak:

vector<int>* v = new vector<int>( {3, 4, 5} );

Niestety w C++ nie jest to możliwe. W praktyce trzeba stosować nienaturalnepodejście proceduralne:

vector<int>* b = new vector<int>;v->push_back( 3 );v->push_back( 4 );v->push_back( 5 );

Języki programowania zorientowane są na opis struktur danych i algorytmów.Opis danych wydaje się stanowić odrębną dziedzinę. Poza wyjątkiem Lispa, przyjęłosię, że dane są tylko„wejściem” dla programu, a nie jego częścią. Ponadtoproblemem jest brak standardów. Istnieje duża liczba specjalizowanych językówopisu danych – np. HTML do opisu stron internetowych czy VHDL do opisuukładów elektronicznych, ale jedynie XML pretenduje do miana językauniwersalnego.

Autor niniejszej pracy wyraża pogląd, że dobry język programowania ogólnegoprzeznaczenia powinien zawierać w sobie uniwersalną notację zorientowaną na opisdanych. Dane bowiem często stanowią integralną część programów, a czasami nawetmogą być traktowane jako programy same w sobie. Dla przykładu lista operatorówwraz opisem ich priorytetów i zasad łączności musi być częścią programuobliczającego wartości wyrażeń arytmetycznych, natomiast dokument HTML możebyć traktowany jako program wykonywany na maszynie, którą jest przeglądarkainternetowa. Ponadto bardzo wskazane byłoby wprowadzenie w dziedzinie opisudanych takich standardów, jakie obecne są od lat w językach programowania.

Elementy opisuNajwiększym problemem, wciąż czekającym na rozwiązanie, jest uzyskanie

powszechnej zgody co do tego, z jakich konstrukcji powinien składać sięuniwersalny język opisu danych. Zacznijmy od przeglądu istniejących rozwiązań.W języku XML opisy składają się z nazwanych „elementów”, które mogą miećatrybuty w postaci krótkich napisów oraz treść tekstową, która może zawierać takżeinne (zagnieżdżone) elementy. W języku List występują S-wyrażenia, które są listamizawierającymi symbole i zagnieżdżone listy. W wielu językach specjalistycznychznajdujemy także opisy w postaci zbiorów par klucz-wartość (np. opisy obiektóww Pov-Rayu).

Zdaniem autora żadne z istniejących rozwiązań nie jest wystarczająco dobre.XMLa dyskwalifikuje dziwaczny (z punktu widzenia programowania) model danych

Page 25: Tylko książka (pdf)

2.3 Język opisu danych 23

nastawiony na „wzbogacanie” informacji tekstowej. S-wyrażenia z Lispa są zbytprymitywne i często trudno jest określić ich znaczenie bez odpowiedniegokomentarza.

Idealny język opisu danych powinien spełniać przynajmniej następujące wymagania:

1. Powinien być podzbiorem języka programowania, aby możliwe byłobezproblemowe osadzanie opisów w kodzie źródłowym programów oraz,aby jego model danych był kompatybilny z modelem danych językaprogramowania.

2. Tekstowy opis danych powinien mieć własność samoopisywalności – tj.znaczenie danych powinno być zrozumiałe dla człowieka nieznającegodefinicji ich struktury.

3. Powinien być prosty.

Język Lisp spełnia tylko postulat pierwszy i trzeci, a XML tylko drugi (nad trzecimmożna dyskutować, aczkolwiek Lisp na pewno jest dużo prostszy).Rekordy i listy

Analizy różnych możliwości, ich wad i zalet oraz próby ich zastosowaniaw praktyce doprowadziły autora tej pracy do głębokiego przekonania, żew kontekście wyżej wymienionych postulatów, najlepszym rozwiązaniem są opisyzłożone z list, które mogą zawierać dowolne zagnieżdżone opisy, i z rekordówskładających się z par klucz (prosty napis) – wartość (dowolny opis). Ponadto każdyopis powinien móc być poprzedzony znacznikiem (prosty napis) wskazującym naznaczenie całości.

Taki model danych może być traktowany jako rozszerzenie S-wyrażeń z Lispao konstrukcje, które wprowadzają własność samoopisywalności – znaczenieodpowiednio skonstruowanych danych może być łatwo odczytane bez pomocydodatkowych wyjaśnień. Co więcej, listy i rekordy są konstrukcjami dobrze znanymiz języków programowania i również dla opisu struktury danych z nich złożonychmożna z powodzeniem zastosować dobrze znane definicje typów (odpowiednikXML Schema).Przykład

W językach programowania pozbawionych konstrukcji zorientowanych na opisdanych często spotyka się budowanie złożonych obiektów za pomocą sekwencjiwywołań metod. Przykładem takiej sytuacji jest tworzenie elementów interfejsuużytkownika – w dokumentacji biblioteki Swing z języka Java można znaleźćnastępujący przykład2:

//Set up the banner at the top of the windowbanner = new JLabel("Welcome to the Tutorial Zone!",

JLabel.CENTER);banner.setForeground(Color.yellow);banner.setBackground(Color.blue);banner.setOpaque(true);banner.setFont(new Font("SansSerif", Font.BOLD, 24));banner.setPreferredSize(new Dimension(100, 65));

JPanel bannerPanel = new JPanel(new BorderLayout());bannerPanel.add(banner, BorderLayout.CENTER);bannerPanel.setBorder(BorderFactory.createTitledBorder("Banner"));

2 źródło: http://java.sun.com/docs/books/tutorial/uiswing/components/example-1dot4/ColorChooserDemo.java

Page 26: Tylko książka (pdf)

24 Rozdział 2 Wybrane zagadnienia programowania wysokiego poziomu

To, że wygodniej definiuje się takie konstrukcje za pomocą opisu, niż zapomocą sekwencji wywołań metod, zauważono już dawno i z tego powodu powstałynarzędzia umożliwiające definiowanie interfejsu użytkownika w plikachzewnętrznych, najczęściej w formacie XML. Na przykład na stronach poświęconychprojektowi „Java GUI Builder”3 napisano:

The Java Gui Builder program is designed to decouple the GUI building codefrom the rest of the application code, without hand-writing code. It allows oneto describe the layout of windows and controls using an XML file.

XML'owym odpowiednikiem wcześniejszego kodu mógłby być następujący opis:<panel layout=”border” border=”titled”> <label alignment=”center” foreground=”yellow” backgroud=”blue” opaque=”false”> <font face=”SansSerif” style=”bold” size=”24”/> <preferred_size x=100 y=65/> </label></panel>

chociaż przy dużej liczbie atrybutów lepiej wyglądałoby zastosowaniezagnieżdżonych elementów:

<panel> <layout> border </layout> <border> titled </border> <label> <alignment value="center"/> <foreground value="yellow"/> <background value="blue"/> <opaque value="false"/> <font face=”SansSerif” style=”bold” size=”24”/> <preferred_size x=100 y=65/> </label></panel>

To samo można zapisać za pomocą S-wyrażeń (z Lispa) – zwięźle:(panel border_layout titled ((label „Welcome to the Tutorial Zone!” center yellow blue true (font „SansSerif” „bold” 24) (dimension 100 65))))

ewentualnie nieco bardziej czytelnie:(panel

(layout border)(border titled)(label

(alignment center)(foreground yellow)(background blue)(opaque false)(font „SansSerif” „bold” 24)(preferred_size 100 65)

))

3 http://jgb.sourceforge.net/index.php , wrzesień 2005

Page 27: Tylko książka (pdf)

2.3 Język opisu danych 25

Za pomocą zaproponowanych list i rekordów opis tego samego panelu mógłbywyglądać następująco:

Panel ( layout = `border_layout border = `titled elements = { // lista jednoelementowa Label ( text = "Welcome to the Tutorial Zone!" alignment = `center foreground = `yellow background = `blue opaque? = true font = Font( "SansSerif", `bold, 24 ) preferred_size = Dimension(100, 65) ) })

Notacja użyta dla atrybutów font oraz preferred_size jest dodatkową notacjąkrotkową zapewniającą lepszą zwięzłość w przypadkach, gdy znaczenieposzczególnych elementów można łatwo wydedukować.

Zaletą zapisu przy użyciu rekordów i list jest spójna logika konstrukcji –przeciwnie do XMLa, w którym często nie jest jasne, które własności powinny byćatrybutami, a które elementami i nie ma rozróżnienia pomiędzy listami elementówa listami atrybutów. W stosunku do zapisu lispowego wprowadza się większąjednoznaczność dzięki odróżnieniu nazw atrybutów od innych wartości.Dlaczego te i tylko te konstrukcje?

Można by się zastanowić nad wprowadzeniem kilku dodatkowych konstrukcji– podczas prac koncepcyjnych nad proponowanym językiem opisu danychrozważano początkowo wprowadzenie także zbiorów (listy nieuporządkowane) orazmap (odwzorowania o dowolnych kluczach, nie tylko napisach). Uznano jednak, żechoć jednoznaczność byłaby jeszcze większa4, to jednak para konstrukcji lista-rekordma zdecydowanie lepszy stosunek ekspresywności do złożoności. Warto także dodać,że szereg konstrukcji podwyższających ekspresywność jest nieskończony – pozazbiorami i mapami można by dalej dodawać np. listy bez powtórzeń, mapyróżnowartościowe itd.Język Harpoon

Szczegóły dotyczące sposobu realizacji przedstawionego modelu opisu danychw połączeniu językiem programowania przedstawione są w rozdziałachprezentujących język programowania Harpoon. W szczególności bardzo istotne jest,w jaki sposób definiuje się strukturę takich danych – jest to omówione jednocześniez omówieniem systemu typów języka Harpoon, ponieważ w wyniku zunifikowaniajęzyka programowania z językiem opisu danych, system typów pełni funkcjępodwójną.

4 Zbiory pozwoliłyby odróżnić listy, w których kolejność ma znaczenie (np. lista instrukcji kodzie źródłowymprogramu) od tych, gdzie porządek może być dowolny (np. lista deklaracji podprogramów). Mapy natomiastbyłyby przydatne np. do opisu pozycji w partii szachów, która jest odwzorowaniem współrzędnych pól nasymbole bierek. Można to zrobić za pomocą listy rekordów, ale nie byłoby wtedy możliwości zadeklarowania, żena jednym polu może znajdować się tylko jedna bierka.

Page 28: Tylko książka (pdf)

26 Rozdział 2 Wybrane zagadnienia programowania wysokiego poziomu

Bazy danychMimo, iż cele, dla których zaprojektowano język opisu danych oparty na

rekordach i listach były inne, to jednak samo narzuca się pytanie o możliwośćzastosowania tego modelu do tworzenia baz danych. Wiadomo, że potrzebastworzenia alternatywy dla relacyjnego modelu baz danych jest silnie odczuwanaprzez wielu programistów. Nowych możliwości upatruje się w obiektowych bazachdanych oraz w bazach danych opartych na języku XML. Język XML jest równieżczęsto stosowany do jako format docelowy mechanizmów serializacji realizującejtzw. „lekką” trwałość obiektów. Choć zagadnienia te nie zostały jeszcze dokładniezbadane z punktu widzenia zaproponowanego modelu danych, to jednak wydaje się,że możliwości na tych polach są bardzo duże.

Page 29: Tylko książka (pdf)

2.4 Konstruktory 27

2.4 Konstruktory

Konstruktory klasyczneW popularnych językach programowania obiektowego konstruktor jest

specjalną metodą, której zadaniem jest inicjacja poprawnego stanu obiektu. Czasamimówi się, że konstruktor jest odpowiedzialny za konstruowanie obiektu, alew praktyce możliwe jest to jedynie w prostych przypadkach. W celu utworzeniazłożonych obiektów najczęściej tworzy się obiekt za pomocą konstruktora,a następnie za pomocą normalnych metod dodaje się kolejne elementy i ustawia sięróżne parametry.

Zwykle konstruktory nie mogą być deklarowane w interfejsach i nie podlegajądziedziczeniu. Można jednak wskazać sytuacje, w których wiele różnych klas używatakich samych form konstruktora – np. konstruktory klas kolekcji w bibliotece STLmogą pobierać pary iteratorów wyznaczające elementy, które mają się znaleźćw tworzonym obiekcie. Czyż konstruktor taki nie powinien być elementem ogólnegointerfejsu kolekcji?

Nowe spojrzenie na konstruktoryW języku Harpoon proponuje się zupełnie inne spojrzenie na to czym jest

konstruktor – uważa się go za metodę deserializującą, a nie inicjującą. Parametremwywołania konstruktora jest więc pełny opis obiektu, który ma być utworzony.W językach C++, Java czy C# podejście takie byłoby bardzo trudno realizowalne,ponieważ, nie licząc absurdalnej dla prostych przypadków możliwości deserializacjiz plików XML'owych, języki te nie zapewniają odpowiedniego wsparcia dlatworzenia złożonych opisów w kodzie źródłowym programu. Harpoon jednakposiada wbudowany język opisu danych, za pomocą którego omawiane zadanie stajesię proste.

Przykład (ten sam, który znajduje się w rozdziale Język opisu danych):var panel : Panel = Panel ( layout = `border_layout border = `titled elements = { // lista jednoelementowa Label ( text = "Welcome to the Tutorial Zone!" alignment = `center foreground = `yellow background = `blue opaque? = true font = Font( "SansSerif", `bold, 24 ) preferred_size = Dimension(100, 65) ) } )

Jest to deklaracja zmiennej zainicjowana opisem, który z punktu widzeniajęzyka programowania jest wywołaniem konstruktora klasy Panel (słowokluczowe new jest nieobowiązkowe) z parametrami w postaci par klucz-wartość. Typem parametru elements jest wbudowana klasa List. Jak widać poprzykładach wywołań konstruktorów Font oraz Dimension, gdy jest towskazane można używać także standardowej składni wywołania funkcji.

Szczegóły dotyczące przebiegu procesu konstruowania obiektu, od

Page 30: Tylko książka (pdf)

28 Rozdział 2 Wybrane zagadnienia programowania wysokiego poziomu

przydzielenia pamięci po zapewnienie zainicjowania każdego elementu klasy, zostałoomówione w rozdziale poświęconym językowi Harpoon.

Page 31: Tylko książka (pdf)

2.5 Polimorfizm 29

2.5 Polimorfizm

Czym jest polimorfizm?Polimorfizm jest jednym z podstawowych pojęć w programowaniu

obiektowym. Różne klasy w różny sposób implementują te same interfejsy, coprzejawia się tym, że ta sama metoda wywołana dla różnych obiektów, możerealizować swoje zadanie w zupełnie inny sposób. Dzięki temu można tworzyćuniwersalne algorytmy, które mogą być stosowane dla obiektów różnych klas. Mówisię tutaj o polimorfizmie (wielości form), ponieważ różnie działające obiektyużywane są tak samo. Jest to jednak jeden z wielu możliwych rodzajówpolimorfizmu – tzw. polimorfizm behawioralny – który nie jest jedynymspotykanym w programowaniu. Istnieje również polimorfizm strukturalny,w czasach panowania języka C często spotykany, dziś często potępiany i zepchniętyna margines przez ideologów głoszących hasła o jedynie słusznej orientacjiobiektowej. O tym, że mimo powszechnego oburzenia nie jest on chorobą, i że możebyć właściwym rozwiązaniem w pewnych sytuacjach postarano się wykazaćw niniejszym podrozdziale.

Polimorfizm strukturalnyKomunikaty w GUI

Typowym przykładem polimorfizmu strukturalnego są obiekty komunikatówreprezentujące akcje wykonywane przez użytkownika w graficznym środowiskuużytkownika – może to być informacja o przesunięciu myszy wraz ze współrzędnymikursora albo o wciśnięciu klawisza na klawiaturze z kodem tego klawisza. Doreprezentowania takich obiektów o zmiennej strukturze używa się mechanizmu unii,której użycie poprzedza zazwyczaj instrukcja switch rozstrzygająca właściwyprzypadek struktury.

W języku C procedury przetwarzające takie komunikaty wyglądają podobnie do tej:void process_message(SDL_Event event){ ... switch (event.type) { case SDL_KEYUP : ... break; case SDL_MOUSEBUTTONDOWN : ... break; case SDL_MOUSEBUTTONUP : ... break; } ...}

Dla komunikatów pochodzących od myszki i od klawiatury nie możnastworzyć wspólnego interfejsu, ponieważ nie różnią się one zachowaniem(p. behawioralny), ale zawierają informacje o różnej strukturze (p. strukturalny).

W tym przypadku można jednak inaczej zorganizować program, tak abyodpowiadał zasadom paradygmatu obiektowego – trzeba w tym celu zastosowaćwzorzec projektowy „Obserwator” [10] (np. słuchacze w Javie), który w miejscenieformalnego kontraktu w postaci specyfikacji zbioru możliwych komunikatów,wprowadza obowiązek rejestrowania się odbiorców u nadawców w ten sposób, żesegregacja komunikatów ze względu na strukturę przekazywanej informacjinastępuje już po stronie nadawcy, a każdy z odbiorców (jest ich tutaj więcej) jestprzystosowany do odbierania komunikatów tylko jednego rodzaju.

Page 32: Tylko książka (pdf)

30 Rozdział 2 Wybrane zagadnienia programowania wysokiego poziomu

Przeciwnicy unii argumentują, że utrudniają one rozbudowę programu,ponieważ za każdym razem, gdy zachodzi konieczność dodania nowego wariantu(w tym przypadku: nowego rodzaju zdarzeń), lub co gorsza trzeba zmienić strukturęjednego z wariantów, to w konsekwencji zmian wymaga kod w wielu miejscachprogramu. Jako remedium proponuje się stałe dążenie do wydzielania wspólnychinterfejsów. Stwierdzeniom tym trudno odmówić słuszności, aczkolwiek zdarzają siętakie sytuacje, w których można mieć pewność, że zbiór wariantów pozostanieniezmienny albo projekt jest z założenia prosty i nad wymaganiem rozszerzalnościdominuje wymaganie prostoty (np. w przypadku programów pisanych w celu ichjednokrotnego uruchomienia).Drzewa składniowe

Spotyka się także sytuacje, w których unie są najbardziej naturalnymrozwiązaniem. Jest tak w przypadku drzew analizy składniowej programówkomputerowych. Elementami takich drzew są opisy deklaracji zmiennych, instrukcjisterujących, definicji procedur itd., które bardzo różnią się między sobą podwzględem struktury niesionej informacji – np. opis deklaracji zmiennej zawierainformacje o nazwie i typie, natomiast opis pętli składa się z opisu warunku orazopisu ciała pętli; ciało pętli natomiast jest opisywane za pomocą listy opisówkolejnych jej instrukcji. Drzewa te są tworzone, analizowane i modyfikowane naróżnych etapach pracy kompilatora i lista możliwych operacji na nich może sięczęsto zmieniać w trakcie rozwoju projektu. Sugerowane przez paradygmatobiektowy utworzenie metod odpowiadających wszystkim operacjom związanym zdanymi zawartymi w węzłach byłoby beznadziejnie złym rozwiązaniem, gdyżprowadziłoby do zawarcia w tych samych modułach tak różnych procedur jaksprawdzanie typów i generowanie kodu pośredniego. Linie podziałów międzymodułami kompilatora powinny przebiegać w zupełnie inny sposób.

Zastosowanie w tym przypadku unii jest więc najbardziej naturalnymrozwiązaniem, o czym świadczą nawet rozwiązania przyjmowane przezzwolenników czystej obiektowości. W takich sytuacjach wyprowadzają oniwszystkie klasy węzłów ze wspólnego typu bazowego (np. TreeNode), co jestrównoważne utworzeniu unii wszystkich typów węzłów. W celu uniknięcia instrukcjiswitch proponują zastosowanie wzorca projektowego „Wizytator” [10], który jednaknie usuwa wad rozwiązania z języka C – zmiany w zbiorze wariantów mają nadalszerokie konsekwencje, a jego jedyną istotną zaletą jest to, że nie wymagaodwoływania się do dynamicznej informacji o typie obiektów i rzutowania w dół.

Unie obiektoweMechanizm unii spotykany w języku C jest prymitywny, jego główną wadą jest

to, że nie zapewnia automatycznej identyfikacji aktualnego wariantu (typuwchodzącym w skład unii), co wymusza na programiście dodatkowy wysiłek alboprowadzi do błędów. Można jednak znaleźć lepsze rozwiązania – np. w językachOCaml [5] i Vault [29] zastosowano tzw. unie oznaczone (ang. tagged union),w których oprócz wartości wariantów zawarte są również wartości znaczników(inaczej zwanych konstruktorami lub tagami) identyfikujące bieżący wariant.

Jeszcze inną możliwością jest połączenie unii z paradygmatem obiektowym.Jeśli przyjrzeć się różnym przypadkom programów, w których występuje rzutowaniew dół lub wzorzec projektowy Wizytator, to okaże się, że występujące tam typy takna prawdę traktowane są jako unie typów, a do identyfikowania wariantu możnawykorzystać mechanizm RTTI (identyfikacji typu podczas działania programu).

Page 33: Tylko książka (pdf)

2.5 Polimorfizm 31

Nadużywanie rzutowania w dół i RTTI jest słusznie uznawane za przejaw złego styluprogramowania, ale w pewnych, stosunkowo rzadkich sytuacjach, może być torozwiązaniem właściwym; szczególnie, jeśli możliwe byłoby jasne oddzielenie tychsytuacji od tych, gdzie rzutowanie w dół nie powinno mieć miejsca.

Postulat:

System typów obiektowego języka programowania powinien dostarczaćmechanizmów pozwalającym rozróżnić typy:

• pełniące funkcje interfejsów, przeznaczonych do bezpośredniego użyciai z zabronionym rzutowaniem w dół,

• pełniące funkcje unii, dopuszczających rzutowanie w dół w celuuzyskania interfejsu odpowiedniego dla bieżącego wariantu.

W obecności takiej funkcjonalności niemożliwe byłoby nadużywanie RTTIw sytuacjach, gdy nie jest to potrzebne, np. wewnątrz podprogramów (składnia C++):

int count_elements( List* list );void Set::contains( Object* obj ); // wystarczą porównania

natomiast w przypadku podprogramów, do których obiekt przekazywany jest jakounia, rozstrzyganie wariantów byłoby dozwolone:

void generate_code( TreeNode* node ); // część kompilatoravoid process_message( Event* e ); // komunikaty w GUIvoid Set::insert( Object* obj ); // kolekcja jak w Javie 1.4

Szczególnie przykład klasy Set jest interesujący ze względu na szerokąobecność w standardowej bibliotece kolekcji języka Java (w wersji 1.4). Konteneryprzechowują tam obiekty typu Object, ale w rzeczywistości traktują je jako uniewszystkich typów.Nowe oblicze uniiMożliwe jest włączenie koncepcji unii do paradygmatu obiektowego uznając, że:

• Unia jest wspólnym nadtypem pewnej grupy typów, który może byćtworzony po utworzeniu typów składowych (w przeciwieństwie donadtypów wynikających z dziedziczenia).

• Funkcję identyfikatora wariantu (znacznika) pełni RTTI (getClass,instanceof).

Oczywiście należy zapewnić odpowiednie wsparcie dla tych koncepcji napoziomie składni języka.

Przykład (język Harpoon):// definicja uniiunion Event has

MouseEventKeyboardEventWindowEvent

end// przykład rozstrzygania wariantuswitch event.interface to // odpowiednik getClass() z Javy

case MouseEvent : ...case KeyboardEvent : ...case WindowEvent : ...

end

Page 34: Tylko książka (pdf)

32 Rozdział 2 Wybrane zagadnienia programowania wysokiego poziomu

Unie z typem NullKoncepcja unii pozwala także bardzo ładnie potraktować przypadki

zmiennych, które nie mogą mieć wartości null. Zmienne, które mogą przyjmować tęwartość, można traktować jako zmienne o typie będącym unią pewnego typu i typuNull. Można by wprowadzić dla takich unii ułatwienie składniowe – np. operatorpostfiksowy '*' dla wyrażeń typów (gwiazdka jest nawiązaniem do zerowalnychwskaźników z języka C++).

Przykład (składnia języka Harpoon):routine traverse( tree : BinaryTree* ) // uwaga: może być null!

routine count_elements( List list ) // wartość null byłaby błędna

Podobne rozwiązanie (z sufiksem „?”), aczkolwiek bez interpretacji używającejpojęcia unii, wprowadzono w wersji 2.0 języka C#5.Uwagi na temat nazw

Wprowadzenie unii może czasami powodować konflikty nazw między uniamia interejsami – na przykład nazwa TreeNode może być używana dla ogólnegointerfejsu elementów drzewa, ale także dla unii wszystkich typów tych węzłów.Podobny problem może występować także między interfejsami i klasami.Rozwiązaniem może być stosowanie odpowiednich prefiksów: „C” dla klas, „I” dlainterfejsów, „U” dla unii. Jeśli jednak w praktyce okaże się, że konflikty sąstosunkowo rzadkie, lepiej będzie dopuścić pokrywanie się nazw, a ewentualneniejednoznaczności rozstrzygać dopisując precyzujące słowa class, union, interfaceprzed nazwą typu.

Przykłady:ITreeNode lub interface TreeNode // interfejs wszystkich węzłówUTreeNode lub union TreeNode // unia wszystkich typów węzłów

IFile lub interface File // interfejs plikówCFile lub class File // klasa plików fizycznych

// przykład użycia w deklaracji podprogramuroutine generate_code( data : union TreeNode )routine generate_code( data : UTreeNode )

Dodatkowo zasadne byłoby wprowadzenie notacji odpowiadającej przypadkomz innych języków – interfejsom jako uniom typu ze wszystkimi jego podtypami.Jedną z możliwości byłoby wprowadzenie operatora sufiksowego „+”. Wtedy typUTreeNode byłby równoważny typowi ITreeNode+. Unia typu ze wszystkimi jegonadtypami mogłaby być oznaczona za pomocą analogicznego operatora „–”.Korzyści

Unie w przedstawionej powyżej formie stanowią uzupełnienie interfejsów i, cobardzo ważne, zachowują pełną spójność języka. Ich wprowadzenie znaczącopodnosi ekspresywność systemu typów, ponieważ umożliwiają one:

• stosowanie polimorfizmu strukturalnego, a nie tylko behawioralnego,

• rozróżnienie typów, dla których dopuszczalne jest rzutowanie w dół od tych,dla których jest to zabronione,

• określenie, które zmienne mogą przyjmować wartość null, a które nie.

5 http://msdn.microsoft.com/vcsharp/2005/overview/language/nullabletypes/

Page 35: Tylko książka (pdf)

2.5 Polimorfizm 33

Ponadto za ich pomocą można szybciej implementować proste programy, dla którychmechanizm interfejsów bywa nadmiernie złożony.

Page 36: Tylko książka (pdf)

34 Rozdział 2 Wybrane zagadnienia programowania wysokiego poziomu

2.6 Niektóre zasady projektowania obiektowegoPoznanie repertuaru mechanizmów dostarczanych przez język nie jest

wystarczającym warunkiem dla poprawnego programowania. Bardzo ważne sąrównież zasady właściwego ich stosowania. W tym podrozdziale przedstawionozasady istotne z punktu widzenia możliwości języka prezentowanego w rozdziale 3.niniejszej pracy.

AtomowośćSzwajcarski scyzoryk ma bardzo wiele funkcji – może zastąpić śrubokręt,

pilnik, nóż, piłę, nożyczki, otwieracz do konserw i jeszcze wiele innych. Na co dzieńjednak nikt go jednak nie używa do wykonywania poważnych prac. Lepiejsprawdzają się narzędzia atomowe – proste i wyspecjalizowane do jednego jasnookreślonego zadania. Reguła ta jest wyjątkowo uniwersalna – dotyczy zarównonarzędzi majsterkowicza, jak i projektów programistycznych. Z punktu widzenia klasw programie obiektowym, Bruce Eckel podaje następujące jej znaczenie:

„Niech klasy będą tak atomowe jak to tylko możliwe. Każda klasa powinnamieć jedno jasne przeznaczenie.” [8]

Jednym z powodów istotności tej zasady jest to, że jeżeli jedna klasa łączyw sobie wiele funkcjonalności, to w przypadku wystąpienia potrzeby modyfikacjijednej z nich, programista jest zmuszony do modyfikowania kodu, w którymwystępują zarówno te elementy, które trzeba zmienić, jak i te, które powinnypozostać niezmienione, a to stwarza niebezpieczeństwo powstania błędów.

Szczególne znaczenie zasada atomowości ma dla interfejsów. Jeżeli interfejsynie są atomowe, to podczas ich implementacji może okazać się, że trzebazaimplementować metody, które nie będą wykorzystywane. Problem ten występujedość często w bibliotece standardowej Javy – np. interfejs List zawiera 25 metod,spośród których wiele mogłoby być zaimplementowanych jako zwykłe zewnętrznefunkcje (np. addAll, toArray). Aby implementowanie własnych klas na bazie takichinterfejsów było wykonywalne dostarcza się odpowiednich klas abstrakcyjnychstanowiących szkielet implementacji. Rozwiązanie to nie może być jednak uznane zaeleganckie, ponieważ przeczy możliwości wielodziedziczenia interfejsów,a interfejsy pozostają nieczytelne z powodu swoich rozmiarów.

Niebezpieczne jest traktowanie obiektów jako „dostawców usług”. Prowadzi toczasem do błędnego wniosku, że obiekt powinien dostarczać wszystkich operacjiz nim związanych i w efekcie powoduje łamanie zasady atomowości. Przykładówmożna znaleźć całe mnóstwo w bibliotece standardowej Javy:

// To może być realizowane przez zewnętrzną funkcjępublic URL File.toURL()

// Trudno powiedzieć co to w ogóle ma robicpublic int JComponent.getDebugGraphicsOptions()

W bibliotece STL również można znaleźć ciekawe przypadki:size_type string::find_last_not_of(const basic_string& s, size_type pos = npos) // Searches backward within *this, beginning at min(pos, size()),// for the first character that is equal to any character within s.

Page 37: Tylko książka (pdf)

2.6 Niektóre zasady projektowania obiektowego 35

Obiekt powinien być raczej traktowany po prostu jako element modeluo jednym konkretnym zadaniu. Jeżeli zadań ma więcej, powinien zostać podzielonyna kilka mniejszych obiektów. Pokusa dodawania metod pomocniczych (ang. helpermethods), których zadanie mogłyby być realizowane z zewnątrz na baziepodstawowego interfejsu klasy, jest objawem krótkowzrocznego myślenia.W perspektywie dłuższego czasu najważniejsza jest prostota, którą zapewnia zasadaatomowości. Jeżeli metody pomocnicze stanowiłyby istotne ułatwienie, to dobrymrozwiązaniem jest tworzenie klas opakowujących klasy o interfejsach atomowych wcelu dodania nowych metod (wzorzec projektowy Dekorator [10]).

Jedynym istotnym czynnikiem mogącym czasami decydować o odrzuceniuzasady atomowości jest wydajność. Zdarza się bowiem, że pewne zadanie, któreodpowiada wywołaniu serii metod pewnego interfejsu może być zaimplementowaneefektywniej za pomocą pojedynczej metody (np. addAll, zamiast serii wywołań addw obiekcie listy). Wybór rozwiązania zależy od wymagań konkretnego projektu,niemniej jednak należy pamiętać, że z punktu widzenia jakości projektu udział metodpomocniczych w interfejsach powinien być znikomy. Atomowość metod

Innym przykładem wcielenia zasady atomowości są zasady tworzenia metod.Z języka Eiffel pochodzi zasada Command-Query Separation, która nakazuje bymetody albo modyfikowały stan obiektu (np. vector::pop_back), albo zwracałyinformację o nim (np. vector::back). Metody wykonujące te dwa zadaniajednocześnie są niedozwolone.

Przyjęcie odpowiednich konwencji dla nazw – np. kończenie metodpierwszego rodzaju wykrzyknikiem, może sprawić, że rozróżnienie to będzie dobrzewidoczne.

Podział na modułyDuże programy muszą być podzielone na moduły dobrze określonych

interfejsach i niezależnych implementacjach. Podział taki nie może być jednakprzypadkowy. Zazwyczaj intuicyjnie narzucającą się metodą podziału jestodzwierciedlanie logicznej struktury modelowanego systemu, ale to podejście nie jestwłaściwe. Ważne jest, aby granice między modułami były tak ustalone, aby podczasdodawania nowych możliwości do programu lub podczas przystosowywania go dozmian w wymaganiach, jednoczesnej modyfikacji wymagało jak najmniej modułów(w idealnym przypadku jeden). Z tego powodu generalną zasadą, którą należy siękierować jest:

Należy oddzielać to co może się zmieniać, od tego, co jest stałe.Przykład:

Jednokierunkowe wielowarstwowe sieci neuronowe są obiektami o jasnoustalonej strukturze i zasadzie działania, ale algorytmy uczenia dla nichprzeznaczone mogą być różne. Błędem byłoby więc zdefiniowanie i ukryciestruktury sieci w jednym module, ponieważ algorytmy nauki na tej strukturzebazują i musiałyby być zaimplementowane (przynajmniej we fragmentach)w tym samym miejscu. Jako, że struktura sieci się nie zmienia, można od niejuzależnić wiele modułów. Natomiast algorytmy muszą być ściśleodseparowane. Dobrym rozwiązaniem jest więc stworzenie modułu dla obiektusieci oddzielającego strukturę sieci od implementacji struktury sieci oraz

Page 38: Tylko książka (pdf)

36 Rozdział 2 Wybrane zagadnienia programowania wysokiego poziomu

osobnych modułów dla algorytmów nauki na tej strukturze bazujących.

Jednym z mechanizmów pozwalających na oddzielenie tego co jest zmienne odtego co jest stałe, są metody wirtualne. Za ich pomocą można oddzielić stały interfejsod zmiennych implementacji. Czasami jednak interfejs może być zmienny przystałym zbiorze możliwych wariantów – wtedy należy stosować polimorfizmstrukturalny, co opisano w podrozdziale 2.5 Polimorfizm.

Pełne oddzielenie interfejsów od implementacjiOddzielanie interfejsów klas od ich implementacji pozwala później tworzyć

nowe implementacje, które mogą być podstawiane w miejsce starych. Osobnekonstrukcje dla klas i interfejsów, jakie zastosowano w językach Java i C#, mają nacelu wspieranie takiego projektowania. Niestety, często nie są stosowane napoziomie projektu.

Przykład z biblioteki standardowej Javy:

Klasa File reprezentująca pliki w systemie operacyjnym implementujetylko interfejsy Serializable i Comparable<File>. Wyobraźmy sobie programprzeglądarki plików, który pliki archiwów (*.zip, *.rar) traktuje jako„wirtualne” katalogi, które można przeglądać tak samo, jak pozostałe. Gdybyistniał wydzielony interfejs File, można by było zaimplementować go dla klasyobiektów reprezentujących pliki „wirtualne” i dzięki temu w jednakowy sposóbpozyskiwać informacje o plikach rzeczywistych i wirtualnych. Niestetymożliwość taka nie została przewidziana, więc trzeba kombinować na około.

Przyjęcie zasady, że dla każdej klasy należy osobno zdefiniować interfejs,pozwoliłoby na uniknięcie takich problemów.

Page 39: Tylko książka (pdf)

2.7 Prawa własności, aliasing i zarządzanie pamięcią 37

2.7 Prawa własności, aliasing i zarządzanie pamięcią

Sieć obiektówObiekty mogą przechowywać wskaźniki (lub referencje) do innych obiektów.

Połączenia takie tworzą graf (sieć) powiązań międzyobiektowych. Rozumieniestruktury i dynamiki tego grafu jest jednym z podstawowych warunków rozumieniaprogramu. W językach programowania pozbawionych automatycznego odśmiecaniapamięci (zwanego także „zbieraczem nieużytków” lub „odśmiecaczem”) wiedza tama także szczególne znaczenie dla zapewnienia poprawności, ponieważ jej brak jestprzyczyną bardzo złośliwych wycieków pamięci i błędów spowodowanychodwołaniami do obiektów, których pamięć została zwolniona. W językachposiadających mechanizm odśmiecania problemów jest nieco mniej, co nie znaczy,że nie występują wcale – do kłopotów może prowadzić m.in. brak rozróżnieniamiędzy metodami zwracającymi obiekty nowe, a tymi, które jedynie udostępniająwidoki na obiekty już istniejące.

Kluczem do rozwiązania wymienionych problemów (a także kilku innych) jestuświadomienie sobie, że niektóre powiązane ze sobą obiekty występują w relacjiwłaściciel-własność, a pozostałe jedynie wiedzą o swoim istnieniu. Z tego powodunależy rozróżnić dwa rodzaje krawędzi występujących w grafie powiązańmiędzyobiektowych – „własnościowe”, które prowadzą do obiektów „własnych”i „poboczne”, które do „obcych”. Dla przykładu kontrolki klasy ComboBox mogązawierać referencje do „obcych” obiektów oczekujących na zdarzenia oraz referencjedo „własnych” kontrolek składowych – m.in. pola edycyjnego, rozwijanej listy orazprzycisku powodującego rozwinięcie (rysunek 2.7.1). Własności grafu obiektówzwiązane z tym rozróżnieniem mają zasadnicze znaczenie dla wielu aspektówprogramu. Zostały one omówione po kolei w dalszej części tego podrozdziału.

Rysunek 2.7.1. Fragment przykładowej sieć obiektów. Referencje doobiektów własnych zaznaczono linią ciągłą, do obcych – przerywaną.

ComboBox

zbiórsłuchaczy

poleedycyjne

lista

Okienkonadrzędne

Page 40: Tylko książka (pdf)

38 Rozdział 2 Wybrane zagadnienia programowania wysokiego poziomu

Logiczna hierarchia danychPo pierwsze, krawędzie własnościowe wyznaczają bardzo ważną logiczną

hierarchię danych. Najbardziej bezpośrednią korzyścią z jej ujawnienia jest to, żejednoznacznie wyznacza ona sens operacji głębokiego porównywania, klonowaniaobiektów i serializowania – otóż operacje te powinny być kontynuowane „w głąb” pokrawędziach własnościowych, a kończone na krawędziach pobocznych (wartościdowiązań pobocznych prowadzących do obiektów klonowanych też muszą byćzmodyfikowane). Wprowadzenie odpowiednich adnotacji formalnych na poziomiejęzyka programowania sprawiłoby więc, że operacje te stałyby się z punktu widzeniaprogramisty tak samo trywialne jak ich płytkie odpowiedniki z innych językówprogramowania.

Poza nielicznymi wyjątkami rodzaj połączenia między obiektami może być,podobnie jak jego typ, określony statycznie, tj. jednakowo dla każdej chwili działaniaprogramu. Powoduje to, że sensowne jest poszerzenie deklaracji zmiennych(lokalnych, argumentów, wartości zwracanych oraz pól w klasach) o odpowiedniąadnotację – owned albo referenced, w skrócie: own i ref. Oczywiście niewielepożytku byłoby z nich bez odpowiedniego wsparcia ze strony kompilatora – próbazduplikowania wskaźnika własnościowego (own) powinna być powstrzymana –chyba, że jednocześnie wskaźnik źródłowy byłby zerowany. Nie możliwe powinnobyć także przepisanie wartości ze wskaźnika ref na wskaźnik own oraz użyciewskaźnika ref do zwolnienia pamięci.PrzykładZaproponowane adnotacje można użyć do bardziej precyzyjnego zdefiniowania klasykontrolki ComboBox w następujący sposób:

class ComboBox{ ... // Odnośnik do okienka nadrzędnego ref Window* parent_window; // Elementy składowe własne own EditBox* editor; own Button* button; own ListBox* list; ...};

AliasingPo drugie, bardzo ważnym aspektem grafu połączeń między obiektami jest

istnienie w nim grup wierzchołków, do których z pozostałej części grafu prowadzitylko jedno dowiązanie. Z punktu widzenia kodu, który operuje takim dowiązaniem,modyfikacje w obrębie wskazywanej grupy danych mogą być dokonywane tylko zapośrednictwem tego dowiązania. Inaczej mówiąc, jeżeli „unikatowe” dowiązanie niezostało użyte, to można założyć, że dane nie zmieniły się. Zjawisko przeciwne doopisywanego, polegające na istnieniu dodatkowych zewnętrznych dowiązań, zwanejest aliasingiem i w dużej mierze stanowi o trudności programowania w językachniefunkcyjnych. W codziennej swojej pracy programiści stale korzystają z założeń oistnieniu albo nieistnieniu zewnętrznych dowiązań – chociażby podczas pisaniainstrukcji zwalniającej pamięć pewnego obiektu.

Normalnym sposobem radzenia sobie z aliasingiem jest nieformalne

Page 41: Tylko książka (pdf)

2.7 Prawa własności, aliasing i zarządzanie pamięcią 39

dokumentowanie możliwości jego powstawania. Niestety specyfikacja nie zawszejest wystarczająca, a brak mechanizmów formalnych uniemożliwia interwencjękompilatora w sytuacjach ewidentnie błędnych.

Definicja

Dowiązaniem zewnętrznym względem pewnego wskaźnika P, posiadającegoprawo własności do pewnego obiektu, nazywamy takie i tylko takiedowiązanie, które nie jest bezpośrednią ani pośrednią własnością wskaźnika P,

PrzykładyDokumentacja metody getBounds klasy Component z biblioteki standardowej

Javy jest następująca:Rectangle getBounds()// Gets the bounds of this component// in the form of a Rectangle object.

Zasadne wydaje się pytanie czy zmodyfikowanie zwróconego obiektu zmienistan komponentu. Choć dla doświadczonych programistów odpowiedź może byćoczywista, to jednak formalnie rzecz biorąc, powyższa specyfikacja jest niepełna – naprzykład w żaden sposób nie zabrania przyjęcia w klasie dziedziczącej rozwiązaniapolegającego na zwracaniu referencji do obiektu stanowiącego jej wewnętrznąreprezentację. Lepsze byłyby formalne (i weryfikowalne) adnotacje rozróżniająceobiekty nowo utworzone od dowiązań do obiektów już istniejących.

Podobnie sprawa wygląda w przypadku argumentów podprogramów. Czyw poniższym przykładzie (C++) tworzona jest całkowicie nowa lista czy teżwykorzystywane są węzły z list przekazanych jako argumenty (co w efekciepowoduje zniszczenie tych list)?

LinkedList* merge( LinkedList* p, LinkedList* q );

W pierwszym przypadku należałoby zaznaczyć, że pobierane są referencje doobcych list. W przypadku drugim wskazana byłaby adnotacja, że przekazane listypowinny mieć tę samą własność co obiekty w momencie niszczenia – brakzewnętrznych dowiązań. W obu przypadkach zwracany obiekt powinien byćoznaczony jako nowo utworzony.Notacja formalna

Problemy aliasingu można częściowo rozwiązać za pomocą tej samej notacji,która przeznaczona jest do wyróżnienia logicznej hierarchii danych, poszerzonejo nową adnotację unq (unique), która oznacza obiekty, do których nie istnieją innezewnętrzne dowiązania. Typowym przykładem takich obiektów są obiekty nowoutworzone lub przeznaczone do zniszczenia. Oczywiście odpowiednie dowiązania,jako jedyne w całej sieci, są własnościowe, tak jak w przypadku adnotacji own.

Dla wymienionych wcześniej przykładów miałoby to następujące zastosowanie:// Javaunq Rectangle getBounds() // zwraca kopięref Rectangle getBounds() // zwraca referencję do istniejącego obiektu

// C++unq LinkedList* merge( ref LinkedList* p, ref LinkedList* q )// tworzy całkowicie nową listę

unq LinkedList* merge( unq LinkedList* p, unq LinkedList* q )// tworzy listę ze starych wierzchołków, listy przekazane są niszczone

Page 42: Tylko książka (pdf)

40 Rozdział 2 Wybrane zagadnienia programowania wysokiego poziomu

Adnotacja unq ma także zastosowanie do oznaczania obiektów stanowiącychwewnętrzną reprezentację innych obiektów – np. w definicji klasy okna mogąznaleźć się następujące pola:

unq Point position;unq Int width;unq Int height;

co spowoduje, że dane te nie będą mogły mieć zewnętrznych dowiązań.

Uwaga:

Adnotacje own i ref oznaczają prawo własności lub jego brak, natomiastadnotacja unq związana jest bardziej z kontrolą występowania aliasów, a jeszcze inneadnotacje zostaną wprowadzone później. Pojawia się więc problem nadania tymokreśleniom wspólnej nazwy. Terminy „prawa własności” czy „adnotacji aliasowej”nie stanowią odpowiedniej generalizacji. Dla potrzeb niniejszej pracy przyjęto więc,określenie dyscypliny (ang. discipline).

Uwaga:

Warunek deklarowany za pomocą dyscypliny unq jest trudny do pełnegoi zarazem szybkiego zweryfikowania. Wiele przypadków błędnych użyć możebyć wykrytych na etapie kompilacji, jednak, jak się wydaje, nie wszystkie.W konsekwencji pełna weryfikacja tego warunku będzie najprawdopodobniejwykonywana w czasie działania w trybie “debug”.

Zarządzanie pamięciąPo trzecie, sieci obiektów stanowią podstawową strukturę omawianą

w kontekście mechanizmów zarządzania pamięcią. Na przykład ręczne zwalnianiepamięci opiera się na założeniu o braku aliasingu, natomiast algorytmy zbieranianieużytków przeglądają tą sieć i zwalniają pamięć odpowiadającą wierzchołkomnieosiągalnym ze zbioru wierzchołków początkowych. Metoda zaproponowana w tejpracy wykorzystuje dodatkową informację o prawach własności.Prawo do zwalniania pamięci

W językach bez zbieracza nieużytków prawo własności do obiektu jest takżeprawem do zwalniania pamięci – w destruktorach zwalnia się pamięć obiektówwłasnych, natomiast obiekty obce pozostawia się w opiece ich prawowitychwłaścicieli. Odpowiedzialność za zapewnienie braku dowiązań do niszczonegoobiektu spoczywa na właścicielu obiektu.

Jako, że w języku C++ nie ma możliwości wyrażenia tego prawa w sposóbformalny, aby nie dopuścić do błędów, programiści musieli sobie radzić za pomocąnieformalnych specyfikacji – na przykład w kodzie źródłowym przeglądarki Firefoxznajdują się następujące zapisy:

(1) morkBookAtom* GetAid(morkEnv* ev, mork_aid inAid); // GetAid() returns the atom equal to inAid, or else nil // note the atoms are owned elsewhere, usuall by morkAtomSpace(2) /** * Initialize a drawing surface using a Macintosh GrafPtr. * aPort is not owned by this drawing surface, just used by it. * @param aPort GrafPtr to initialize drawing surface with * @return error status

Page 43: Tylko książka (pdf)

2.7 Prawa własności, aliasing i zarządzanie pamięcią 41

**/ NS_IMETHOD Init(CGrafPtr aPort) = 0;

(3) nsImapMoveCoalescer *m_moveCoalescer; // strictly owned by nsParseNewMailState;

Zastosowanie adnotacji own i ref zniosłoby taką potrzebę, a ponadtopozwalałoby na wyłapywanie większej ilości błędów przez kompilator.

Page 44: Tylko książka (pdf)

42 Rozdział 2 Wybrane zagadnienia programowania wysokiego poziomu

Zliczanie referencjiProstą metodą automatycznego zarządzania pamięcią jest przechowywanie dla

każdego obiektu licznika referencji, które na niego wskazują. Gdy liczba ta spadniedo zera, pamięć może zostać zwolniona. Zasadniczą wadą tego rozwiązania jest brakmożliwości wykrycia nieużywanych obiektów, które tworzą cykl w grafie powiązań.Mimo, że ich liczniki mają stany większe niż zero, to jednak powinny byćzniszczone. Ten problem dyskwalifikuje proste zliczanie referencji jako pełnąmetodę automatycznego odśmiecania. Zarządzanie pamięci oparte na logicznej strukturze sieci obiektów

Okazuje się jednak, że po wprowadzeniu podziału dowiązań na własnościowei poboczne, problem staje się znacznie prostszy. Otóż, do każdego obiektu ze stertyprowadzi dokładnie jedno dowiązanie własnościowe, więc powstanie cykluw obrębie tych powiązań nie jest możliwe. Pozostaje więc zliczać referencjepoboczne, a pamięć zwalniać po zniszczeniu lub ustaniu ważności dowiązaniawłasnościowego. Jeśli w takim momencie licznik referencji pobocznych byłbyniezerowy, oznaczałoby to błąd, który byłby automatycznie wykrywany.

Co więcej, po gruntownym przetestowaniu programu i stwierdzeniupoprawności działania, można by (w trybie release) całkowicie zrezygnować zezliczania referencji, dzięki czemu możliwe byłoby osiągnięcie maksymalnejmożliwej efektywności pamięciowej i czasowej, dokładnie tak, jak w języku C.

Uwagi dodatkoweKiedy zachodzi unikatowość?

Własność unikatowości dowiązania wyznaczona adnotacją unq oznacza brakinnych zewnętrznych dowiązań, ale w praktyce to ograniczenie może być trochę zbytsilne. Dla przykładu w poniższym fragmencie kodu:

var bmp : unq Bitmap = Bitmap(640, 480)bmp.draw( Line( 0, 0, 100, 50 ) )screen.blit(x, y, &bmp)delete bmp

wydaje się właściwe uznać, że uchwyt bmp jest unikatowy, ale jednak przekazując godo metody blit tworzymy tymczasowe dowiązanie. Czas ważności tego dowiązanianie przekracza jednak wywołania podprogramu blit, więc z punktu widzenia tegotylko kodu (lokalność) uchwyt pozostaje unikatowy.

Aby rozwiązać ten problem, można dla podprogramów dodać adnotacjęspecjalną lent (tak, jak w [3]) do oznaczania argumentów, których czas ważności niemoże przekroczyć czasu trwania danego podprogramu. Wtedy można by tworzyćtymczasowe dowiązania, które nie zaburzałyby lokalnego założenia o unikatowościw podprogramie nadrzędnym.

TODO: Adnotacja 'lent' oznacza także lokalną unikatowość (jak 'restricted' z C)?

Page 45: Tylko książka (pdf)

2.7 Prawa własności, aliasing i zarządzanie pamięcią 43

WartościDotychczasowe rozważania dotyczyły tylko danych składowanych na stercie

i dostępnych zawsze tylko za pośrednictwem wskaźnika. Ze względówwydajnościowych istotne znaczenie mają jednak także dane przechowywane nastosie i przekazywane „przez wartość”, czyli kopiowane (klonowane), a niedowiązywane.

Z drugiej strony, w języku zorientowanym na programowanie obiektowewskazane jest traktowanie wszystkich rodzajów danych jednakowo. Najlepiej bybyło, gdyby to kompilator wykrywał odpowiednie przypadki i generował szybki kodkorzystający z danych składowanych na stosie. Aby ułatwić to zadanie możnawprowadzić jeszcze jedną dyscyplinę dla zmiennych – val (value), która oznaczaobiekty o tzw. „semantyce wartości”, czyli spójne pod względem składni użyciaz innymi obiektami, ale zawsze kopiowane przez wartość. Operator przypisania

Przypisanie jest najważniejszą operacją z punktu widzenia zapewnieniaspełnienia niezmienników wynikających z adnotacji unq, own, ref i val. Z punktuwidzenia semantyki, można rozróżnić trzy przypadki tej operacji:

• Kopiowanie wskaźnika („wiązanie”)- tworzony jest nowy uchwyt do obcych danych.

• Przekazanie wskaźnika- wraz z prawem własności; wskaźnik źródłowy jest zerowany.

• Klonowanie- tworzony jest całkowicie nowy obiekt identyczny ze źródłowym.

Na poziomie kodu źródłowego odpowiedni przypadek może wynikaćz kontekstu lub też, w przypadku niejednoznaczności, można by zastosowaćspecjalną składnię, np.:

a = &b, foo(&b) // kopiowanie wskaźnika

a = ~b, foo(~b) // przekazanie

a = %b, foo(%b) // klonowanie

Mnemotechnicznie znak „&” symbolizuje „wiązanie”, „~” destrukcję (jakdestruktory w C++), natomiast „%” przedstawia dwa różne obiekty po dwóchstronach kreski.

Zasady inferencji właściwej wersji operacji przypisania na podstawiedyscypliny wyrażenia źródłowego i docelowego wyznacza poniższa tabelka:

źródło \ cel val unq own refval % % % N/A

unq % ~ ~ &

own % ~6 ~ &

ref % % % &

Jeżeli właściwą operacją jest przekazanie („~”), a wyrażenie źródłowe niedopuszcza możliwości swojego zniszczenia lub wyzerowania, to występuje błąd.

6 Licznik referencji pobocznych musi mieć wartość 0. W przeciwnym razie sygnalizowany powinien być błąd.

Page 46: Tylko książka (pdf)

44 Rozdział 2 Wybrane zagadnienia programowania wysokiego poziomu

Inne praceSprytne wskaźniki

Przedstawiona tutaj koncepcja dyscyplin zmiennych wyewoluowała odpomysłu wbudowania w język programowania tzw. sprytnych wskaźników (ang.smart pointers), które są rozwiązaniem dla omawianych w tym podrozdzialeproblemów dla języka C++.

W bibliotece standardowej języka C++ znajduje się klasa auto_ptr, którareprezentuje wskaźniki unikatowe i z automatycznym zwalnianiem pamięci.Dodatkowo biblioteka Boost [21] wprowadza cztery kolejne rodzaje sprytnychwskaźników:

Klasa Znaczeniescoped_ptr Wskaźnik niekopiowalny, czyli bezproblemowy.shared_ptr Wskaźnik na obiekty, do których prawo własności jest dzielone

przez wiele wskaźników. Implementuje nieinwazyjne zliczaniereferencji.

weak_ptr Wskaźnik na obiekty współdzielone bez prawa własności. Służydo unikania cykli w obiektach połączonych za pomocąshared_ptr.

intrusive_ptr Wskaźnik na obiekty współdzielone z inwazyjnym zliczaniereferencji – licznik jest wbudowanym w obiekt.

Rozwiązanie to pozwala na uniknięcie ręcznego zwalniania pamięci,rozwiązuje problem wycieków pamięci podczas zgłaszania wyjątków, ale wymagajednak pewnej uwagi w celu ominięcia problemu cykli. Nie wspiera także ścisłegorozróżnienia pomiędzy dowiązaniami własnościowymi i pobocznymi, więc nie niesiekorzyści związanych z ujawnieniem logicznej struktury danych. Istnieją jednak innebiblioteki wspierające ścisłe i przekazywalne prawa własności [25], ale i taknajwięcej korzyści można osiągnąć wbudowując tego rodzaju mechanizm w samjęzyk.AliasJava

W zakresie kontroli aliasingu na poziomie języka programowania były jużprowadzone badania – w artykule „Alias Annotations for Program Understanding”[3] przedstawiono język AliasJava będący rozszerzeniem języka Java o systemczterech adnotacji towarzyszących typom w deklaracjach zmiennych, pozwalający naformalne specyfikowanie występowania aliasingu:

Adnotacja Znaczenieunique Gwarantuje, że wszystkie inne dowiązania są zmiennymi (nie

mogą być to pola obiektów) oznaczonymi lent.owned Gwarantuje, że wszystkie inne dowiązania prowadzą od

zmiennych lub pól mających tego samego właściciela luboznaczonymi lent.

lent Oznacza dowiązania o czasie trwania ograniczonym dowywołania jednej metody.

shared Oznacza obiekt nie podlegający zasadom kontroli aliasingu.

Page 47: Tylko książka (pdf)

2.7 Prawa własności, aliasing i zarządzanie pamięcią 45

System ten w inny sposób traktuje pojęcie własności – jako określenie dlamechanizmu podobnego w przeznaczeniu do standardowej adnotacji private, alezapewniającego całkowity brak możliwości skopiowania „własnej” referencji pozawyznaczone granice. W przypadku standardowej prywatności referencja i tak możebyć przekazana na zewnątrz klasy za pomocą metody zawartej w części publicznej.

Adnotacje unique i lent są bardzo podobne do ich odpowiednikówprzedstawionych w tej pracy, aczkolwiek pojęcie unikatowości jest traktowane niecodokładniej – w AliasJavie dotyczy całkowitej unikatowości, dla której wyjątkistanowią jedynie odnośniki lent, a nie unikatowości zewnętrznej, która dopuszczaistnienie dowiązań prowadzących od obiektów będących w obrębie tego samegowłaściciela.Unikatowość zewnętrzna

Koncepcja unikatowości zewnętrznej pochodzi z artykułu „External Uniqunessin Unique Enough” [6] i stanowi istotne ulepszenie w stosunku do tego, coprezentuje AliasJava, dlatego też została przyjęta w niniejszej pracy.

ProblemyProblem listy cyklicznej

Pewną przeszkodą we wprowadzeniu formalnych adnotacji wyznaczającychlogiczną strukturę powiązań międzyobiektowych stanowi fakt, że czasami zdarza się,że pewne dowiązania do obiektów własnych istnieją niejawnie. Przykładem takiejsytuacji jest lista cykliczna, która zawiera (własne) obiekty węzłów. Właścicielemwszystkich obiektów węzłów jest obiekt listy, a powiązania między węzłami mająjedynie charakter dowiązań do obiektów obcych (rysunek 2.7.2a). Przyjęcieinterpretacji, że lista ma prawo własności do jednego z węzłów, a każdy następnywęzeł jest własnością poprzedniego, jest niedopuszczalne, ponieważ prowadziłoby todo cykli w grafie własności. Można ewentualnie złamać cykl zamieniając ostatniedowiązanie na poboczne (rysunek 2.7.2b), ale nie odzwierciedla to naturalnejhierarchii tych danych i powoduje pewne komplikacje związane z tym, że dyscyplinysą określane statycznie.

(a) (b)Rysunek 2.7.2. Lista cykliczna.

Najlepszym rozwiązaniem byłoby jawne przechowywanie odpowiednichwskaźników własnościowych w pewnej dodatkowej kolekcji przy założeniu, żezostanie ona usunięta przez optymalizujący kompilator.

lista lista

Page 48: Tylko książka (pdf)

46 Rozdział 2 Wybrane zagadnienia programowania wysokiego poziomu

Problem dyscyplin dynamicznychW językach takich jak C/C++ czy Java typy powiązane są w sposób statyczny

ze zmiennymi. To samo zaproponowano wcześniej dla dyscyplin. Niestety czasami,szczególnie przy braku typów generycznych, zdarza się, że typ wartości może byćzmienny – w języku C w takich sytuacjach stosuje się typ void*, a w Javie typObject. Co jednak zrobić, gdy dotyczyłaby to dyscypliny? Z taką sytuacją mamy doczynienia w przepadku „b” przytoczonej wcześniej listy cyklicznej – podczasdodawania nowego elementu może zajść potrzeba zmienienia prawa własnościostatniego połączenia. Najlepszym rozwiązaniem jest unikanie takich sytuacji,a w ostateczności można przechowywać dwa dowiązania – jedno własnościowe,jedno poboczne, z których jedno zawsze byłoby wyzerowane.

Warto zauważyć, że podobny problem istniałby dla modyfikatora constw języku Java 1.4, gdyby tylko był on częścią tego języka. Otóż, musiałyby istniećdwie wersje standardowe kolekcji – jedne przeznaczone dla obiektów typu Object adrugie dla obiektów typu const Object. Inaczej własność stałości nie mogłaby byćwłasnością statyczną.Problem klas sparametryzowanych

Problem dynamicznych dyscyplin stanowiłby bardzo dużą przeszkodę, jeśli niedysponowalibyśmy biblioteką kolekcji sparametryzowanych typem elementów, jak tomiało miejsce np. w Javie w wersji 1.4 lub wcześniejszych.

W obecności kolekcji sparametryzowanych dyscyplina może byćwyspecyfikowana wraz z typem jako parametr odpowiedniego typu, np.:

Set[own Object]Set[ref Object]

Pojawia się jednak problem z metodą contains? sprawdzającą czy wskazany obiektnależy do kolekcji:

interface Set[T:Type] has // interfejs sparametryzowany typem...method contains?(obj : T) : Bool // deklaracja metody...

end

wszak w przypadku zbiorów wyspecjalizowanych dla obiektów o dyscyplinie ownalbo unq następowałoby w przypadku tej metody przekazanie obiektu na własność,a powinno być przekazanie jedynie referencji. W związku z tym przyjęto konwencję,że właściwą dyscypliną zmiennej jest dyscyplina zadeklarowana jako ostatnia.Dyscypliną zmiennej:

var x : unq ref own val Int

jest więc dyscyplina unq, a pozostałe adnotacje są ignorowane.

W przypadku interfejsu zbioru odpowiednia deklaracja „nadpisuje” informacjęniesioną przez parametr typu:

interface Set[T:Type] has...method contains?(obj : ref T) : Bool...

end

Page 49: Tylko książka (pdf)

2.7 Prawa własności, aliasing i zarządzanie pamięcią 47

Problem właściwościDyscypliny wprowadzają nieco komplikacji do języka, który ma jednocześnie

wspierać konstrukcję właściwości znaną z języków C# albo Delphi. Klasycznawłaściwość jest elementem klasy, której odpowiadają dwa podprogramy – czytającyi zapisujący, zwane z angielskiego getterem i setterem. Nie jest oczywiste jakieznaczenie powinien mieć getter dla właściwości z prawem własności. Czy powinienoddawać prawo własności czy tylko zwracać referencję? Dla adnotacji unq sprawajest oczywista – możliwe jest tylko oddawanie wraz z prawem własności. Równieżw przypadku val jest tylko jedna możliwość – klonowanie. Pozostaje więc problemwłaściwości o dyscyplinie own.

Zastosowanie uproszczenia polegającego na zrezygnowaniu z możliwościprzekazywania wraz z prawem własności spowodowałoby utratę spójności języka,ponieważ proste rekordy są traktowane jako klasy złożone z samych tylkowłaściwości, a pole rekordu powinno być traktowane jak każda zwykła zmienna. Niepozostaje więc nic innego jak dopuścić przeciążanie getterów ze względu nadyscyplinę zwracanego obiektu. Może to prowadzić do niejednoznaczności podczasrozstrzygania operacji przypisania, ale rozwiązaniem tego problemu jest stosowaniew takich przypadkach składni w formie jednoznacznej z symbolami „&”, „~” oraz„%”.

KorzyściDyscypliny zmiennych istotnie polepszają ekspresywność i precyzję języka

programowania, ponieważ:

• Intencje programisty są wyrażone bardziej precyzyjnie co ułatwia poprawnekorzystanie z bibliotek napisanych przez kogoś innego i generowanielepszego kodu przez kompilator.

• Błędy związane z zarządzaniem pamięcią są wychwytywane automatycznienajpóźniej w momencie ich wystąpienia, a czasem nawet podczaskompilacji.

• Poza wskazaniem struktury własności między danymi, programista nie musimartwić się o zarządzanie pamięcią, a przy tym nie ma potrzeby stosowaniazbieracza nieużytków, co poprawia efektywność programów, ponieważ daneniszczone są w momencie, gdy stają się niepotrzebne i to w sposób równieszybki jak w języku C.

• Opcjonalna instrukcja delete może być stosowana w funkcji asercji, dziękiczemu programista ma większą kontrolę nad swoimi programami.

• Jawność logicznej hierarchii danych pozwala zapomnieć o problemachzwiązanych z głębokim porównywaniem i klonowaniem. Operacje te sądostępne bez żadnego dodatkowego wysiłku programisty.

Page 50: Tylko książka (pdf)

48 Rozdział 2 Wybrane zagadnienia programowania wysokiego poziomu

2.8 WspółprogramyNajnowsze języki programowania dostarczają wsparcia dla programowania

współbieżnego w swoich bibliotekach standardowych. Zarówno Java, jak i C#oferują klasę wątków oraz rozbudowane mechanizmy służące do synchronizacji.W tym podrozdziale zaprezentowano alternatywne rozwiązanie dla problemówprogramowania współbieżnego – współprogramy, które można traktować jakouproszczone wątki, które przekazują sobie sterowanie w sposób jawny, a nieautomatyczny, czyli w sposób, który stosowany był dla procesów w systemieoperacyjnym Windows 3.11. Istnieje kilka mniej znanych języków programowania,które wspierają współprogramy, m.in. Simula, CLU, Stackless Python, alez niewiadomych powodów nie znalazły one szerszego naśladownictwa. Choć możewydawać się dziwactwem wracanie do prehistorycznych rozwiązań, to jednakprzedstawiono tutaj tezę, że choć wątki są lepsze do niektórych zastosowań, toprostsze współprogramy są bardziej właściwe dla większości zastosowań, ponieważsą wystarczająco dobre oraz niewspółmiernie łatwiejsze w użyciu.

Przykład motywującyNierzadkie są takie problemy programistyczne, dla których nie przyjdzie

programiście do głowy użyć wątków, mimo że bez użycia pewnej formyrównoległości są one bardzo trudne do rozwiązania. Przykładem takiego problemujest zadanie sprawdzenia czy dwa drzewa binarne generują taki sam ciąg elementówpodczas przechodzenia ich w porządku od lewej do prawej (najpierw lewepoddrzewo, potem element bieżącego węzła, potem prawe poddrzewo) (przykładzaczerpnięty z książki [9]).

Rysunek 2.8.1. Dwa różne, ale równoważne drzewa binarne

Przechodzenie drzewa binarnego można prosto i elegancko zrealizować zapomocą procedury rekurencyjnej, jednak wykonanie jednoczesnego przejścia podwóch drzewach jest już zadaniem znacznie trudniejszym. Można by wykonaćosobne przejścia zachowując ich wyniki w tablicach i dopiero w ostatnim etapiedokonać porównania, ale byłoby to bardzo nieefektywne – po pierwsze wymaga tododatkowej pamięci i pośredniego etapu przetwarzania danych, a po drugie wymaga

Page 51: Tylko książka (pdf)

2.8 Współprogramy 49

zawsze pełnego przejścia obu drzew, podczas gdy w optymistycznym przypadkuróżnica może być wykryta już po sprawdzeniu pierwszego węzła. Właściwerozwiązanie (w programowaniu sekwencyjnym) polega na zamienieniu procedurrekurencyjnych na iteracyjne z jawnym reprezentowaniem stosu, ale jest tokilkakrotnie trudniejsze do wykonania i mało eleganckie.

Ładniejszym rozwiązaniem, choć niekoniecznie odpowiednio wydajnym,mogłoby być użycie dodatkowego wątku dla każdego drzewa. Wewnątrz wątkówwykonywałaby się procedura rekurencja, a wątek główny nadzorowałby produkcjękolejnych elementów i dokonywał ich porównywania. Takie podejście jest jednaktypowym przykładem strzelania z armaty do komara. Takie problemy nie wymagająskomplikowanego nadzorcy wątków, który automatycznie rozdziela czas procesora.

WspółprogramyGdy potrzebna jest współbieżność, a automatyczne przełączanie nie ma sensu,

właściwym rozwiązaniem są współprogramy, które podobne są do wątków, alesterowanie przekazują sobie w sposób jawny. Tak jak wątki posiadają odrębne stosy,ale nie wymagają mechanizmów synchronizacji i nie prowadzą do trudnych dowykrycia błędów charakterystycznych dla wątków.

W języku Harpoon współprogramy są specjalnymi obiektami klasy Coroutinesparametryzowanej typem przekazywanych wartości, tworzonymi za pomocąkonstruktora pobierającego bezargumentową procedurę. Przekazanie sterowaniapewnemu współprogramowi jest podobne do wywołania funkcji i odbywa siępoprzez wywołanie metody continue. Do oddania sterowania do współprogramunadrzędnego służy instrukcja yield, analogiczna do instrukcji return. Aby zakończyćwspółprogram należy wywołać bezargumentową procedurę exit.

Page 52: Tylko książka (pdf)

50 Rozdział 2 Wybrane zagadnienia programowania wysokiego poziomu

Rozwiązanie problemu przykładowego 7 // Procedura wspolprogramuroutine traverse( tree : Tree* ) : Int is if tree != null then traverse( tree.left )

// oddaje sterowanie do wspolprogramu nadrzednego // oraz przekazuje tam wartosc elementu drzewa yield tree.value

traverse( tree.right ) endend// Zwykla procedura porownujaca drzewa binarneroutine compare_trees( tree1, tree2 : Tree ) : Bool is // Utworzenie wspolprogramow. // Parametrem konstruktora jest procedura bezarguentowa, // ktora powstaje przez specjalizacje procedury 'traverse'. var cr1 : Coroutine[Int] = Coroutine( traverse[tree1] ) var cr2 : Coroutine[Int] = Coroutine( traverse[tree2] ) // Iteracja po kolejnych elementach repeat var x : Int = cr1.continue(); // kontynuacja wspolprogramu var y : Int = cr2.continue(); if x != y then return false end until not cr1.ready? // sprawdzenie czy wspolprogram sie zakonczyl return not cr2.ready?end

GeneratoryTypowym zastosowaniem współprogramów jest generowanie ciągu pewnych

wartości za pomocą równoległej procedury (jak w przypadku iteracji po elementachdrzewa binarnego). Dla takiego przypadku istnieją w niektórych językachprogramowania konstrukcje jeszcze prostsze niż współprogramy – generatory,czasem zwane iteratorami – np. w języku CLU.

Przykładowo w języku Pasic (eksperymentalny język, który stanowił wprawkęprzed utworzeniem języka Harpoon) można wygenerować sekwencję liczbpierwszych z zadanego zakresu za pomocą następującego programu:

gen primes(a, b : Int) : Int // generatorx : Intfor x in range(a, b) do

if prime?(x) thenret <- x // wartosc zwracanayield

endend

endproc main() x : Int for x in primes(1, 100) do // uzycie generatora print(x) put( Char(32) ) // nowa linia endend

7 Przykład nie przetestowany z powodu braku wsparcia dla obiektów i specjalizowanych funkcji w obecnej wersjikompilatora języka Harpoon.

Page 53: Tylko książka (pdf)

2.8 Współprogramy 51

Zastosowanie generatorów są jednak węższe niż współprogramów – występująone zawsze w hierarchicznej zależności między sobą i sterowanie może przechodzićtylko bezpośrednia między generatorem a używającą go procedurą (która może byćinnym generatorem), podczas gdy współprogramy nie są w żaden sposób ograniczonepod tym względem.

Leniwe struktury danychSwoje najpiękniejsze oblicze współprogramy ujawniają w implementacjach

tzw. leniwych struktur danych. Cóż to takiego? Leniwe struktury danych to procesyobliczeniowe, które mają interfejs danych. Mając np. interfejs listy można by sobiezażyczyć obiektu będącego listą wszystkich(!) liczb pierwszych. Jest to przykładdanych o nieskończonej lub praktycznie nieskończonej wielkości, więc zmieszczenieich w pamięci nie wchodzi w grę. Można je oczywiście starać się implementować wklasyczny sposób, co nie jest niemożliwe, jednak, szczególnie gdy w grę wchodziużywanie procedur rekurencyjnych, najczęściej bardziej eleganckie rozwiązaniemożna stworzyć przy pomocy współprogramów.

Dla przykładu wszystko co można zrealizować za pomocą generatorów możnatakże zaimplementować jako listy jednokierunkowe. Metodzie powodującej przejściedo kolejnego elementu musi jedynie odpowiadać wywołanie generatora lubkontynuacja współprogramu. Mając takie leniwe listy (zwane strumieniami) możnado nich stosować te same funkcje, co do zwykłych list, co pozwala na osiągnięcielepszej jakości kodu.

Implementacja quasi-wyjątkówWyjątki są dobrym mechanizmem radzenia sobie z sytuacjami nietypowymi

w programie, ponieważ oddzielają kod odpowiedzialny za wykonanie zadania odkodu realizującego obsługę błędów. Są niestety również niebanalne w implementacjii komplikują język programowania, który powinien być prosty. Mając jednakwspółprogramy jako konstrukcję podstawową można problem obsługi błędówrozwiązać prościej: w przypadku napotkania błędu można wywołać procedurę abort,a niebezpieczny kod (np. odczyt z pliku, funkcję zawierającą dzielenie) wykonywaćw osobnym współprogramie, co w efekcie spowoduje, że w przypadku powstaniabłędu zostanie zakończony tylko jeden współprogram. Można by nawet nie kończyćwspółprogramu, a jedynie spowodować jego wstrzymanie i przenieść nawspółprogram nadrzędny decyzję co należy zrobić – może się nawet okazać, że błądmożna naprawić na wyższym poziomie (np. otworzyć plik, który był zamkniętypodczas próby czytania) i przerwany współprogram mógłby być kontynuowany.

Za pomocą przedstawionej metody można osiągnąć niemalże to samo, co zapomocą wyjątków, z wyjątkiem automatycznej segregacji i propagacjinieobsłużonych wyjątków do wyższych podprogramów. Blokowi try-catchodpowiada tutaj instrukcja if uruchamiająca niebezpieczny kod w osobnymwspółprogramie i sprawdzająca poprawność po jego zakończeniu. Zgłoszeniuwyjątku odpowiadają instrukcje abort lub yield, które mogą przekazywać obiektbędący odpowiednikiem obiektu wyjątku. Tym sposobem udaje się za pomocą jednejkonstrukcji językowej zrealizować dwa cele – programowanie współbieżne z jawnymprzekazywaniem sterowania i obsługę sytuacji wyjątkowych.

Page 54: Tylko książka (pdf)

52 Rozdział 2 Wybrane zagadnienia programowania wysokiego poziomu

2.9 Operatory

Ułatwienie składniowe dla...Operatory arytmetyczne, relacyjne i logiczne nie są niezbędne z punktu

widzenia paradygmatu obiektowego, ale trudno sobie wyobrazić bez nich wygodnyjęzyk programowania. Wiadomo, że są one tylko ułatwieniem składniowym dlabardziej podstawowych konstrukcji, ale to, jakie konkretnie są to konstrukcje jest jużróżnie określane. W samym tylko C++ można definiować operatory jako metodyi funkcje zaprzyjaźnione. W C# są to już tylko statycznie wiązane metody statyczneklas, natomiast w języku D przeciwnie – są to metody wiązane dynamicznie [24].

Największym problemem z operatorami binarnymi jest to, że z punktuwidzenia matematyki ich operandy powinny być traktowane symetryczne, co niepasuje do standardowego paradygmatu obiektowego, który zakłada, że zawsze tylkojeden obiekt jest odbiorcą komunikatu, a pozostałe mogą być tylko argumentami.Rozwiązanie przyjęte w C# odpowiada postulatowi symetrii, ale niestety nie jestobiektowe w tym sensie, że nie korzysta z dynamicznego wiązania podprogramów napodstawie aktualnego typu obiektu. Aby spełnić oba wymagania – symetrycznetraktowanie operandów i dynamiczne wiązanie potrzebny jest mechanizmwielokrotnej dyspozycji (ang. multiple dispatching), polegający na tym, żenajwłaściwsze podprogramy wybierane są dynamicznie na podstawie typówwszystkich argumentów, a nie tylko na podstawie typu obiektu this będącego„odbiorcą komunikatu”. Niestety wielokrotna dyspozycja jest mechanizmem dośćskomplikowanym ze względu trudności techniczne i pojawiające sięniejednoznaczności.

Spośród dwóch możliwości – metod statycznych i metod traktujących operandyniesymetrycznie – dla języka programowania będącego przedmiotem niniejszej pracywybrano drugie rozwiązanie, uznając, że z asymetrią można sobie zawsze poradzićodpowiednio porządkując operandy i dokonując odpowiednich jawnych rzutowań,natomiast metody statyczne bezwzględnie narzucają odrzucenie dynamicznegowiązania.

SkładniaRównież pod względem składni rozwiązanie pochodzące z języka D [24],

polegające na zarezerwowaniu dla operatorów pewnych szczególnych nazw metod,takich jak opAdd czy opEquals, wydaje się być lepsze od przyjętego w C++ i C#używania słowa kluczowego operator, ponieważ identyfikatory odpowiadającenazwom metod dla operatorów w języku D mogą być nadal używane do innychcelów, natomiast w językach C++ i C# słowo „operator” definitywnie staje sięsłowem zarezerwowanym i nie może być oznaczone do nazwania zmiennych czymetod.

Page 55: Tylko książka (pdf)

3 Język programowania Harpoon 53

3 Język programowania Harpoon3.1 Przegląd cech

Charakterystyczne cechy języka Harpoon• Harpoon jest językiem zorientowanym obiektowo.

• Wartości prymitywne i podprogramy są obiektami.• Interfejsy są ściśle oddzielone od implementacji.

• Interfejsy oprócz metod mogą zawierać właściwości proste isparametryzowane. Właściwość specjalna apply odpowiada indekseromz C# oraz operatorowi funkcji.

• Możliwe są dwa sposoby modelowania danych: dziedziczenie interfejsów(stały interfejs, ale łatwe dodawanie podtypów po stronie klienta) oraz unie(dowolne interfejsy, ale brak możliwości dodawania wariantów po stronieklienta).

• Zarządzanie pamięcią, a także głębokie porównywanie i klonowanieobiektów, oparte jest na jawnej własności obiektów.

• Język Harpoon zawiera w sobie język opisu danych (alternatywny dlaXML'a), dzięki czemu nie jest problemem tworzenie algorytmówsterowanych danymi.

• Dostępne są współprogramy stanowiące prostszą alternatywę dla wątków.

• Za pomocą współprogramów można zastąpić wyjątki oraz implementowaćleniwe struktury danych.

• Klasy, metody i podprogramy mogą być sparametryzowane. Parametramimogą być także wartości, a nie tylko typy.

• Hermetyzacja występuje na poziomie modułu, a nie na poziomie klasy.

• Konstruktory traktowane są jako procedury deserializujące, a nie inicjujące.

• Operatory są ułatwieniem składniowym dla metod (wirtualnych).

• Żadna z konstrukcji języka Harpoon nie wyklucza możliwości napisania wprzyszłości kompilatora, który generowałby kod równie szybki jak dlaprogramów pisanych w języku C.

Konstrukcje nieobecne• Prawa dostępu (public, protected, private)

- zamiast tego jest prostsza hermetyzacja na poziomie modułu.

• Metody i pola statyczne- dzięki hermetyzacji na poziomie modułu można zamiast tego stosowaćprocedury i dane globalne.

• Zbieracz nieużytków- zamiast tego istnieje hierarchia własności danych, która umożliwiaznacznie prostsze zarządzanie pamięcią.

• Wątki, mechanizmy synchronizacji

Page 56: Tylko książka (pdf)

54 Rozdział 3 Język programowania Harpoon

- zamiast tego są współprogramy z jawnym przekazywaniem sterowania.

• Wyjątki- można wskazać alternatywę implementowaną za pomocąwspółprogramów.

• Dziedziczenie implementacji- zamiast tego istnieje mechanizm delegacji na poziomie języka.

• Typ tablicowy- zamiast tego istnieje normalna klasa Array implementująca jednocześnieinterfejsy listy oraz funkcji liczb naturalnych.

Page 57: Tylko książka (pdf)

3.2 Zagadnienia składniowe 55

3.2 Zagadnienia składniowe

Struktura leksykalnaKod źródłowyKod źródłowy jest zawarty w pliku tekstowym w standardzie ASCII.Białe znakiNadmiarowe białe znaki, z wyłączeniem znaku końca wiersza, są ignorowane.Znak przejścia do następnego wiersza

Znak „\” będący ostatnim niebiałym znakiem w wierszu oznacza żądaniezignorowania znaku końca wiersza, co jest przydatne do zapisywania długichwyrażeń.

Na przykład: var a : Int = 1 \ + 2 \ + 3

Z powodu braku średników na końcu instrukcji brak znaku „\” spowodowałbyuznanie, że wyrażenie kończy się wraz ze znakiem końca wiersza.KomentarzeKomentarzami, w sensie ciągów ignorowanych przez analizator leksykalny, są napisyzawarte pomiędzy:

• ciągiem „//” (włącznie) a znakiem końca wiersza (wyłącznie),

• ciągiem „(*” (włącznie) a ciągiem „*)” (włącznie).IdentyfikatoryIdentyfikatory są ciągami znaków innymi niż słowa kluczowe i:

• rozpoczynają się literą lub znakiem podkreślenia,

• na dalszych pozycjach mogą zawierać litery, znaki podkreślenia i cyfry,

• a ponadto na ostatnich pozycjach mogą zawierać pytajnik, wykrzyknik lubznak małpy tudzież dowolną ich kombinację.

Przykłady poprawnych identyfikatorów:Set best_score empty? rotate! abc123

Przykłady ciągów, które nie są identyfikatorami:2element best-score empty??? a?b

Literały numeryczne całkowiteLiterały numeryczne dziesiętne:

• składają się z ciągu cyfr (mogą mieć zero na pierwszej pozycji),

• mogą kończyć się jedną z liter: C, B, SI, SN, I, N, LI, LN zarówno w wersjimałej jak i dużej, oznaczające typ (odpowiednio: Char, Byte, ShortInt,ShortNat, Int, Nat, LongInt, LongNat).

Literały numeryczne szesnastkowe:

Page 58: Tylko książka (pdf)

56 Rozdział 3 Język programowania Harpoon

• rozpoczynają się ciągiem „0x”,

• na dalszych pozycjach składają się z cyfr oraz liter od A do Z zarówno wwersji małej jak i dużej, ale tej samej w obrębie literału.

• mogą kończyć się jedną z liter: C, B, SI, SN, I, N, LI, LN zarówno wwielkości (ang. case) różnej od użytej dla cyfr, oznaczające typ tak samo, jakdla literałów dziesiętnych.

Literały numeryczne binarne:

• rozpoczynają się ciągiem „0b”,

• na dalszych pozycjach składają się ze znaków „0” oraz „1”,

• mogą kończyć się jedną z liter: C, B, SI, SN, I, N, LI, LN zarówno w wersjimałej jak i dużej, oznaczające typ tak samo, jak dla literałów dziesiętnych.

Nie istnieją literały numeryczne ósemkowe.

Przykłady literałów numerycznych:123 123I 0997 0xff 0x00a7 0b01011011w

0xbb // 187 dziesiętnie, liczba o typie Int0xbB // 11 dziesiętnie, liczba o typie Byte0xBb // j/w

Literały numeryczne zmiennoprzecinkoweLiterały numeryczne zmiennoprzecinkowe w notacji klasycznej składają się z:

• (być może pustego) ciągu cyfr, kropki, niepustego ciągiem cyfri opcjonalnego sufiksu,

• albo: niepustego ciągu cyfr i obowiązkowego sufiksu,

gdzie sufiksem może być jedna z liter: SR, R, LR, sr, r, lr.

Literały numeryczne zmiennoprzecinkowe w notacji naukowej składają się z:

• (być może pustego) ciągu cyfr, kropki, części wykładniczej i opcjonalnegosufiksu,

• albo: niepustego ciągu cyfr, części wykładniczej i opcjonalnego sufiksu,

gdzie część wykładnicza oznacza ciąg złożony z:

• litery „e” lub „E”,

• opcjonalnego znaku „+” lub „-”,

• niepustego ciągu cyfr.Literały znakoweLiterały znakowe zawierają między parą znaków apostrofów jedno z:

• pojedynczy znak ASCII,

• ciąg „\'” oznaczający znak apostrofu,

• ciąg „\”” oznaczający znak cudzysłowu,

• ciąg „\\” oznaczający znak „backslash”,

• ciąg „\t” oznaczający tabulację,

Page 59: Tylko książka (pdf)

3.2 Zagadnienia składniowe 57

• ciąg „\n” oznaczający znak końca wiersza,

• ciąg „\f” oznaczający znak o kodzie ASCII 0x0c.

• ciąg „\r” oznaczający znak o kodzie ASCII 0x0d.Literały napisoweLiterały napisowe zawierają między parą znaków cudzysłowu ciąg znaków z tymisamymi sekwencjami wyjścia co w przypadku literałów znakowych.Literały symboliczne

Literałami symbolicznymi są dowolne niepuste ciągi liter, cyfr i znakówpodkreślenia poprzedzone znakiem apostrofu.

Przykłady:`left `speed_optimized `class `agent007 `4pip `23

Literały logiczneLiterałami logicznymi są napisy „false” i „true”.Literał nullLiterałem null jest napis „null”.OperatoryOperatorami binarnymi są:

+ - ! notOperatorami arytmetycznymi są:

+ - * / % ^ << >>

Operatorami relacyjnymi są:< > <= >= == != === !==

Operatorami logicznymi i binarnymi są:& && | || #and cand or cor xor

Słowa kluczoweSłowami kluczowymi i zastrzeżonymi są:

and as begin break candcase cimplies class const continuecor default delete do elifelse end enum false finalfloating for getter goto hasif implements implies import ininterface is lent method modulemutable new not null orover own property ptr readonlyrecord ref repeat return routinesetter struct subtypes switch thenthis to true tuple typetype_of union unq until valvar variable while xor yield

Niektóre ze słów kluczowych można zapisywać za pomocą skrótówrozpoczynających się znakiem dolara:

class $c final $fin getter $geinterface $i floating $flo setter $seunion $u readonly $ro method $me

Page 60: Tylko książka (pdf)

58 Rozdział 3 Język programowania Harpoon

struct $s const $co property $precord $r mutable $mutuple $tsubtypes $subtimplements $impl

Słowami kluczowymi nie są m.in.:abstract operator static virtualpublic private protected internalchar short int long booldouble float signed unsignedtry catch finally throw throws

Konwencje składniowe – przedstawienie i uzasadnienieStruktura blokowa programu

Niekwestionowaną podstawową zasadą kształtowania kodu źródłowegoprogramów jest wyróżnianie ich blokowej struktury za pomocą wcięć. Jest to bardzoistotne z punktu widzenia łatwości rozumienia programów przez ludzi.

Od strony formalnej składni, do wyróżniania bloków programu stosuje sięróżne rozwiązania w różnych językach, wśród których można wyróżnić 3 głównenurty:

1. wyróżnienie za pomocą pary nawiasów klamrowych spotykane w rodziniejęzyków wywodzących się od języka C,

2. wyróżnienie za pomocą pary słów kluczowych begin oraz end występującem.in. w Pascalu i Adzie,

3. wyróżnienie za pomocą samych tylko wcięć (najbardziej kontrowersyjne)znane z języka Python.

Każde z tych rozwiązań ma pewne wady i zalety – wpisanie klamerek,podobnie jak wielu innych znaków specjalnych, wymaga znacznego przesunięciadłoni z pozycji wyjściowej, słowa begin oraz end są za to długie, i co gorszauniemożliwiają stosowanie ich nazw jako identyfikatorów, natomiast same tylkowcięcia powodują problemy techniczne powodowane mieszaniem znaków spacji itabulacji przez różnych programistów.

W języku Harpoon zastosowano uproszczoną wersję rozwiązania nr 2 niewykluczającą wprowadzenia rozwiązania nr 3 w przyszłości – bloki programukończą się słowem end, ale słowo begin nie jest wymagane, bowiem początekbloków można rozpoznać na podstawie składni poszczególnych konstrukcji, naprzykład w instrukcji warunkowej bloki zaczynają się po słowach then albo else, a wdeklaracji klasy po słowie has. Poniżej zamieszczono przykłady w kilku możliwychwersjach:

if a <= b then print(a)end

class BlockTest has method test()end

if a <= bthen

print(a)endclass BlockTesthas

method test()end

if a <= b then print(a) endclass BlockTest has method test() end

Page 61: Tylko książka (pdf)

3.2 Zagadnienia składniowe 59

Ponadto wydaje się zasadne umożliwić pisanie krótkich instrukcji warunkowych, jakrównież krótkich pętli while, w jednej linii:

if a <= b then print(a)while a <= b do print(a)

Składnia deklaracji zmiennychKolejną zasadniczą różnicą składniową między różnymi językami jest forma

deklarowania zmiennych. Spotykane są następujące rozwiązania:

1. najpierw nazwa typu, potem nazwa zmiennej,

2. najpierw nazwa zmiennej, potem deklaracja typu,

z których pierwsze, wywodzące się z języka C, wymaga mniejszej liczby znaków, alejest znacznie trudniejsze do analizy składniowej, a ponadto z powodu tego, żewariancja długości wyrażeń określających typ jest zazwyczaj znacznie większa odwariancji długości nazw zmiennych, powoduje, że przejrzyste kolumnowanie kodujest utrudnione.

Przykład:// wersja 1map<int, vector<string> > names_table;int total_names;set< string > all_names;

// wersja 2var names_table : map<int, vector<string> >var total_names : intvar all_names : set< string >

Z wyżej wymienionych względów w języku Harpoon przyjęto rozwiązania drugie –najpierw nazwa zmiennej, potem deklaracja typu.Notacja dla parametrów

Parametry typów i podprogramów generycznych specyfikuje się pomiędzynawiasami kwadratowymi (tak jak w języku Eiffel), a nie kątowymi, ponieważnawiasy kątowe mogą być mylone z operatorami relacyjnymi (ten problem występujenp. w C++), natomiast nawiasy kwadratowe nie kolidują z notacją tablicową,ponieważ takowa w Harpoonie nie istnieje (zamiast tego stosuje się składnięwywołania funkcji).

Przykład:// Harpoon - okSet[ sizeof(Long) > sizeof(Int) ? Long : Int ]

// C++ - jest problemSet< sizeof(Long) > sizeof(Int) ? Long : Int >

Rezygnacja ze średnikaW językach C++, Java i C# kolejne instrukcje oddziela się za pomocą

średników. Dzięki temu można zapisywać kilka instrukcji w jednym wierszu lubrozłożyć jedną złożoną instrukcję na kilka linii. Ze względu na to, że pierwszamożliwość nie należy do dobrego stylu, a druga jest rzadziej stosowana, w językuHarpoon przyjęto odmienne rozwiązanie – instrukcję kończy znak końca wiersza,a w razie potrzeby można, stosując znak „\”, zaznaczyć, że instrukcja będziekontynuowana. Więcej na ten temat w punkcie Znak przejścia do następnegowiersza.

Page 62: Tylko książka (pdf)

60 Rozdział 3 Język programowania Harpoon

Konwencje pisowni identyfikatorówW celu uniknięcia znanego z języków C i C++ pomieszania styli, w języku

Harpoon zaproponowano standardową konwencję tworzenia identyfikatorów.Podstawą podjęcia w tej materii odpowiednich decyzji było wprowadzenie dwóchzasad morfologicznych:

1. Forma graficzna nazwy określającej ten sam byt w różnych kontekstachpowinna być zachowana.

2. Forma graficzna nazwy określającej różne byty w różnych kontekstachpowinna być zróżnicowana.

Przykłady łamania zasady pierwszej można znaleźć w Javie:// deklaracja pewnego polaint length;

// deklaracja settera dla tego samego pola, forma graficzna zmienionavoid setLength( int value )

Zasada druga łamana jest na przykład wtedy, gdy stosuje się tę samą konwencję dlanazw ogólnych (np. „człowiek” w sensie gatunku) i jednostkowych (np. „człowiek”w sensie Jana Kowalskiego). W językach programowania obiektowego z takimprzypadkiem mamy do czynienia, gdy ktoś w ten sam sposób nazywa klasyi zmienne:

class window; // deklaracja klasy

window window; // próba deklaracji zmiennej

W języku Harpoon przyjęto konwencję, że:

• nazwy obiektów, w tym stałych, oraz podprogramów pisze się małymiliterami, kolejne słowa oddzielając znakami podkreślenia,

• nazwy typów pisze się rozpoczynając każde słowo dużą literą.

A ponadto:

• nazwy funkcji zwracających wartość logiczną kończy się pytajnikiem,

• nazwy metod modyfikujących stan obiektu this (z wyjątkiem setterów)i nazwy procedur modyfikujących stan obiektów globalnych kończy sięwykrzyknikiem,

• nazwy podprogramów modyfikujących stan swoich argumentów kończy sięznakiem małpy („@”)8

Przykłady:ComboBox // nazwa klasy

combo_box // nazwa obiektu

gcd // nazwa funkcji (procedury bez efektów ubocznych)

empty? // nazwa podprogramu zwracającego wartość logiczną

rotate! // nazwa metody modyfikującej stan obiektu this

process_stream@ // nazwa procedury zmieniającej obiekty argumentów

8 Znak „@” ma mnemotechnicznie przypominać mieszadło robota kuchennego, które wirując modyfikujeargumenty.

Page 63: Tylko książka (pdf)

3.2 Zagadnienia składniowe 61

appen_file!@ // nazwa metody modyfikującej zarówno this // jak i argumenty

Pozostałe konwencje:

• Zaleca się stosowanie różnych nazw dla interfejsów i realizujących jeimplementacji – interfejs powinien posiadać nazwę ogólną (np. Vector),natomiast klasa nazwę nawiązującą do implementacji (np. GrowableArrayalbo LinkedList).

Page 64: Tylko książka (pdf)

62 Rozdział 3 Język programowania Harpoon

3.3 Instrukcje i wyrażenia

Instrukcje sterująceW zakresie repertuaru instrukcji sterujących wydaje się, że to co do tej pory

wymyślono, stanowi już dość dobrze okrzepnięty standard. Język Harpoon w tymaspekcie nie różni się więc znacząco od innych.Instrukcja warunkowa

Instrukcja warunkowa składa się kolejno: ze słowa kluczowego if, wyrażeniastanowiącego warunek główny, słowa kluczowego then, ciała głównego, opcjonalnejlisty przypadków alternatywnych rozpoczynających się słowami kluczowymi eliforaz z opcjonalnego przypadku domyślnego, rozpoczynającego się słowemkluczowym else, po którym bezpośrednio występuje kod ciała domyślnego. Ostatnieciało jest zakończone słowem kluczowym end.

Przykład:if x < 3.14 then

print(x)elif x < 6.28 then

print(x – 3.14)else

print(0)end

Pętla while-do Pętla while-do składa się kolejno: ze słowa kluczowego while, warunku, słowa

kluczowego do oraz ciała pętli zakończonego słowem kluczowym end.

Przykład:while x < y do

x.inc()end

Pętla repeat-until Pętla repeat-until składa się kolejno: ze słowa kluczowego repeat, ciała pętli

zakończonego słowem kluczowym until oraz warunku.

Przykład:repeat

x.inc()until x >= y

Pętla for Pętla for służy do iterowania elementów kolekcji implementujących interfejs

Iterable (generuje sekwencje iteratorów) lub Enumerable (generuje sekwencjeobiektów). Składa się kolejno: ze słowa kluczowego for, deklaracji zmiennejpozbawionej słowa kluczowego var i (opcjonalnie) typu (może być wydedukowanyautomatycznie), słowa kluczowego in (dla enumeratorów) albo over (dla iteratorów),obiektu kolekcji, słowa kluczowego do oraz ciała pętli zakończonego słowemkluczowym end.

Przykład:// pętla „przez” kolekcję - sekwencja obiektówfor x:Int in range(0,100) do

println(x)end

Page 65: Tylko książka (pdf)

3.3 Instrukcje i wyrażenia 63

// pętla „nad” kolekcją - sekwencja wskaźnikówfor p over range(0,100) do

println( [p] ) // „[p]” – to samo co „*p” w Cend

Instrukcja switch W instrukcji switch instrukcja break nie jest potrzeba na końcu każdego

przypadku i dopuszcza dowolny typ wyrażenia, na podstawie którego wykonywanejest rozwidlenie (inaczej niż w C).

Składa się kolejno: ze słowa kluczowego switch, wyrażenia (o dowolnymtypie), słowa kluczowego to oraz z listy przypadków zakończonej słowemkluczowym end.

Każdy przypadek, poza domyślnym, rozpoczyna się słowem kluczowym case,po którym występuje niepusta lista wyrażeń stałych oddzielonych przecinkami,zakończona dwukropkiem. Kolejne po dwukropku jest ciało przypadku, które kończysię wraz z następnym słowem case, default albo end.

W przypadku domyślnym zamiast słowa kluczowego case i listy wyrażeństałych występuje tylko słowo kluczowe default.Przykład:

switch x tocase 0, 1:

x = x + 1case 1, 2:

x = x - 1default:

return 1end

Uwaga: Dopuszczalne jest powtórzenie tej samej wartości w warunkach wieluprzypadków – oznacza to, że sterowanie ma przejść przez kilka bloków. Wpowyższym przykładzie zmienna x zawsze osiągnie wartość 1.Instrukcja dispatch

Instrukcja dispatch jest podobna do instrukcji switch, ale zamiast nawartościach, operuje na interfejsach i najczęściej służy do rozstrzygania wariantuunii. Co bardzo istotne, w bloku case użyta zmienna ma dla kompilatora typwynikający z bieżącego przypadku. Nie ma więc potrzeby jawnego rzutowania.

Przykład:var e : Event = ...dispatch e to case MouseEvent : return e.is_left_button_pressed? case KeyboardEvent : return e.is_pressed?( VK_SPACE ) default : return falseend

Page 66: Tylko książka (pdf)

64 Rozdział 3 Język programowania Harpoon

Instrukcje break i continueInstrukcje break i continue, mające postać pojedynczych słów kluczowych,

powodują odpowiednio skok do następnej instrukcji po ciele pętli oraz skok dopierwszej instrukcji pętli. Dotyczy to pętli for, while-do oraz repeat-until.Przykład:

i = 0while true do

if i == 100 thenbreak

endi = i + 1

end

WyrażeniaW języku Harpoon występują następujące rodzaje wyrażeń:

• stałe liczbowe, znakowe, napisowe i symboliczne,

• identyfikatory zmiennych i procedur,

• wyrażenia krotkowe ( np. (1, 2) ),

• wyrażenia strukturowe ( np. (a = 1, b = 2) ),

• wyrażenia listowe ( np. {1, 2} ),

• wywołania procedur zwracających pewne wartości,

• wywołania metod zwracających pewne wartości,

• zastosowania operatorów (ułatwienie składniowe dla metod),

• wyrażenia typowe:

• identyfikatory typów,

• wyrażenia typów unijnych ( Int | String ),

• wyrażenia typów wyliczeniowych ( enum { 'a, 'b, 'c } ),

• wyrażenia typów krotkowych ( (Int, String) lub Int * String ),

• wyrazenia typów strukturowych ( ( a : Int, b : String ) ).

Page 67: Tylko książka (pdf)

3.3 Instrukcje i wyrażenia 65

OperatoryOperatory są ułatwieniami składniowymi dla metod. Priorytety i łączność

operatorów binarnych wyznacza poniższa tabela:

Priorytet Łączność Operatory Znaczenie16 prawa ^ potęgowanie15 lewa * / % mnożenie, dzielenie, reszta14 lewa + - dodawanie, odejmowanie13 prawa << >> przesunięcia bitowe12 lewa < > <= >= : as relacyjne, rzutowania11 lewa in należenie do zbioru10 lewa === !== porównania wskaźników9 lewa == != porównania obiektów8 lewa & and iloczyn logiczny lub bitowy7 lewa # xor suma modulo 26 lewa | or suma logiczna lub bitowa5 lewa && cand iloczyn logiczny warunkowy4 lewa || cor suma logiczna warunkowa3 prawa => implies implikacja logiczna lub

bitowa2 prawa ==> cimplies implikacja warunkowa1 prawa ?: wyrażenie warunkowe0 prawa = ^= *= /= %= += -=

<<= >>= &= &&= |= ||=#=

przypisania

Ponadto operatory prefiksowe mają zawsze większy priorytet niż binarne.Operatory szybko kończące

Operatory logicznej sumy i alternatywy mają ciekawą własność, żew niektórych sytuacjach wystarczy obliczenie wartości lewego operandu, aby jużwiedzieć, jaka będzie wartość całego wyrażenia. Konkretnie, jeśli lewy operandiloczynu ma wartość false, to wiadomo, że wartością iloczynu będzie false. Dla sumylogicznej, jeżeli lewy operand ma wartość true, to wartością całości także będzietrue. Może mieć to istotne znaczenie dla wydajności, ale zbytnie uproszczenia mogąprowadzić do utraty ekspresywności. Obliczenie nadmiarowego wyrażenia możebowiem być istotne ze względu na jego efekty uboczne. W C++ przyjęto, żeoperatory iloczynu logicznego i sumy logicznej są zawsze „szybkie”, czyli może niebyć wykonywana ewaluacja prawego operandu. W Harpoonie oba operatorywystępują w dwóch wersjach: „warunkowej” (&&, ||) oraz „dokładnej” (&, |).

Page 68: Tylko książka (pdf)

66 Rozdział 3 Język programowania Harpoon

Przykład:// a, b – dwie baaaardzo duze liczby

// wersja 1if prime?(a) & prime?(b) then

...end// wersja 2 – najczęściej znacznie szybszaif prime?(a) && prime?(b) then

...end

Wywołania procedurWywołanie procedury zapisuje się podając kolejno nazwę procedury (lub

obiektu, którego wartością jest procedura) i obiekt będący listą argumentów.W przypadku procedur bez- i jedno-argumentowych nawiasy nie są potrzebne.

Przykłady:gcd( 5, 6 ) // procedura pobierająca dwa argumenty, czyli krotkę

println(4) // procedura jednoargumentowaprintln 4

Ciekawą właściwością języka Harpoon jest to, że pozwala zapisywaćargumenty w postaci par klucz-wartość (wyrażenia strukturowe), co pozwalazapomnieć o kolejności argumentów i znacząco poprawia czytelność.

Przykład:draw_circle( device = screen, pos = (150, 80), radius = 30 )draw_circle( pos = (150, 80), radius = 30, device = screen )

Wywołania metodWywołanie metody składa się kolejno: z wyrażenia, którego wartością jest

pewien obiekt, znaku kropki, nazwy metody i argumentów. W przypadku metod bez-i jedno-argumentowych nawiasy mogą być pomijane.

Przykładfile.close!file.close!() // nawiasy są opcjonalne

list.append!(5)list.append! 5 // nawiasy są opcjonalne

Wyrażenia krotkowe i słownikoweWartości rekordów i struktur można w kodzie źródłowym wyrazić za pomocą

wyrażeń strukturowych lub krotkowych. Notacja krotkowa polega na ujęciuw nawiasy okrągłe obiektów tworzących rekord lub strukturę w kolejności, w jakiejzdefiniowane są odpowiednie pola w odpowiednim typie rekordowym (standardowanotacja przekazywania argumentów do funkcji w wielu językach). Alternatywnanotacja strukturowa pozwala zapomnieć o kolejności pól i zapisać rekord lubstrukturę za pomocą zbioru par klucz-wartość.

Page 69: Tylko książka (pdf)

3.3 Instrukcje i wyrażenia 67

Przykład z użyciem zagnieżdżonych konstruktorów rekordów:// notacja słownikowavar e : Expr =

BinaryOperExpr (lhs_arg = „a”rhs_arg =

BinaryOperExpr (lhs_arg = „b”rhs_arg = „c”ident = „+”

)ident = „*”

)

// notacja krotkowavar e : Expr =

BinaryOperExpr(„a”, BinaryOperExpr(„b”, „c”, „+”), „*”)Wyrażenia listowe

Wyrażenia listowe pozwalają wyrazić w kodzie źródłowym wartości obiektówmających interfejs List. Stałe, którym odpowiadają te wyrażenia są obiekty klasyArray, która implementuje interfejs List.Przykład:

// Stała lista var list : const List = { 1, 2, 3, 4 }// Wektor zainicjowany stałą listąvar test : Vector = GrowableArray { 1, 2, 3, 4 }

Page 70: Tylko książka (pdf)

68 Rozdział 3 Język programowania Harpoon

3.4 Typy danych

WstępSystem typów języka Harpoon w spójny sposób łączy ze sobą typowy system

zorientowany obiektowo, oparty na interfejsach i klasach, z systememzorientowanym na opis danych, opartym na rekordach, listach i uniach. Dzięki temumożliwe jest korzystanie zarówno z polimorfizmu behawioralnego, jaki strukturalnego (patrz: 2.5 Polimorfizm).

Na system ten składa się 8 konstrukcji. Interfejsy są specyfikacjami obiektów,definiują listy operacji, które obiekty udostępniają. Zadaniem klas jest dostarczeniekonkretnych implementacji dla interfejsów. Rekordy są ułatwieniami składniowymsłużącymi do jednoczesnego definiowania interfejsów i klas złożonych tylkoi wyłącznie z właściwości, czyli odpowiadających strukturom z języka C (ułatwienieto jest o tyle istotne, że rekordy wraz z listami są podstawowymi konstrukcjamijęzyka opisu danych). Struktury są nieoznaczonymi rekordami, a krotki strukturamio anonimowych polach.

Wyliczenie jest typem zdefiniowanym przez użytkownika przez podaniewszystkich obiektów, które są tego typu (elementami wyliczeń najczęściej są tzw.obiekty symboliczne). Dla zmiennych, które mogą mieć wartości różnych typówprzewidziane są unie – definiują one listy interfejsów, spośród których przynajmniejjeden jest implementowany. Konstrukcją komplementarną względem unii sąprzecięcia, które odpowiadają obiektom implementującym jednocześnie kilkainterfejsów.

Typy podstawoweTypami podstawowymi są:

Nazwa podstawowa Nazwa przyjazna Odpowiednik z C++Int8 Char charNat8 Byte, AChar unsigned charInt16 ShortInt shortNat16 ShortNat, WChar unsigned shortInt32 Int intNat32 Nat unsigned intInt64 LongInt long longNat64 LongNat unsigned long longReal32 ShortReal floatReal64 Real doubleReal80 LongReal long doubleBool Bool boolNat1 Bit N/ANull Null N/A

Zakresy wartości poszczególnych typów odpowiadają zakresom ichodpowiedników z języka C++.

Page 71: Tylko książka (pdf)

3.4 Typy danych 69

InterfejsyInterfejs jest specyfikacją obiektów. Deklaruje listę metod, ich sygnatury,

a także, mniej lub bardziej formalnie, ich warunki początkowe i końcowe. Z punktuwidzenia kompilatora istotne jest, że z definicji interfejsu wynika struktura tablicymetod wirtualnych, której znajomość potrzebna jest na etapie generowania kodu.

Definicja interfejsu składa się kolejno: ze słowa kluczowego interface, nazwy,listy parametrów, opcjonalnej listy nad-interfejsów (dziedziczenie interfejsów)poprzedzonej słowem kluczowym subtypes i listy elementów interfejsu umieszczonejmiędzy słowami kluczowymi has oraz end.

Elementami interfejsów mogą być następujące deklaracje:

• metoda (ang. method),

• getter, setter-- metody specjalne o składni przypisania (dla właściwości),

• właściwość (ang. property)-- jednoczesna deklaracja gettera i settera o tej samej nazwie.

Deklaracja metody może zawierać wyrażenie (po znaku równości), którebędzie spełniało funkcję warunku końcowego lub nawet implementacji przy brakuodpowiedniej implementacji w klasie dziedziczącej dany interfejs. Nie koliduje toz zasadą oddzielenie interfejsów od implementacji, ponieważ podane wyrażenie macharakter czysto deklaratywny.

Elementów interfejsów bazowych (dziedziczonych) nie trzeba powtarzaćw definicji interfejsu pochodnego (dziedziczącego), ale można i istnienie przy tymmożliwość zawężenia typów (zamienienia na typ pochodny) argumentów i/lub typuwartości zwracanej.

Oto przykładowe definicje interfejsów:interface Circle subtypes Shape, Resizablehas

property x : Intproperty y : Intproperty radius : Intmethod diameter : Int = 2 * radiusmethod area : Real = pi * radius ^ 2.0method resize!( new_size : Real )

endinterface Set[ T : Type ] subtypes Collection[T]has method content_type : Type = T method contains?( obj : ref T ) : Bool method insert!( obj : T ) method remove!( obj : T )end

Page 72: Tylko książka (pdf)

70 Rozdział 3 Język programowania Harpoon

Metody specjalneOprócz metod normalnych istnieją także metody specjalne odpowiedzialne za

realizacje operacji, dla których istnieją ułatwienia składniowe, takich jak na przykładoperatory binarne. Metody te przedstawione zostały w poniższej tabeli.

deklaracja znaczenie składniamethod contains?( arg:T ) : Bool interfejs zbiorów i typów x in obj

getter apply( ... ) : ... interfejs funkcji, tablic, map y = obj(x)

setter apply( ... ) : ... interfejs tablic, map obj(x) = y

method apply( ... ) interfejs procedur obj(x)

method content : T interfejs pośrednika (proxy) [obj]

method clone : T klonowanie x <- obj

method eq? : Bool porównywanie obj == x

method ne? : Bool porównywanie obj != x

method lt?( rhs:T ) : Bool operator < obj < x

method gt?( rhs:T ) : Bool operator > obj > x

method le?( rhs:T ) : Bool operator <= obj <= x

method ge?( rhs:T ) : Bool operator >= obj >= x

method plus( rhs:T ) : V operator binarny + obj + x

method minus( rhs:T ) : V operator binarny - obj - x

method times( rhs:T ) : V operator binarny * obj * x

method divide( rhs:T ) : V operator binarny / obj / x

method modulo( rhs:T ) : V operator binarny % obj % x

method power( rhs:T ) : V operator binarny ^ obj ^ x

method neg() : V operator unarny - -obj

method and( rhs:T ) : V operator binarny and obj & x

method or( rhs:T ) : V operator binarny or obj | x

method xor( rhs:T ) : V operator binarny xor obj # x

method not : V operator unarny not !obj

method implies( rhs:T ) : V operator implikacji obj ==> x

method shl( rhs:T ) : V operator binarny >> obj << x

method shr( rhs:T ) : V operator binarny << obj >> x

method init!( ... ) metoda inicjująca -

method finalize!() destruktor -

Uwagi:

• Wszystkie wymienione metody, poza dwoma ostatnimi, nie mogąmodyfikować swoich obiektów ani argumentów.

• Operatory muszą spełniać warunki wynikające z ich matematycznychinterpretacji. Np. (a >= b) == !(a < b).

Page 73: Tylko książka (pdf)

3.4 Typy danych 71

• Operatory == oraz != służą do głębokiego porównywania obiektów. Doporównywania wskaźników przeznaczone są operatory === oraz !==9.

• Odpowiedni podprogram dla operatora binarnego jest wybierana w czasiedziałania na podstawie typu lewego i tylko lewego argumentu.

KlasyKlasa jest typem konkretnym, co oznacza, że na podstawie jej definicji

możliwe jest tworzenie konkretnych obiektów (instancji klasy), czego nie możnapowiedzieć o interfejsach. Klasy w języku Harpoon mogą dziedziczyć dowolnąliczbę interfejsów, ale nie mogą dziedziczyć implementacji.

Elementy interfejsów mogą być w klasach implementowane na trzy sposoby:

1. Przez składowanie-- implementacja bezparametrowych właściwości polegająca nazarezerwowaniu dla nich przestrzeni w pamięci.

2. Przez obliczanie-- implementacja za pomocą podprogramu pisanego przez programistę lubgenerowanego automatycznie na podstawie wyrażenia.

3. Przez delegowanie-- przekazanie komunikatu do pewnego podobiektu.

To, jaki sposób implementacji zostanie wybrany deklaruje się w tzw. definicjiklasy, która składa się kolejno: ze słowa kluczowego class, nazwy, listy parametrów,opcjonalnej listy implementowanych interfejsów poprzedzonej słowem kluczowymimplements i listy elementów definicji klasy umieszczonej między słowamikluczowymi has oraz end.

Elementami definicji klasy mogą być deklaracje:

• zmiennych (ang. variable)-- oznaczające implementacje przez składowania.

• metod, getterów, setterów-- zapowiadające implementacje proceduralne lub delegacje.

Implementacje przez składowanieDeklaracja zmiennej wewnątrz klasy stanowi implementację przez składowanie

getterów i setterów o tej samej nazwie.

Przykład:interface IVector has

method size : Int // interfejs...

endclass CVector implements IVector has

var size : Int // implementacja...

end

9 Znaczenie operatorów przypisania i porównywania można mnemotechnicznie rozumieć tak, że im dłuższyoperator tym „silniejsza” równość: „=” oznacza równość po operacji, „==” zarówno przed, jak i po, natomiast„===” oznacza równość nawet po zmianie wartość jednej ze stron.

Page 74: Tylko książka (pdf)

72 Rozdział 3 Język programowania Harpoon

Implementacje proceduralneImplementacje proceduralne mogą być zdefiniowane poprzez:

• zadeklarowanie i napisanie odpowiedniego podprogramu,

• dopisanie wyrażenia po deklaracji metody,

• oddelegowanie komunikatu do pewnego obiektu.

Przykłady:// implementacje za pomocą wyrażeńmethod age : Date = Date::current_date - date_of_birthmethod output : List = core.output// implementacja za pomocą podprogramuclass CPoint has

...method move!( dx : Int, dy : Int )

endmethod CPoint::move!( dx : Int, dy : Int ) is

x = x + dxy = y + dy

end // implementacja za pomocą delegacjiclass CDialog implements IWindow has

var wnd : CWindowmethod close() --> wnd // delegacja

end

Deklaracje klasZadaniem deklaracji klasy jest dostarczenie informacji o istnieniu pewnej

implementacji i wskazaniu jej nazwy oraz listy implementowanych interfejsów.

Deklaracji klasy składa się kolejno: ze słowa kluczowego class, nazwy klasy,opcjonalnej listy parametrów i opcjonalnej listy implementowanych interfejsówpoprzedzonej słowem kluczowym implements.

Przykłady deklaracji klas:class Point implements Objectclass GrowableArray[T:Type] implements Vector[T]

Uwagi:

• Definicja klasy jest jednocześnie definicją interfejsu o tej samej nazwie.

• Możliwe jest pomijanie w definicji klasy tych elementów (typów, prawwłasności), które wynikają z implementowanych interfejsów.

Metody inicjująceW powszechnym zastosowaniu są specjalne elementy klas zwane

konstruktorami. Ale nie w Harpoonie. Konstruktor, jako element nie podlegającydziedziczeniu, musi być zadeklarowany w każdej klasie. W Harpoonie jednakżezakłada się, że widoczne na zewnątrz modułu powinny być jedynie publiczneinterfejsy i deklaracje klas (ograniczone do nazwy, listy parametrów i listyimplementowanych interfejsów), a tymczasem sygnatury konstruktorów muszą byćznane klientowi, aby mógł on tworzyć obiekty.

Page 75: Tylko książka (pdf)

3.4 Typy danych 73

Stąd, w języku Harpoon zamiast konstruktorów stosuje się metody inicjujące(inicjatory, o nazwie init!), które mogą być dziedziczone (mogą być elementamiinterfejsów). Dodatkową zaletą tego podejścia jest to, że metodę taką możnawywołać także na nienowym obiekcie w celu „zresetowania” jego stanu albo w celuszybkiego recyklingu pamięci po nieużywanym obiekcie.

Metoda init! jest jedyną, na początku której mogą nie być spełnione warunkiklasy, na przykład niezerowalne wskaźniki mogą mieć wartość null. Niemniej jednakna wyjściu wszystkie warunki muszą być spełnione.

Funkcję przeciwną do metod inicjujących pełni metoda finalize!, którejzadaniem jest zwolnienie zasobów, ale nie należy do jej obowiązków zwolnieniepamięci podobiektów własnych, ponieważ to wykonywane jest automatycznie.Składnia tworzenia obiektu

Obiekt tworzy się wywołując pseudofunkcję o nazwie identycznej z nazwąklasy, której obiekt ma być utworzony. Jako listę parametrów przekazuje się listęparametrów dla inicjatora.

Przykład:var x : Vector = GrowableArray( {1, 2, 3} )

Proces tworzenia obiektuDziałanie konstruktora (niejawnej funkcji tworzącej obiekt) jest następujące:

1. Przydzielana jest pamięć dla obiektu.

2. Przydzielana jest pamięć dla podobiektów własnych, których typ zabranianadawania im wartości null.

3. Dla obiektów skonstruowanych w punkcie 2 wywoływane są ich domyślneinicjatory (bezparametrowe), chyba, że odpowiednie wywołania zostałyzapisane przez programistę i jest pewność, że zostaną wykonane.

4. Pozostałym podobiektom przydzielana jest wartość null.5. Wywoływana jest odpowiednia metoda init! dla tworzonego obiektu.

Właściwości, rekordy, struktury i krotkiWłaściwości

W języku C stosuje się struktury, które są prostym złączeniem kilkuzmiennych, natomiast w języku C++ właściwie zabroniono ich szeroko stosować,gdyż uznano je za niekompatybilne z ideą oddzielenia interfejsu od implementacji.W języku Harpoon można stosować zarówno proste pola publiczne (właściwości),jak i metody, i w obu przypadkach możliwe jest oddzielenie interfejsu odimplementacji. Co więcej, właściwości mogą być sparametryzowane dokładnie taksamo jak metody oraz możliwe jest traktowanie całego obiektu jako jednej dużejwłaściwości sparametryzowanej (właściwość apply, odpowiednik indekserów z C#).Rekordy

Istnieje specjalna konstrukcja (ułatwienie składniowe) służąca do deklaracjirekordów, które są tożsame zwykłym klasom i odpowiadającym im interfejsom, aleskładają się jedynie z właściwości prostych implementowanych przez składowaniedanych (w przeciwieństwie do obliczania).

Page 76: Tylko książka (pdf)

74 Rozdział 3 Język programowania Harpoon

Definicja rekordu jest podobna do definicji klasy, z tę różnicą, że zamiastsłowa class występuje słowo record i jedynym możliwym rodzajem elementów sązmienne (przy czym słowo kluczowe var może być pomijane). Rekordy mogąimplementować interfejsy rekordowe tak samo, jak klasy mogą implementowaćinterfejsy dowolne.

Dla każdego rekordu generowana jest automatycznie metoda inicjująca o liścieargumentów odpowiadającej liście pól rekordu.

Przykłady:record Point has

x : Inty : Int

endvar p : Point = Point(5, 8)record BinaryOperExpr has

lhs_arg : Exprrhs_arg : Exprident : String

end

StrukturyPodobnie jak struktury w języku C, rekordy w Harpoonie są typami

oznaczonymi. Oznacza to, że dwa rekordy zdefiniowane tak samo, ale różniące sięnazwami są niezależnymi typami; nie są możliwe przypisania pomiędzy obiektamidwóch takich typów.

Do definiowania odpowiednich typów nieoznaczonych służą struktury.Definiuje się je tak samo jak rekordy albo za pomocą wyrażeń wyliczających nazwyi typy pól pomiędzy nawiasami:

Przykład:// Definicja typu nazwanego, ale nieoznaczonego.struct Point has

x : Inty : Int

end

lub za pomocą wyrażeń:// Wyrażenie określające typ anonimowy, nieoznaczony,// równoważny powyższemu.( x : Int, y : Int )

// To samo krócej( x, y : Int )

// Definicja typu na podstawie wyrażeniatypedef Point = ( x, y : Int )

Struktury nie opierają się jednak na polimorfizmie behawioralnym, niekorzystają z tablic metod wirtualnych. Obiekty im odpowiadające są prostymizłączeniami danych we wspólnych blokach pamięci, w kolejności takiej, w jakiejokreślono ich elementy. Implementacje operacji związanych ze strukturami(przypisanie, dostęp do pól) są zawsze określane statycznie (w czasie kompilacji).Typy strukturowe nie uczestniczą w dziedziczeniu, aczkolwiek automatycznakonwersja między strukturami zgodnymi w sensie nazw pól i ich typów jest możliwa.

Page 77: Tylko książka (pdf)

3.4 Typy danych 75

Podstawowym zastosowanie typy strukturowe znajdują w deklaracjach listargumentów podprogramów – to co w innych językach jest listą deklaracjiargumentów, w Harpoonie jest definicją typu strukturowego:

routine gcd( a, b : Int ) : Introutine Canvas::draw_circle!( x, y : Int, radius : Int )

Bardzo ważną własnością struktur jest to, że istnieją odpowiadające imwyrażenia – tak jak istnieją wyrażenia dla liczb i jak nie istnieją wyrażenia dla klas –opisane jest to w punkcie Wyrażenia krotkowe i słownikowe.Krotki

Krotki odpowiadają strukturom o anonimowych polach (mogą być takżetraktowane jako listy o stałej długości i typach elementów ustalonych osobno dlakażdej pozycji) – są prostymi złączeniami danych, nie korzystają z metodwirtualnych, nie uczestniczą w dziedziczeniu.

Głównym celem ich istnienia jest zapewnienie zwięzłości składni – szczególniew wywołaniach podprogramów, gdzie często zastępują wyrażenia strukturowe. Listaargumentów wywołania podprogramu w notacji takiej, jak w większości innychjęzyków, jest traktowana jako krotka, np.:

gcd( a = 55, b = 121 ) // notacja strukturowa wywołaniagcd( 55, 121 ) // notacja krotkowa wywołania

Krotki są automatycznie konwertowane na odpowiadające im struktury wedługkolejności występowania odpowiednich elementów – zadanie to jest o tyle proste, żena najniższym poziomie – w pamięci fizycznej – krotki w niczym nie różnią się ododpowiednich struktur.

Typy krotkowe można definiować za pomocą dwóch rodzajów wyrażeń:(Move, Int) // typ krotkowy

Move * Int // ten sam typ krotkowy

Można także definiować nazwane typy krotkowe:// Typ określający możliwy do wykonania ruch wraz// z jego oceną w pewnym programie szachowym.tuple MoveCandidate has

MoveInt

endtypedef MoveCandidate = (Move, Int)

Wyrażenia krotkowe są wykorzystywane do realizacji złożonej instrukcjiprzypisania:

(a, b) = (b, a) // to samo co: swap(a, b)

UnieParadygmat obiektowy stara się zastępować złożone instrukcje if-else oraz

switch wywołaniami metod wirtualnych. Nie zawsze jednak ten mechanizm działa –problemy pojawiają się na przykład wtedy, gdy potrzeba dokonać dynamicznegowyboru metody na podstawie typu argumentu (a nie typu obiektu this). Problem tennajczęściej pojawia się podczas tworzenia algorytmów sterowanych danymi.Niektóre języki stosują w tym celu złożony koncepcyjnie mechanizm multiple-

Page 78: Tylko książka (pdf)

76 Rozdział 3 Język programowania Harpoon

dispatching, czyli biorą pod uwagę także typ argumentów przy wyborze funkcjiwirtualnych, inne stosują wzorzec projektowy „Visitor”. Język Harpoon stawia naprostotę i umożliwia stosowanie klasycznej metody, często dziś uważanej zaniepoprawną, którą jest stosowanie unii i rozstrzygania przypadku za pomocąinstrukcji switch.

Istnieje zasadnicza różnica w używaniu unii w języku Harpoon w porównaniuz językiem C. W Harpoonie unie są częścią obiektowego modelu danych. Na niskimpoziomie zamiast wspólnej pamięci o rozmiarze odpowiadającym największemuwariantowi, unie są równoważne wskaźnikom, a do identyfikowania wariantówużywają informacji RTTI (ang. Run Time Type Identification). Odpowiadają więcbardziej typom przeznaczonym do rzutowania w dół, niż uniom z języka C.

Połączenie unii z paradygmatem obiektowym spowodowało jegouprecyzyjnienie. Możliwe jest wyraźnie oddzielenie rzadkich przypadków obiektów,dla których dopuszcza się rzutowanie w dół (unie typu i wszystkich jego podtypów),od tych, których interfejs powinien być wystarczający. Ponadto za pomocą uniiz typem Null określa się czy wskaźnik może być pusty (wartość null). Składnia

Definicja unii składa się kolejno: ze słowa kluczowego union, nazwy unii,słowa kluczowego has oraz listy nazw oznaczonych interfejsów, klas i rekordówzakończonej słowem kluczowym end.

Przykład:union Event has

KeyboardEventMouseEventSystemEvent

end

Elementami unii nie mogą być nieoznaczone struktury, krotki i inne unie,ponieważ brak jest dla tych typów możliwości stosowania RTTI.Unia z typem Null

W Harpoonie można określić czy wartość pewnego wskaźnika możeprzyjmować wartość null, co nie jest możliwe w innych popularnych językach.Mechanizm jest prosty – typ zwykły T oznacza brak takiej możliwości; dopiero uniaz typem Null zapisywana union { T, Null } lub prościej T* pozwala na zerowaniewskaźnika.Unia wszystkich podtypów

We wcześniejszych językach obiektowych istnieje pewna dwuznacznośćokreślenia, że pewna zmienna x jest pewnego typu T. Może to oznaczać, że (1) dokorzystania z obiektu x wystarczy interfejs T lub, że (2) interfejs obiektu x jestinterfejsem T lub pochodnym interfejsu T. Ta subtelna różnica znaczeń implikujeważną możliwość rzutowania w dół. Dotychczas zwykło się zakładać, że rzutowaniew dół jest zawsze możliwe i prawie zawsze niewskazane. W Harpoonie te dwiesytuacje są wyraźnie oddzielone. Jeżeli zmienna jest typu T, to znaczy, że interfejs T(lub wyższy) musi wystarczyć do korzystania z niej. Aby zaznaczyć, że zmienna ma„co najmniej interfejs T” korzystamy z unii wszystkich podtypów zapisywanejprosto jako T+ i wtedy możliwe jest rzutowanie w dół. Gdy rozważany typ dotyczyargumentu podprogramu, różnica między typem T a typem T+ jest taka, żew pierwszym przypadku odpowiednie rzutowanie jest wykonywane przed wejściem

Page 79: Tylko książka (pdf)

3.4 Typy danych 77

do podprogramu, a w drugim w jego wnętrzu podczas korzystania z tego argumentu.

Przykłady:// musi wystarczyć ten interfejsmethod Bitmap::blit( src : ref Bitmap )

// dopuszczalne jest późniejsze rzutowanie w dółmethod Set::insert( arg : Object+ )

var a : Object // uchwyt o interfejsie Objectvar b : Object* // uchwyt o interfejsie Object lub wartość nullvar c : Object+ // uchwyt o interfejsie Object lub pochodnymvar d : Object+* // kombinacja dwóch powyższychvar e : A | B // dodatkowa notacja dla unii

Uwaga:

Zmienne pewnego typu T+ można rzutować w dół, ale jest to zabronione dlainterfejsu T. Niestety zakaz ten można dość łatwo obejść, rzutując najpierw z Tna T+, a następnie dowolnie w dół.

Problem ten można rozwiązać wymagając by unie zawierały, oprócz wskaźnikana obiekt, także identyfikator interfejsu, przez który dostępny był obiekt przedumieszczeniem go w unii. W ten sposób próba obejścia zakazu rzutowania wdół będzie wykryta podczas działania programu.

Typ AnyTyp specjalny Any jest unią wszystkich typów, czyli tym samym co Object+.

PrzecięciaSkoro istnieją unie typów, które odpowiadają sumom zbiorów, to naturalne

wydaje się istnienie także przecięć typów, które odpowiadają częściom wspólnymzbiorów.

Choć przecięcia nie są powszechnie stosowaną koncepcją, to jednak mogą byćbardzo przydatne w sytuacji, gdy programista ściśle przestrzega zasady tworzeniaatomowych interfejsów. Może się wtedy zdarzyć, że w deklaracji argumentupodprogramu będzie potrzeba określenia, że przekazywany obiekt musiimplementować jednocześnie dwa lub więcej interfejsów. Można oczywiście takiinterfejs wcześniej utworzyć za pomocą dziedziczenia i nazwać, ale w prostychprzypadkach lepsze może być po prostu wyliczenie odpowiednich interfejsówbezpośrednio tam, gdzie są stosowane.

np.:var x : Map[Nat,T] & List[T] // interfejs tablicy

var x : Set[Int] & List[Int] // coś jak SortedSet

Typy wyliczeniowe i obiekty symboliczneTyp wyliczeniowy jest typem tych, i tylko tych obiektów, które są wyliczone w

jego definicji. Elementami wyliczenia mogą być dowolne obiekty stałe, alenajczęściej stosuje się tzw. obiekty symboliczne, będące obiektami o nieznanejstrukturze, o których wiadomo tylko tyle, że są. Dzięki temu można w wieluwyliczeniach stosować te same elementy i nie powoduje to konfliktów nazw.

Typy wyliczeniowe są, podobnie jak struktury, krotki i unie, typaminieoznaczonymi.

Page 80: Tylko książka (pdf)

78 Rozdział 3 Język programowania Harpoon

Obiekty symboliczne zapisuje się za pomocą tzw. literałów symbolicznych,które są podobne do identyfikatorów, ale zaczynają się od znaku „`”.

Przykład:enum NormalizationMethod has

`depth_map`bitmap`furrows

endTo samo można zapisać inaczej:

enum Method { `depth_map, `bitmap, `furrows }enum Method = enum { `depth_map, `bitmap, `furrows }

Obiekty symboliczne różnią się od napisów tym, że:

• są niemodyfikowalne,

• implementowane są za pomocą typu liczb całkowitych,

• dwa jednakowo zapisane są zawsze równoważne,

• implementują metodę to_string, co jest przydatne podczas debuggowania.

Od stałych liczbowych odróżniają się m.in. tym, że nie muszą być deklarowane i jakoliterały nie są objęte zasadami przestrzeni nazw.

Inny przykład zastosowania:method Image::blur( radius : Nat, _method : enum{ `rle, `iir } )

Typ typówW języku Harpoon typy mogą być wartościami – tj. mogą istnieć wyrażenia,

których wartościami są typy. Typem takich wyrażeń jest typ Type będący typemtypów. Ze względu na wymóg zachowania statycznego określania typów,zastosowanie takich wartości jest ograniczone do wyrażeń stałych i do porównań.

Do dynamicznego określenia typu wyrażenia służy operator type_of.Wyrażenia o typie Type mogą być porównywane za pomocą operatorów

relacyjnych, przy czym pary operatorów („<=”, „>=”) i („<”, „>”) wyznaczają relacjebycia podtypem i bycia podtypem właściwym.

Przykład 1:var x : Int = 2 // deklaracja normalnej zmiennejvar s : Type = Int // zmienna typu Type o wartości Intvar t : Type = type_of(x) // zmienna typu Type inicjowana typem // zmiennej xif s == t then // porównania typów ...end

Przykład 2:// klasa sparametryzowana typemclass Set[ T:Type ]// zmienna typu Set[Int]var s : Set[ Int < Object ? Int : String ]

Interfejs obiektów typów zawiera także metodę specjalną contains? pozwalającąsprawdzić czy pewien obiekt jest danego typu, np.:

Page 81: Tylko książka (pdf)

3.4 Typy danych 79

// wersja 1if Int.contains?(5) then

...end// wersja 2 – ułatwienie składnioweif 5 in Int then

...endNie jest możliwe używanie zmiennych o typie typów w funkcji typu

w deklaracji zmiennej, ponieważ wymagałoby to dynamicznego wiązania typówz obiektami.

var t : Type = String // okvar s : t // niedozwolone!

Typy procedurProcedury (nie będące metodami), podobnie jak typy, są statycznymi

obiektami, które mają swoje typy. Definiuje się je za pomocą składni analogicznej dodeklaracji procedur, ale z pominięciem nazwy, za to z zachowaniem sufiksów ?, !,@. Alternatywnie dostępna jest specjalna klasa Routine (a także klasy: Routine!,Routine?, Routine@ i ich kombinacje) sparametryzowana typem argumentówi wartości zwracanej.

Przykłady:// wersja 1var gcd : routine ( a, b : Int ) : Intvar fclose : routine@( file : File )// wersja 2var gcd : Routine [ ( a, b : Int ), Int ]var fclose : Routine@[ ( file : File ), Null ]// wywołaniax = gcd(15, 21)fclose( f )

RzutowanieRzutowanie stosuje się do zmiany aktualnego interfejsu obiektu. Zazwyczaj

wymaga to wykonania pewnego kodu w czasie działania programu. Możliwe są dwaprzypadki:

• rzutowanie w górę, podczas którego kompilator jest w stanie szybko pobraćodpowiednią v-table poprzez aktualną v-table,

• rzutowanie w dół, podczas którego wymagane jest sięgnięcie do i-table(tablicy interfejsów) obiektu.

Składnia rzutowania jest następująca:// Niech B będzie interfejsem dziedziczącym po A.// Interfejs A zawiera metodę aaa(), interfejs B dodaje metodę bbb().

// Przykład z rzutowaniem w góręvar x : B = ...(x as A).aaa() // rzutowanie w góręx.aaa() // też działa (rzutowanie niejawne)

// Przykład z rzutowaniem w dółvar x : B = ...var y : A = x // niejawne rzutowanie w górę(y as B).bbb() // obowiązkowe jawne rzutowanie w dół

Page 82: Tylko książka (pdf)

80 Rozdział 3 Język programowania Harpoon

Rzutowanie nie może zmienić dyscypliny zmiennej.

Typy i podprogramy generyczneSzczegóły zastosowania typów i podprogramów generycznych w języku

Harpoon nie zostały jeszcze ustalone. W językach Java i C# zagadnienie to zostałouwzględnione dopiero w najnowszych wersjach (odpowiednio 1.5 oraz 2.0), coświadczy o tym, że temat ten nie jest jeszcze dokładnie zbadany. Wiadomo, żeistotnym ulepszeniem w stosunku do wzorców z C++, które niewątpliwie powinnobyć naśladowane, jest niezależna kompilacja konstrukcji generycznych (w C++wzorce były „rozwijane” w miejscu użycia i za każdym razem kompilowane odnowa). Wskazane jest także wprowadzenie ograniczeń dla parametrów określającychtypy oraz automatycznej dedukcji parametrów.

Tzw. „dzikie karty” z Javy nieco podnoszą ekspresywność, ale nierozstrzygnięto jeszcze czy rekompensuje to większe skomplikowanie języka.W szczególności zbadania wymagają alternatywne metody oparte na danychunikatowych (dyscyplina unq) oraz na danych tylko-do-odczytu (atrybut readonly),które, jak się wydaje, mogą stanowić silną konkurencję dla dzikich kartw rozwiązywaniu problemów związanych z zagadnieniami kowariancji typów(problem polegający na tym czy List[Int] jest podtypem List[Object] [4], [20]).

W języku Harpoon planowane jest zunifikowanie pojęć parametrówi argumentów podprogramów. Niech parametrami będą dowolne wartości, a nie tylkotypy. Wtedy można by dopuścić częściową specjalizację podprogramów prowadzącądo otrzymywania podprogramów o zmniejszonej liczbie argumentów.

Przykład:routine add[ T : Type ]( obj : T, list : List[T] ) // parametryzacjaroutine add( T : Type, obj : T, list : List[T] ) // to samo!

add[] // podprogram o trzech argumentachadd[Int] // podprogram o dwóch argumentachadd[Int, 5] // podprogram o jednym argumencie

add[Int]( 5, list ) // wywołanie podprogramu o dwóch argumentachadd[Int, 5]( list ) // wywołanie podprogramu o jednym argumencie

add( 5, list ) // początkowe parametry mogą być wydedukowane! // (jeśli tylko dotyczą typów)

Rozróżnienie pomiędzy parametrami (nawiasy kwadratowe) a argumentami(nawiasy okrągłe) służyłoby wtedy do odróżnienia obiektów stanowiącychspecjalizowane podprogramy od ich wywołań.

routine foo( a : Int )foo(5) // wywołanie funkcji jednoargumentowejfoo[5] // obiekt bezargumentowej funkcji specjalizowanejfoo[5]() // wywołanie

Do specyfikowania ograniczeń dla typów mogłyby służyć wbudowane typygeneryczne Subtype oraz Supertype:

class Set[ T : Subtype[IComparable] ]

Wielokrotne ograniczenia można by wyrażać za pomocą przecięć typów:class Set[ T : Subtype[IComparable], Subtype[ISerializable] ]

Page 83: Tylko książka (pdf)

3.5 Zmienne 81

3.5 ZmienneW języku Harpoon deklaracje zmiennych zawierają oprócz nazw i typów także

specyfikacje tzw. dyscyplin i specyfikacje trybów dostępu.

DeklaracjeDeklaracja zmiennej składa się ze słowa kluczowego var (pomijane m.in.

w nagłówkach podprogramów), listy nazw, znaku dwukropka, a następnie z:

• opcjonalnej specyfikacji dyscypliny (unq, own, ref lub val),• opcjonalnej specyfikacji trybu (const, readonly, mutable),• wyrażenia określającego typ,

• wyrażenia inicjującego.

Tryby dostępu (deprecated!)Istnieją następujące tryby dostępu, jaki zmienna może mieć do swojej wartości:

• mutable (domyślny) oznacza dostęp do obiektów, które możnamodyfikować,

• readonly oznacza dostęp tylko do odczytu do obiektów, które jednak mogąbyć modyfikowane z innego miejsca,

• const oznacza dostęp do obiektów stałych.

Tryb readonly odpowiada trybowi const z języka C++ i jest jedyniemodyfikatorem interfejsu usuwającym z niego wszystkie metody mogące zmieniaćstan obiektu. Tryb const natomiast służy do wyrażenia założenia o absolutnej stałościprzetwarzanego obiektu w czasie trwania danej zmiennej. Próba modyfikacji tegoobiektu w innych podprogramach, współprogramach czy wątkach spowoduje błądczasu wykonania. Stałość, o której mowa, jest stałością głęboką, wyznaczoną przezlogiczną hierarchię danych tak samo, jak wyznaczone jest głębokie klonowaniei porównywanie. Wydajne zaimplementowanie kontroli tej własności może byćjednak zadaniem bardzo trudnym, więc dopuszcza się możliwość weryfikacjiczęściowej, na przykład w postaci stałości płytkiej wspieranej sprawdzaniem stałościsum kontrolnych.Tryby wskaźnika

Powyżej przedstawione tryby dostępu dotyczą dostępu do danych. Ponadtoistnieją dwa tryby wskaźnika – float (domyślny dla mutable i readonly) i final(domyślny dla const), które mogą być łączone z trybami dostępu. Tryb floatingoznacza, że wartość wskaźnika może ulec zmianie, natomiast tryb final oznaczastałość wskaźnika. Głównym zadaniem trybów wskaźników jest ułatwianiekompilatorowi generowania wydajnego kodu.ZestawienieZnaczenie zestawień poszczególnych trybów dostępu i wskaźnika jest następujące:

float mutable -- zmienna referencja do zmiennych danefloat readonly -- zmienna referencja do danych tylko do odczytufloat const -- zmienna referencja do stałych dane

final mutable -- stała referencja do zmiennych danychfinal readonly -- stała referencja do danych tylko do odczytufinal const -- stała referencja do stałych danych

Page 84: Tylko książka (pdf)

82 Rozdział 3 Język programowania Harpoon

DyscyplinyZagadnienie dyscyplin zostało szczegółowo omówione w podrozdziale

2.7 Prawa własności, aliasing i zarządzanie pamięcią, więc tutaj zaprezentowano jeskrótowo.

Dyscyplina określa prawo własności do obiektu oraz możliwość tworzeniadowiązań zewnętrznych zgodnie z poniższą tabelą:

Dyscyplina Znaczenieunq (unique)

Uchwyt z prawem własności do obiektu. Lokalnie możnazałożyć, że do wskazywanego obiektu nie istnieją zewnętrznedowiązania.

own (owned)Uchwyt z prawem własności do obiektu, do którego mogą istniećtrwałe zewnętrzne dowiązania.

ref (referenced)Dowiązanie bez prawa własności (obserwator).

val (value) Prawo specjalne, podobne do unique. Różni się tym, że jedynymmożliwym rodzajem operacji kopiowania DO zmiennej o prawievalue jest klonowanie.

Ponadto dostępny jest specyfikator lent, który użyty dla argumentupodprogramu lub dla zmiennej lokalnej oznacza, że wraz z końcem danegopodprogramu oznaczone dowiązanie zostanie zniszczone i nie będą istnieć żadnejego kopie. Stanowi to więc warunek końcowy, który pozwala przekazywać dopodprogramu dowiązania do obiektów oznaczonych unq z zachowaniem pewności,że warunek braku zewnętrznych dowiązań do nich pozostanie zachowany.

Instrukcja kopiowaniaIstnieją trzy warianty instrukcji przypisania: dowiązywanie, przekazywanie

i klonowanie – szczegóły znajdują się w części teoretyczne, w punkcie: Operatorprzypisania.

Page 85: Tylko książka (pdf)

3.6 Współprogramy 83

3.6 WspółprogramyW niektórych sytuacjach dużo wygodniej jest wyrażać programy w sposób

wielowątkowy. Niestety mechanizmy takie, jak wątki i procesy systemuoperacyjnego wymagają bardzo kosztownej synchronizacji (zarówno pod względemefektywności jak i obciążenia umysłu programisty). Alternatywą jest użyciewspółprogramów (ang. coroutines), które tym różnią się od wątków, że sterowanieprzekazują sobie w sposób jawny (instrukcje yield i continue). Są to obiekty, którezawierają własny stos oraz wskaźnik instrukcji i mogą być uruchamiane orazwznawiane z poziomu innych współprogramów. Więcej informacji teoretycznychznajduje się w podrozdziale 2.8 Współprogramy.

W języku Harpoon współprogramom odpowiadają obiekty klasy Coroutine,której definicja ma następującą postać10:

interface Coroutine[ T : Type ]has

// Konstruktor pobiera procedurę bezargumentową zwracającą Tmethod init!( proc : ref routine!():T )// Wznowienie współprogramumethod continue!() : T

// Sprawdza czy współprogram może być kontynuowanymethod ready? : Bool

end

Obiekt współprogramu tworzy się za pomocą konstruktora, który pobiera jakoargument procedurę stanowiącą dla niego „punkt wejścia”. Po utworzeniuwspółprogram jest w trybie gotowości. Aby przekazać mu sterowanie należywywołać metodę continue!. Później można wywoływać ją jeszcze wielokrotnie, takdługo, jak metoda ready? zwraca wartość true.Komunikacja

Pewnym problemem może być przekazywanie danych między różnymiwspółprogramami. Można zamiast zwykłej procedury w konstruktorze podać obiektproceduralny, który może zawierać wspólne dane. Możliwe jest takżewykorzystywanie wartości zwracanych przez metodę continue!, co jest szczególnieprzydatne do implementowania generatorów.

10 Propozycja wstępna, wymaga jeszcze dopracowania.

Page 86: Tylko książka (pdf)

84 Rozdział 3 Język programowania Harpoon

3.7 Organizacja dużych programów

ModułyModuły służą do organizowania programu analogicznie do konstrukcji

budowanych z klocków Lego™. W języku Harpoon mają one szczególne znaczenie,ze względu na to, że są jednostkami hermetyzacji (patrz: 2.2 Hermetyzacja).

Kody źródłowy dużych programów podzielone są na wiele plikównagłówkowych (*.hh) i implementacyjnych (*.hrp). Pliki nagłówkowe zawierająinformacje niezbędne do współpracy między różnymi modułami. Plikiimplementacyjne natomiast są przeznaczone dla szczegółów, które powinny byćnieznane na zewnątrz.

Jeden plik może zawierać kod tylko jednego modułu, który jest zadeklarowanyw nagłówku za pomocą słowa kluczowego module i odpowiedniej nazwy. Z drugiejstrony implementacja jednego modułu może być rozłożona pomiędzy wiele plikówimplementacyjnych. Plik nagłówkowy powinien być jednak zawsze tylko jeden.

Używanie modułów zewnętrznych deklaruje się w nagłówkach plików zapomocą słowa kluczowego import i odpowiedniej nazwy. Jeżeli nazwy z kilkuimportowanych modułów pokrywają się, to pozostają ukryte i dostęp do nichmożliwy jest po wskazaniu odpowiedniej przestrzeni nazw.

Przykład:// nagłówek pewnego plikumodule Std::Collection::Stackimport Std::Collection::Array

Przestrzenie nazwW języku Harpoon przestrzenie nazw utworzone są przez nazwy modułów

i typów, tj. każdy moduł wprowadza nową przestrzeń o nazwie takiej, jak nazwamodułu; podobnie każdy interfejs i każda klasa wprowadza nową przestrzeńo nazwie będącej złączeniem nazwy modułu i nazwy odpowiedniego typu.

Poszczególne człony przestrzeni nazw oddzielone są podwójnymidwukropkami („::”), a nie kropkami, aby łatwiejsze było dla kompilatora odróżnienieczłonów przestrzeni nazw od nazw obiektów globalnych.

Przykłady:Std::Math.sin(5) -- wywolanie metody obiektu MathStd::Math::sin(5) -- wywolanie funkcji sin z przestrzeni Math

Page 87: Tylko książka (pdf)

4 Realizacja 85

4 Realizacja4.1 Kompilator i interpreter

WstępStworzenie pełnego zestawu narzędzi dla niebanalnego języka programowania

jest zadaniem trudnym i wymagającym dużych ilości czasu. Autorowi niestety nieudało się tego dokonać w całości dla języka Harpoon omówionego w rozdziale 3.przed upływem terminu złożenia niniejszej pracy. W bieżącym rozdziale omówionorealizację znacznie uproszczonego języka Harpoon-- (czytaj: harpun minus minus),który pod względem składni przypomina swój teoretyczny pierwowzór, ale z punktuwidzenia dostarczanych mechanizmów jest od niego bardzo daleki. Pozapodstawowymi elementami, takimi jak typy podstawowe, podprogramy i pętle,z ciekawszych rzeczy zawiera jedynie pilotażowe wersje współprogramów,strukturowych argumentów podprogramów oraz tablic jako klas sparametryzowanychtypem i rozmiarem.

Język Harpoon-- nie posiada konstrukcji zorientowanych na opis danych, którestanowią jedną z zasadniczych cech języka Harpoon, ale za to dostępna jestbiblioteka klas sd3 dla języka C++ realizująca ten sam schemat oparty na listach,rekordach i uniach. Jej zadaniem jest dynamiczne odczytywanie takich danychz plików tekstowych, przetwarzanie ich w postaci obiektów w programach językaC++ i zapisywanie do plików. Bieżąca implementacja różni się w szczegółachskładni od tego, co zaproponowano wcześniej jako część języka Harpoon, jednak sąto różnice mało znaczące. Największą jest to, że jedynym typem podstawowym jestnapis.

Co bardzo ważne, biblioteka sd3 została wielokrotnie pomyślnie wykorzystanapodczas tworzenia kompilatora i maszyny wirtualnej dla języka Harpoon--. Przy jejużyciu zrealizowano konstruowanie drzewa składniowego, jego zapis do plikui późniejszy odczyt w programie generatora kodu. Ponadto w ten sposób opisano listęschematów instrukcji i akcje semantyczne dla maszyny wirtualnej. Napisano programgenerujący kod źródłowy interpretera na podstawie takiego opisu i eksportujący opisprawie 3000 wyspecjalizowanych instrukcji, na podstawie którego później wykonujeswoje zadanie generator kodu. Wszystko w jednym formacie danych. Deklaracjetypów dla węzłów drzewa składniowego zostały opisane za pomocą składnideklaracji typów języka Harpoon (patrz załącznik na końcu pracy). Choć opis tenpozostał nieformalny, to stanowił dla autora bardzo ważną ściągę, która pomagałazachować spójność struktury danych pomiędzy parserem a generatorem kodu. Wewszystkich tych zastosowaniach przyjęty model danych oparty na rekordach, listachi uniach został zastosowany z pełnym powodzeniem, co stanowi praktycznepotwierdzenie jego właściwego zaprojektowania. Również zaproponowana składniajest już dobrze przetestowana i dopracowana (w tym spójna ze składnią językaHarpoon), ponieważ omawiana biblioteka sd3 jest już trzecią wersją przemyślanąi napisaną zupełnie od nowa11.

11 Pierwsza wersja biblioteki była użyta w styczniu 2005 roku do implementacji języka Pasic, który byłprzedmiotem projektu autora na przedmiocie Elementy Translatorów. Wersja druga (sd2) powstała z myślą ojęzyku Harpoon – została zaimplementowana wraz z modułem deklaracji i sprawdzania typów, a także z opcjąeksperymentalnej, bardzo zwięzłej składni opartej na analizie składniowej sterowanej deklaracjami typamów(analogicznie do analizy sterowanej gramatyką formalną). Wersja trzecia (sd3) musiała powstać ponownie odnowa po usunięciu kilku sprzeczności w składni wersji sd2 ze składnią zaproponowanego języka Harpoon.

Page 88: Tylko książka (pdf)

86 Rozdział 4 Realizacja

Biblioteka sd3Aktualna wersja biblioteki sd3 składa się z dwóch modułów: sd3_Data oraz

sd3_DataIO, z których pierwszy zawiera klasy reprezentujące dane w postaci drzewobiektów, a drugi implementuje procedury służące do odczytywania i zapisywaniatych danych z i do plików tekstowych w formacie (prawie) zgodnym ze składniąjęzyka Harpoon.Konstruktory i znaczniki

Przy zachowaniu tej samej składni istnieje jednak jedna istotna różnicaw sposobie interpretacji pewnego szczegółu – to, co w Harpoonie jest wywołaniemkonstruktora pewnego obiektu, z punktu widzenia języka opisu danych jestznacznikiem (tagiem).

Przykład:Point (

x = „14”y = „32”

)

W języku Harpoon powyższy zapis oznaczałby wywołanie konstruktorarekordu Point, którego argumentem jest wyrażenie strukturowe. Z punktu widzeniajęzyka opisu danych jest to po prostu rekord ze znacznikiem Point (znaczniki takiepozwalają odróżnić semantycznie różne opisy, które mają taką samą strukturę – np.rozróżniają opisy pętli while-do oraz repeat-until, z których oba składają się z opisuwarunku i ciała pętli). Taka dualność znaczenia stałej konstrukcji składniowejpozwoliła osiągnąć bardzo ważny cel, jakim było uzyskanie spójności pomiędzyjęzykiem programowania a językiem opisu danych.Klasy danych

Typem bazowym dla wszystkich pozostałych jest klasa Data, która dostarczafunkcjonalności pozwalającej na sprawdzanie rodzaju danych (napis, rekord, lista),sprawdzanie znacznika i rzutowanie. Dziedziczą po niej klasy String (reprezentującadane prymitywne) oraz Record. Istnieje jeszcze klasa implementująca listy, alezostała ona zrealizowana nieco inaczej, z pełnym oddzieleniem interfejsu odimplementacji, tak aby możliwe było posługiwanie się obiektami „ogonów” jakowidokami nad pewną listą o tym samym interfejsie. Szczegóły zawarte są na rysunku4.1.1 na następnej stronie.Funkcje obsługi plików

Do odczytywania i zapisywania opisów danych z i do plików służą funkcjemodułu sd3_DataIO:

Data* deserialize_data( const char* file_name );

void serialize_data ( Data* data, const char* file_name );void serialize_data ( Data* data, FILE* f, int indent = 0 );

Page 89: Tylko książka (pdf)

4.1 Kompilator i interpreter 87

Rysunek 4.1.1. Hierarchia klas biblioteki sd3

Analizator składniowyAnalizator składniowy (parser) jest programem, który tworzy drzewo

składniowe na podstawie kodu źródłowego kompilowanego programu. Drzewoskładniowe jest strukturą danych, która w swoich węzłach zawiera informacjeo poszczególnych elementach programu (deklaracjach, instrukcjach).

Program hparser jest analizatorem składniowym dla języka Harpoon-- (chociażwspiera także większość konstrukcji języka Harpoon, np. klasy sparametryzowane).Oparty jest on na algorytmie zejść rekurencyjnych, ponieważ autor, na podstawiewcześniejszych doświadczeń, uznał to za metodę powodującą mniej kłopotów niżzastosowanie generatorów parserów w rodzaju programu Yacc. Z tych samychpowodów również analiza leksykalna realizowana jest za pomocą ręcznie napisanegokodu.Lekser

Analiza leksykalna realizowana jest za pomocą klas File i Lexerimplementujących odpowiednio odczytywanie i buforowanie znaków orazwydzielanie i buforowanie leksemów (ang. token). Ich definicje są następujące:

StringString( const string& s = „”, const string& tag = „” )

String* clone()const string& as_str()string as_string()int as_int()double as_double()

DataData( Kind kind, const string& tag = „” )

Data* clone()String* as_string()Record* as_record()IList* as_list()Kind kind()const string& tag()

RecordRecord( const string& tag = „” )

Record* clone()

Data* operator[] (const string& id)Data* peek( const string& id )bool has_field( const string& id )void clear()void append( const string& id, Data* value )

IListIList( const string& tag = „” )

IList* clone()Data* head()IList* tail()bool is_empty()int size()

List Tail

Page 90: Tylko książka (pdf)

88 Rozdział 4 Realizacja

class File{ public: void open(const char* file_name); void close();

Terminal peek(int k = 0); Terminal get(); void pop(); Location location();

// sprawdza czy strumien wej. zaczyna sie od wskazanego napisu bool check( const char* str );

private: void feed();

FILE* f; Location loc; deque<Terminal> buf;};

class Lexer{ public: void open( const char* file_name ); void close(); Token peek_token(int k = 0); void pop_token(); void pop_token(string s, bool no_error = false); void pop_token(string s1, string s2, bool no_error = false);

static bool is_ident_char(char c); void skip_eol();

private: Token read_token(); void skip_comment();

private: File file; deque<Token> tokens; char buffer[ BUF_SIZE ];};

Struktury Terminal oraz Token reprezentują odpowiednio symbole terminalnei leksemy wraz z informacją o ich lokalizacji w pliku źródłowym (numer wierszai kolumny), co jest później wykorzystywane w komunikatach o błędachskładniowych.

// Lokalizacja terminala: plik, wiersz i kolumnastruct Location{ Location() : file(""), line(0), column(0) {} string file; int line; int column;};

// Symbol terminalny: wartosc i lokalizacjastruct Terminal{ Terminal() {} Terminal( int v, Location l ) : value(v), loc(l) {} int value; Location loc;};

Page 91: Tylko książka (pdf)

4.1 Kompilator i interpreter 89

struct Token{ Token( const string& value, const string& type, Location loc ) : value(value), type(type), loc(loc) {} string value; // actual string read from input string type; // token type - similar to the value // being returned by yylex() Location loc;};

ParserImplementacja rekurencyjnie zstępującego algorytmu analizy składniowej

składa się z kilkudziesięciu funkcji o ogólnej strukturze w postaci:Data* parse_something( Lexer& lexer );

Na przykład:Data* parse_if_stmt ( Lexer& lexer );Data* parse_var_decl ( Lexer& lexer );Data* parse_type_decl( Lexer& lexer );

Wartościami zwracanymi przez te funkcje są opisy poszczególnych elementówprogramu. W celu analizy złożonych konstrukcji wywołują się one wzajemnie i łącząmniejsze opisy w większe, aż na najwyższym poziomie w funkcji parse_moduletworzony jest obiekt będący opisem całego programu. Obiekt ten jest ostatecznieserializowany do pliku, skąd będzie odczytany przez generator kodu, który stanowikolejny etap pracy kompilatora.

Odpowiada temu następujący kod z funkcji main:Lexer lexer;lexer.open( argv[1] );Data* module = parse_module( lexer );lexer.close();printf("Parsing finished.\n\n");

serialize_data( module, argv[2] );

Analiza składniowa wyrażeń arytmetycznychSzczególnego rodzaju analizy składniowej wymagają wyrażenia arytmetyczne

w notacji infiksowej. Klasycznym algorytmem realizacji tego zadania jest algorytmoparty o strukturę stosu. W omawianym programie użyto jednak metody bazującej nazejściach rekurencyjnych analogicznej do tej, którą użyto dla innych konstrukcji, z tąróżnicą, że odpowiednie funkcje pobierają jako drugi argument dodatkowąinformację pozwalającą uwzględnić priorytety i zasady łączności poszczególnychoperatorów.

Na algorytm ten składają się funkcje:Data* parse_expr ( Lexer& lexer, BinOpInfo left_op );Data* parse_expr_more ( Lexer& lexer, Data* lhs, BinOpInfo left_op );

Funkcja parse_expr analizuje wyrażenie składniowe rozszerzając je w prawoza pomocą funkcji parse_expr_more tak długo, jak kolejny operator po prawejwymaga wcześniejszego związania niż operator czekający na związanie po lewej,którego priorytet i łączność określa argument left_op. Argument lhs funkcjiparse_expr_more reprezentuje lewą stronę wyrażenia, które jest poszerzane.

Page 92: Tylko książka (pdf)

90 Rozdział 4 Realizacja

PrzykładDla kodu źródłowego programu „Hello One” (nie ma niestety obsługi napisów):

routine main() is println(1)end

zostaje zbudowane następujące drzewo:Module ( imports = {} items = { RoutineImpl ( decl = RoutineDecl ( arg_t = StructTypeExpr {} initializer = null name = Ident ( namespace = {} value = "main" ) params = null ret_t = null ) impl = CodeBlock { MethodInvocation ( address = IdentAndParams ( ident = Ident ( namespace = Namespace {} value = "apply" ) params = null ) args = TupleExpr { NumericLiteral "1" } object = IdentAndParams ( ident = Ident ( namespace = {} value = "println" ) params = null ) ) } ) } name = Ident ( namespace = Namespace {} value = "default" ))

Niewątpliwie jest ono duże. Wynika to z tego, że każdy element programu jestrozebrany na najmniejsze możliwe składniki, z których wiele przyjmuje wartości nulllub wartości pustych list. Wywołanie funkcji println przekształcane jest nawywołanie metody apply obiektu println, co wynika z obiektowego modelu procedurw języku Harpoon.

Page 93: Tylko książka (pdf)

4.1 Kompilator i interpreter 91

Generator kodu pośredniegoGenerator kodu pośredniego jest programem, który generuje kod w pewnym

niskopoziomowym języku, którego przekształcenie na kod bajtowy powinno być jużproste. Dla języka Harpoon zaprojektowano specjalny kod trójadresowy. Programhcodegen generuje go na podstawie opisu drzewa składniowego programuutworzonego przez program analizatora składniowego (hparser).

Program generatora kodu zawiera, podobnie jak program parsera, długą listępodobnie wyglądających funkcji, w postaci:

void gen_something( FILE* f, CodeContext& ctx, Data* data );

w których:

• argument f oznacza plik, do którego zapisywany jest generowany kod,

• argument ctx zawiera informacje o kontekście generatora (aktualnąwysokość stosu, informacje o zmiennych lokalnych z aktualnego i wyższychbloków kodu, informacje o punktach granicznych zagnieżdżonych pętli dlainstrukcji continue i break),

• argument data (różnie zwany i różnego typu w różnych funkcjach) zawierafragment drzewa składniowego, któremu odpowiada generowany kod.

Przykłady:void gen_body ( FILE* f, CodeContext& ctx, List* stmt );void gen_var_decl ( FILE* f, CodeContext& ctx, Record* stmt );void gen_assignment( FILE* f, CodeContext& ctx, Record* stmt );void gen_while_loop( FILE* f, CodeContext& ctx, Record* stmt );void gen_break_stmt( FILE* f, CodeContext& ctx, Data* stmt );

void gen_expr( FILE* f, CodeContext& ctx, Data* expr, Arg& result );

void gen_method_invocation ( FILE* f, CodeContext& ctx, Record* stmt, Arg& result );

Ostatnie dwa przypadki są przykładami funkcji, które generują, opróczwłaściwego kodu, także informacje o tym, jak należy wykorzystać wartości obliczonew tym kodzie. Struktura Arg zawiera dane o wyprodukowanej wartości – typ, tryb(stała, zmienna na stosie lub stercie) i offset lub wartość. Informacja ta jestwykorzystywana do generowania kodu w funkcjach wyższego poziomu.

Wynik pracy generatora kodu zapisywany jest do plików o rozszerzeniu„hasm”.

Przykład kod pośredniego (odpowiadający przykładowi ze strony 99: Funkcja „print”– przykład użycia tablic):

module default

routine print_x lo? N[88] I[16] I0 jf I[88] @if_next_9741688 conv C[88] I45 put C[88] sub I[88] I0 I[16] mov I[16] I[88] :if_next_9741688:if_end_9741548 mov I[20] I0 :repeat_beg_9836840 mul N[88] N[20] N1 addr N[92] I24

Page 94: Tylko książka (pdf)

92 Rozdział 4 Realizacja

add N[88] N[88] N[92] mod I[92] I[16] I10 conv C[96] I[92] mov C[[88]] C[96] div I[88] I[16] I10 mov I[16] I[88] add I[88] I[20] I1 mov I[20] I[88] eq? N[88] I[16] I0 jf I[88] @repeat_beg_9836840 :repeat_end_9836840 sub I[88] I[20] I1 mov I[20] I[88] :repeat_beg_9942384 mul N[88] N[20] N1 addr N[92] I24 add N[88] N[88] N[92] conv C[92] I48 add C[96] C[[88]] C[92] put C[96] sub I[88] I[20] I1 mov I[20] I[88] lo? N[88] I[20] I0 jf I[88] @repeat_beg_9942384 :repeat_end_9942384 conv C[88] I10 put C[88] ret end

routine main mov I[32] I1234 call @print_x I16 mov I[32] I0 call @print_x I16 sub I[32] I0 I997 call @print_x I16 exit end

Wyjaśnienia:

• Litery C, I, N stanowiące prefiksy argumentów oznaczają typ.

• Liczba bez nawiasów oznacza stałą, w pojedynczym nawiasie kwadratowym– zmienną na stosie, w podwójnym – zmienną na stercie.

• Etykietami są napisy rozpoczynające się dwukropkiem.

• Znaczenie poszczególnych rozkazów można odczytać z opisu maszynywirtualnej przytoczonego na stronach 93-94.

InterpreterInterpreter jest programem uruchamiającym inne programy na tzw. maszynie

wirtualnej. Praca interpretera zbudowanego dla języka Harpoon-- (program hvm)polega na wczytaniu kodu pośredniego wygenerowanego przez program hcodegen,przetłumaczeniu go na kod bajtowy na podstawie opisu dostępnych instrukcjimaszyny wirtualnej (plik instr_set.hrp) i wykonaniu. Generator maszyny wirtualnej

Zanim zostaną omówione szczegóły programu interpretera, przedstawionynajpierw będzie program generatora maszyny wirtualnej (vmc).

Powody istnienia tego programu leżą w problemie wydajności. Lista instrukcjikodu pośredniego musi być mała, ze względu na prostotę koncepcyjną (podejście

Page 95: Tylko książka (pdf)

4.1 Kompilator i interpreter 93

RISC – Reduced Instruction Set Computer). Jednakże, z punktu widzeniainterpretera, każda instrukcja powinna wykonywać jak najwięcej pracy, ponieważistnieje duży narzut czasowy związany z pobieraniem instrukcji i skokiem do kodujej odpowiadającego. Aby spełnić to wymaganie, zbiór instrukcji musi być bardzoduży (podejście CISC – Complex Instruction Set Computer). W interpreterze dlajęzyka Harpoon przyjęto więc rozwiązanie polegającą na tym, że kod pośredni składasię z niewielkiej liczby – ok. 50 – instrukcji, ale maszyna wirtualna ma ich już prawie3000.

I właśnie tutaj zaczyna się zadanie programu generatora maszyny wirtualnej.Jedna instrukcja kodu pośredniego jest traktowana jako schemat wielu różnychinstrukcji maszyny wirtualnej. Osobna instrukcja generowana jest dla każdejkombinacji typów i trybów argumentów (dane natychmiastowe (stałe), pośrednie(stos) i bezpośrednie (sterta)). Dzięki temu oszczędza się wielu niepotrzebnychrozwidleń w kodzie interpretera.

Wynikiem działania generatora maszyny wirtualnej są:

• plik instr_set.hrp ze szczegółowym opisem wszystkich z kilku tysięcyrozkazów oraz

• plik alu.cpp będący kluczowym fragmentem kodu źródłowego interpretera,składającym się z olbrzymiej instrukcji switch, której poszczególneprzypadki odpowiadają poszczególnym rozkazom maszyny wirtualnej.

Natomiast wejściem dla tego programu jest plik zawierający opis listyrozkazów kodu pośredniego lub, z innego punktu widzenia, zawierający opisschematów instrukcji maszyny wirtualnej.

Każdy schemat instrukcji opisany jest za pomocą rekordu, któremu odpowiadanastępująca definicja typu:

record InstrSchema has mnemonic : String versions : String semantics : String end

Mnemonikami są po prostu nazwy instrukcji (mov, mul, jmp). Pole versionsjest napisem, który może zawierać litery oznaczające jakie wersja rozkazu mają byćwygenerowanie:

• Litera U (unsigned) – oznacza, że ten sam rozkaz może być użyty dla liczbcałkowitych zarówno ze znakiem, jak i bez (np. add).

• Litera S (signed) – przeciwnie do U nakazuje wygenerowanie osobnychwersji dla liczb ze znakiem i bez (np. mul).

• Litera R (real) – oznacza rozkazy, które również powinny mieć swoje wersjedla liczb zmiennoprzecinkowych (np. mul, ale nie shl).

Ostatnie pole – semantics – jest napisem będącym schematem koduźródłowego języka C++, w którym odwołaniom do wartości argumentówodpowiadają zapisy @1, @2, @3 (dane do zapisu i odczytu) lub $1, $2, $3 (danetylko do odczytu). Dodatkowo przed cyfrą może wystąpić jedna z liter: c, b, s, w, i, noznaczająca argument od stałym typie we wszystkich wersjach rozkazu. Znak „#”oznacza długość rozkazu w kodzie bajtowym, czyli offset za jakim znajduje siękolejny rozkaz.

Page 96: Tylko książka (pdf)

94 Rozdział 4 Realizacja

Pełen opis maszyny wirtualnej dla języka Harpoon jest następujący (plikvmdescr.hrp):

VM ( instr_set = { // -------- BASIC ----------------------------------------------------------- // mnemonic | versions | semantics InstrSchema ( "nop" "" "ip += #;" ) InstrSchema ( "mov" "UR" "@1 = $2; ip += #;" ) InstrSchema ( "add" "UR" "@1 = $2 + $3; ip += #;" ) InstrSchema ( "sub" "UR" "@1 = $2 - $3; ip += #;" ) InstrSchema ( "mul" "SR" "@1 = $2 * $3; ip += #;" ) InstrSchema ( "div" "SR" "@1 = $2 / $3; ip += #;" ) InstrSchema ( "mod" "S" "@1 = $2 % $3; ip += #;" ) InstrSchema ( "min" "SR" "@1 = min($2, $3); ip += #;" ) InstrSchema ( "max" "SR" "@1 = max($2, $3); ip += #;" ) InstrSchema ( "swap" "UR" "swap(@1, @2); ip += #;" ) InstrSchema ( "sort" "SR" "if (@1 > @2) swap(@1, @2); ip += #;" ) InstrSchema ( "and" "U" "@1 = ($2 && $3); ip += #;" ) InstrSchema ( "or" "U" "@1 = ($2 || $3); ip += #;" ) InstrSchema ( "xor" "U" "@1 = ($2 == $3); ip += #;" ) InstrSchema ( "not" "U" "@1 = !$2; ip += #;" ) InstrSchema ( "nand" "U" "@1 = !($2 && $3); ip += #;" ) InstrSchema ( "nor" "U" "@1 = !($2 || $3); ip += #;" ) InstrSchema ( "shl" "S" "@1 = ($2 << $3); ip += #;" ) InstrSchema ( "shr" "S" "@1 = ($2 >> $3); ip += #;" ) InstrSchema ( "lo?" "SR" "@n1 = ($2 < $3); ip += #;" ) InstrSchema ( "hi?" "SR" "@n1 = ($2 > $3); ip += #;" ) InstrSchema ( "le?" "SR" "@n1 = ($2 <= $3); ip += #;" ) InstrSchema ( "he?" "SR" "@n1 = ($2 >= $3); ip += #;" ) InstrSchema ( "eq?" "SR" "@n1 = ($2 == $3); ip += #;" ) InstrSchema ( "ne?" "SR" "@n1 = ($2 != $3); ip += #;" ) // jump InstrSchema ( "jmp" "" "ip = (byte*)$n1;" ) // conditional jumps (jt - if_true, jf - if_false) InstrSchema ( "jt" "U" "if ($1) ip = (byte*)$n2; else ip += #;" ) InstrSchema ( "jf" "U" "if ($1) ip += #; else ip =(byte*)$n2;" ) InstrSchema ( "fork" "U" "if ($1) ip = (byte*)$n2; else ip =(byte*)$n3;" ) // primitive conversion InstrSchema ( "conv" "SR" "@b1 = (byte )$2; ip += #;" ) InstrSchema ( "conv" "SR" "@c1 = (char )$2; ip += #;" ) InstrSchema ( "conv" "SR" "@w1 = (word )$2; ip += #;" ) InstrSchema ( "conv" "SR" "@s1 = (short )$2; ip += #;" ) InstrSchema ( "conv" "SR" "@n1 = (nat )$2; ip += #;" ) InstrSchema ( "conv" "SR" "@i1 = (int )$2; ip += #;" ) InstrSchema ( "conv" "SR" "@f1 = (float )$2; ip += #;" ) InstrSchema ( "conv" "SR" "@d1 = (double)$2; ip += #;" ) InstrSchema ( "mbp" "" "bp += $i1; ip += #;" ) // -------- MEMORY MANAGEMENT ----------------------------------------------- InstrSchema ( "alloc" "" "@n1 = (nat)malloc($n2); ip += #;" ) InstrSchema ( "free" "" "free((void*)$n1); ip += #;" ) // -------- SUBROUTINES ----------------------------------------------------- // call $new_ip $delta_bp InstrSchema ( "call" "" "tmp32 = (int)bp + $i2; \ *(nat*)(tmp32 + 0) = (nat)(ip + #); \ *(nat*)(tmp32 + 4) = (nat)bp; \ ip = (byte*)$n1; \ bp = (byte*)tmp32;" ) InstrSchema ( "ret" "" "ip = (byte*)*(nat*)(bp + 0); \ bp = (byte*)*(nat*)(bp + 4);" )

Page 97: Tylko książka (pdf)

4.1 Kompilator i interpreter 95

// -------- COROUTINES ------------------------------------------------------ // create coroutine: spawn @cr $proc-ptr InstrSchema ( "spawn" "" "@n1 = (nat)( new Process((byte*)$n2 )); ip +=#;" ) // CONTinue coroutine: cont $cr @ret InstrSchema ( "cont" "U" "@2 = ((Process*)$n1)->cont(); ip += #;" ) // ready $cr @state // Checks state of a coroutine InstrSchema ( "ready" "" "@n2 = ((Process*)$n1)->state(); ip += #;" ) // Aborts coroutine from another coroutine InstrSchema ( "abort" "" "delete (Process*)$n1; ip += #;" ) // Exit current coroutine InstrSchema ( "exit" "" "ip = 0; goto vm_exit;" ) // Exit current coroutine, but do not finish it (it will be continued) InstrSchema ( "yield" "" "ip += #; goto vm_exit;" ) // -------- OTHER ----------------------------------------------------------- // console InstrSchema ( "put" "" "fputc($c1, stdout); ip += #;" ) InstrSchema ( "get" "" "@c1 = fgetc(stdin); ip += #;" ) // console (higher level) InstrSchema ( "print" "" "printf(\"%u\\n\", $n1); ip += #;" ) InstrSchema ( "print" "" "printf(\"%d\\n\", $i1); ip += #;" ) // get indirect address of a variable on the stack InstrSchema ( "addr" "" "@n1 = (nat)(bp + $i2); ip += #;" ) })

Kilka wyjaśnień:

• Rekordy są zapisane w zwięźlejszej notacji krotkowej.

• Zmienna ip jest wskaźnikiem instrukcji.

• Zmienna bp jest wskaźnikiem podstawy stosu.

Warto zwrócić szczególną uwagę na instrukcje dotyczące współprogramów – operujesię w nich na zagnieżdżonych obiektach maszyny wirtualnej (Process).Pętla głównaPętla główna interpretera prezentuje się nadzwyczaj okazale:

while (true) { int opcode = (*(nat*)ip); #include "alu.cpp" }

Page 98: Tylko książka (pdf)

96 Rozdział 4 Realizacja

Plik alu.cpp, który został wygenerowany automatycznie na podstawie opisulisty rozkazów, zawiera następujący kod:

switch (opcode) { case 0: ip += 4; break; case 1: SC(4) = SC(8); ip += 12; break; case 2: HC(4) = SC(8); ip += 12; break; case 3: SC(4) = HC(8); ip += 12; break;... case 63: SS(4) = HS(8) + CS(12); ip += 14; break; case 64: HS(4) = HS(8) + CS(12); ip += 14; break; case 65: SS(4) = CS(8) + CS(10); ip += 12; break;... case 1919: SN(4) = (SS(8) >= SS(12)); ip += 16; break; case 1920: HN(4) = (SS(8) >= SS(12)); ip += 16; break;... case 2424: if (HS(4)) ip = (byte*)CN(8); else ip = (byte*)CN(12); break; case 2425: if (CS(4)) ip = (byte*)CN(6); else ip = (byte*)CN(10); break; case 2426: if (SI(4)) ip = (byte*)SN(8); else ip = (byte*)SN(12); break;... case 2534: HC(4) = (char )HN(8); ip += 12; break; case 2535: SC(4) = (char )CN(8); ip += 12; break;... case 2954: HC(8) = ((Process*)HN(4))->cont(); ip += 12; break; case 2955: HC(8) = ((Process*)CN(4))->cont(); ip += 12; break;... case 2994: SN(4) = (nat)(bp + CI(8)); ip += 12; break; case 2995: HN(4) = (nat)(bp + CI(8)); ip += 12; break;

default: assert(0 && "Unknown opcode");}

natomiast makra z rodzaju SC, HS definiują wyrażenia dostępu do danych o trybiewyznaczonym przez pierwszą literę i typie wyznaczonym przez drugą.

Na przykład: #define CB(a) (*(byte *)(ip + (a))) #define CC(a) (*(char *)(ip + (a))) #define SI(a) (*(int *)(bp + CI(a))) #define HD(a) (*(double*)(SN(a)))

Page 99: Tylko książka (pdf)

4.2 Instrukcja obsługi narzędzi 97

4.2 Instrukcja obsługi narzędziWszystkie przedstawione narzędzia są programami wywoływanymi z linii poleceń.

Składnia wywołania analizatora składniowego:hparser <plik-programu> <plik-drzewa-skladniowego>

Analizator składniowy przetwarza program znajdujący się w pliku-programu nadrzewo składniowe, które zostaje zapisane do pliku-drzewa-składniowego.

Składnia wywołania generatora kodu pośredniego:hcodegen <plik-drzewa-skladniowego> <plik-kodu-posredniego>

Generator kodu pośredniego odczytuje drzewo składniowe z pliku-drzewa-składniowego i generuje kod pośredni do pliku-kodu-pośredniego.

Składnia wywołania interpretera:hvm <plik-kodu-posredniego>

Interpreter odczytuje kod pośredni z pliku-kodu-pośredniego, konwertuje go na kodbajtowy i wykonuje. Efekty działania można obserwować na standardowym wyjściu.

Przygotowany został także skrypt run.bat, którego zadaniem jest uruchomieniewszystkich narzędzi w odpowiedniej kolejności od parsera do interpretera.

Składnia:run <nazwa-pliku-programu-bez-rozszerzenia>

KompilacjaWszystkie wymienione programy są programami konsolowymi używającymi

tylko standardowego języka C++, dzięki czemu nie powinno być problememskompilowanie ich przy pomocy dowolnego kompilatora. Sprawdzone to zostało dlakompilatorów Borland C++ 5.5 i GCC 3.3.1. Kompilować należy tylko główny plikźródłowy (hparser.cpp, hcodegen.cpp, hvm.cpp) ponieważ pozostałe są inkludowane.

Page 100: Tylko książka (pdf)

98 Rozdział 4 Realizacja

4.3 Przykładowe programy

Wyznaczanie największego wspólnego dzielnikaKod źródłowy (plik gcd.hrp):

routine gcd(a, b : Int) : Int isif a == 0 then

return belif b < a then

return gcd(b, a)else

return gcd(b % a, a)end

endroutine main() is println( gcd(28, 42) ) println( gcd(42350, 55385) )end

Wyjście:1455

Wyjaśnienie:

Funkcja gcd oblicza największy wspólny dzielnik za pomocą algorytmuEuklidesa. W procedurze main wywoływana jest dwa razy w ramach przedstawieniawyników jej działania.

Generowanie liczb pierwszychKod źródłowy (primes.hrp):

routine prime?( x : Int ) : Int is if x > 2 and x % 2 == 0 then return 0 else var d : Int = 3 while d < x / 2 do if x % d == 0 then return 0 end d = d + 2 end return 1 endendroutine main() is var x : Int = 1 while x <= 30 do if prime?(x) then println(x) end x = x + 1 endend

Wyjście (bez nowych linii):1 2 3 5 7 11 13 17 19 23 29

Wyjaśnienie:

Przedstawiony program generuje ciąg liczb pierwszych z zakresu od 1 do 30.

Page 101: Tylko książka (pdf)

4.3 Przykładowe programy 99

Funkcja prime? sprawdza czy liczba jest pierwsza. Typem wartości zwracanej jestInt, a nie Bool, z powodu nieobsługiwania tego ostatniego przez aktualną wersjękompilatora. Pętla while w procedurze main pęłni funkcję pętli for, z analogicznychpowodów.

Przykład użycia argumentów strukturowychKod źródłowy (struct_arg.hrp):

routine sub( a, b : Int ) : Int is return a - bendroutine main() is println( sub( 7, 2 ) ) // prints "5" println( sub( a = 7, b = 2 ) ) // prints "5" ! println( sub( b = 2, a = 7 ) ) // prints "5" !!!end

Wyjście:555

Wyjaśnienie:

Funkcja sub zwraca różnicę dwóch liczb. W metodzie main zaprezentowanotrzy równoważne jej wywołania. Argumenty strukturowe pozwalają zapomniećo kolejności argumentów i powodują, że kod staje się bardziej czytelny (chociażw tym prymitywnym przykładzie nazwy argumentów są akurat mało znaczące).

Przykład użycia współprogramówKod źródłowy (cr.hrp):

routine foo() : Int is var i : Int = 0 while i < 10 do yield i i = i + 1 end exit()endroutine main() is var cr1 : Coroutine[Int] = spawn(foo) var cr2 : Coroutine[Int] = spawn(foo)

while 1 do var x : Int = cr1.continue() var y : Int = cr2.continue() if not cr1.ready? then break end println( x ) println( y ) endend

Wyście (bez nowych linii):0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9

Page 102: Tylko książka (pdf)

100 Rozdział 4 Realizacja

Wyjaśnienie:

Procedura foo definiuje proces generujący liczby od 0 do 9. W metodzie maintworzone są dwa obiekty współprogramów, które wykonują ten proces. TypCoroutine[Int] jest klasą współprogramu sparametryzowaną typem Int. Proceduraspawn pełni funkcję konstruktora współprogramu. Jako argument pobiera onaprocedurę stanowiącą punkt wejścia. W pętli while współprogramy uruchamiane sąnaprzemiennie i wyniki ich działania drukowane są na ekranie.

Funkcja „print” – przykład użycia tablicKod źródłowy (print.hrp):

routine print_x( x : Int ) is if x < 0 then put( Char(45) ) // znak minus x = 0 - x end var i : Int = 0 var ar : Array[Char,16] // tablica 16 znaków repeat ar(i) = Char(x % 10) // funkcja Char – konwersja do znaku x = x / 10 i = i + 1 until x == 0 i = i - 1 repeat put( ar(i) + Char(48) ) // wypisanie cyfry i = i - 1 until i < 0 put( Char(10) ) // znak nowej liniiendroutine main() is print_x( 1234 ) print_x( 0 ) print_x( 0 - 997 )end

Wyjście:12340-997

Wyjaśnienie:

Powyższy program demonstruje zastosowanie tablic w podprogramiewypisującym liczbę na ekran, równoważnemu wbudowanej procedurze println. Jakwidać w języku Harpoon tablice są klasami sparametryzowanymi typem i rozmiarem,a ich obiekty używa się za pomocą składni wywołania funkcji. Można nawetpominąć nawiasy! Dziwnie wyglądające wyrażenia w stylu „0 – 997” są substytutemprefiksowego operatora negacji, który jeszcze nie został zaimplementowany.

Page 103: Tylko książka (pdf)

4.4 Testowanie i uruchamianie 101

4.4 Testowanie i uruchamianie

Ogólne zasady testowania i uruchamianiaTestowanie jest procesem mającym na celu wykrycie błędnych przebiegów

wykonania programu, natomiast celem uruchomiania jest lokalizacja i eliminacjaodpowiednich błędów istniejących w programie. Testowanie

Podstawową zasadą testowania jest to, że błędne wykonania programu należywykrywać szybko, ponieważ błędy wykryte wcześniej są łatwiejsze do usunięcia.Dobrą praktyką jest automatyzacja przynajmniej części procesu testowania, ponieważumożliwia częste testowanie bez konieczności zaangażowania programisty.

Porównywanie wyników działania programu po dokonaniu każdej modyfikacji,z wynikami oczekiwanymi i uzyskanymi na odpowiednio dużym zestawie testów,pozwala na zachowanie kontroli nad przebiegiem procesu rozwoju i uruchamianiaprogramu. Testowanie takie nazywane jest regresyjnym.

Poprawność działania złożonych systemów zależy od poprawności działaniaich części składowych, w związku z czym zalecane jest gruntowne testowaniepojedynczych modułów w izolacji od reszty programu przed połączeniami. Możewymagać to przygotowania zaślepek pełniących funkcje innych modułów orazodpowiednich programów symulujących rzeczywiste warunki działania. Istotnew tym procesie jest to, aby testy były wyczerpujące – przypadki testowe powinnyobejmować takie sytuacje, aby każda instrukcja testowanego kodu była wykonanaprzynajmniej raz, a każdy warunek, mogący powodować rozwidlenie, byłprzynajmniej raz spełniony i raz nie. Wskazane jest także przygotowanie testówz danymi o minimalnych, średnich i maksymalnych możliwych rozmiarach, gdyżpozwala to zweryfikować zachowanie się programu w warunkach skrajnych.Uruchamianie

Lokalizacja błędów w programie wymaga od programistów umiejętnościdetektywistycznych. Błędy bowiem podczas rozwoju programu podlegają naturalnejselekcji, która sprzyja przetrwaniu osobników dobrze ukrywających się i przez tonajbardziej złośliwych. W efekcie te, które pozostają niewykryte najdłużej lubiąujawniać się tylko raz na jakiś czas, w trudnych do przewidzenia momentach.Niektóre nawet potrafią współdziałać z innymi błędami w celu wzajemnegoznoszenia swoich objawów.

Pierwszą regułą postępowania programisty w tej walce jest więc takiesterowanie przebiegiem wykonania, aby błędy pojawiały się w sposóbprzewidywalny i za każdym razem. Kolejnym krokiem jest zlokalizowanie fragmentukodu, którego wykonanie powoduje objawienie się błędu. Można w tym celu użyćodpowiedniego programu uruchomieniowego (debuggera) albo zastosować technikęzapisywania „śladu” wykonania programu w pewnym pliku (tzw. logowanie). Trzebabyć jednak przygotowanym na to, że pierwotna przyczyna nieprawidłowego działaniabędzie leżała gdzie indziej. Wykonanie bowiem błędnego kodu najczęściej nie odrazu powoduje widoczny objaw. Zanim więc można będzie przystąpić do czynnościnaprawczych, należy jeszcze przeanalizować przebieg programu wstecz odznalezionego miejsca. Niestety może być to pracochłonne, ponieważ nie można cofaćprogramu krok po kroku. Dodatkowym problemem mogą być problemy objawiającesię lub istniejące w bibliotekach, dla których nie jest dostępny kod źródłowy. Wtedy

Page 104: Tylko książka (pdf)

102 Rozdział 4 Realizacja

konieczna jest analiza metodą czarnej skrzynki, czyli na podstawie zewnętrznychobjawów działania. Przydatne bywa także doświadczenie z zakresu kryptoanalizy.

Przebieg testowania i uruchamianiaBiblioteki sd2 i sd3

Biblioteka sd2 początkowo miała stanowić podstawę implementacji narzędziwchodzących w skład kompilatora języka Harpoon. Jako element używany w wieluróżnych miejscach została przetestowana dokładnie i z pomocą testówautomatycznych dwojakiego rodzaju: dotyczących odczytu danych z plikówi dotyczących weryfikacji danych z deklaracjami ich typów. Same testy zostałyopisane w plikach w formacie sd2(!), takich jak poniższy (testowanie odczytu):

list { test { file = "test1a.sd2" correct = "yes" comment = "Simple record test" }... test { file = "test1c.sd2" correct = "no" comment = "Simple record test, '=' is missing" }... test { file = "test2b.sd2" correct = "yes" comment = "Simple record test, no record keyword, no tag" } test { file = "test2c.sd2" correct = "no" comment = "Simple record test, list keyword" }}

Automatycznie testowanie odczytu danych polegało na próbie odczytaniadanych z plików, których nazwa jest wartością atrybuty file, po czym sprawdzanabyła zgodność wyniku z wartością pola correct. Pole comment było wykorzystywanedo charakteryzowania poszczególnych przypadków testowych.

Niestety biblioteka sd2 musiała zostać zastąpiona biblioteką sd3, którejtestowanie, z powodu ograniczeń czasowych nie było już tak dokładne, ale dziękiwykorzystaniu kodu poprzedniej wersji jako punktu wyjścia nie odnotowano żadnychproblemów związanych z wersją sd3.Kompilator i interpreter

Zestaw narzędzi stanowiących praktyczną realizację języka Harpoon składa sięz trzech osobnych programów: analizatora składniowego (hparser), generatora kodupośredniego (hcodegen) oraz interpretera (hvm). Zostały one przetestowane osobno,a dzięki temu, że (z wyjątkiem interpretera) efekty swojej pracy zapisują one doplików tekstowych weryfikacja ich poprawności nie stanowiła problemu.

Przygotowany został zestaw programów testowych złożony z programówmałych, ale zawierających wszystkie implementowane mechanizmy. Na ichpodstawie sprawdzana była poprawność poszczególnych narzędzi. Analizatorskładniowy generował plik zawierający opis drzewa składniowego, któregopoprawność mogła być zweryfikowana „ręcznie”. Błędy przeoczone objawiały się

Page 105: Tylko książka (pdf)

4.4 Testowanie i uruchamianie 103

szybko w czasie działania generatora kodu, który to drzewo dalej przetwarzał. Efektypracy generatora kodu również były łatwe do sprawdzenia, ponieważ wynikiem byłplik zawierający kod pośredni. Ostatecznym sprawdzianem poprawności całegosystemu była konfrontacja wyników działania przykładowych programóww programie interpretera z wartościami oczekiwanymi.

Wszystkie przykładowe programy wykonują się prawidłowo, aczkolwieknależy zaznaczyć, że obsługa błędów i związane z nią komunikaty dla użytkownikasą niedopracowane.

Wyniki testów szybkościowychBardzo ważną cechą języków programowania jest to czy programy w nich

napisane działają szybko. Na chwilę obecną dla języka Harpoon-- nie istniejekompilator generujący kod maszynowy. Programy na razie wykonywane są namaszynie wirtualnej, więc nie można na tej podstawie rozstrzygać o możliwościachjęzyka. Można za to ocenić efektywność interpretera, choć również w tym przypadkunależy zaznaczyć, że w obecnej wersji nie był on jeszcze poddawany szczególnymzabiegom mającym na celu poprawienie jego wydajności i możliwości usprawnieńjest jeszcze bardzo dużo. Podobnie w testowanej wersji kompilatora nie sąwykonywane żadne optymalizacje.

Szybkość interpretera została sprawdzona dla dwóch programów: generatoraliczb pierwszych i obliczającego funkcję Ackermana. Wyniki porównanoz odpowiednimi programami napisanymi w językach C++ i Python.

Program C++12 Harpoon-- PythonPrimes 1,50 s 8,72 s 31,30 sAckerman 2,86 s 31,30 s 201,66 s

Wyniki te pokazują, że obecne wersje narzędzi dla jezyka Harpoon-- pozwalająw najlepszym przypadku wykonywać programy 5 razy wolniej niż gdyby byłynapisane w C++. Z drugiej strony programy napisane w języku Python są jeszczekilkakrotnie wolniejsze.

12 Kompilowane kompilatorem Borland C++ przy włączonej silnej optymalizacji (opcja -O2)

Page 106: Tylko książka (pdf)

104 Rozdział 5 Uwagi końcowe

5 Uwagi końcoweCelem niniejszej pracy było utworzenie języka programowania ogólnego

przeznaczenia o nowym, wyższym poziomie oferowanych możliwości, przyjednoczesnym zachowaniu względnej prostoty jego konstrukcji. Pod względemwyników teoretycznych cel ten został osiągnięty – uzyskano projekt językaprogramowania, który dzięki połączeniu standardowego paradygmatu obiektowegoz kilkoma mało znanymi rozwiązaniami (współprogramy, kontrola aliasingu)i kilkoma nowymi propozycjami (wsparcie opisu danych, formalne prawa własności),stanowi, w przekonaniu autora, nową jakość w swojej dziedzinie. Ze względu jednakna brak pełnej praktycznej weryfikacji należy przyznać, że istnieje sporeprawdopodobieństwo, że niektóre konstrukcje będą jeszcze musiały zostaćzweryfikowane lub przynajmniej doprecyzowane (w szczególności dotyczy to typówi podprogramów generycznych).

Od strony praktycznej tym co zrealizowano jest zestaw narzędzi dlauproszczonego języka Harpoon--. Implementacja programów analizatoraskładniowego, generatora kodu pośredniego i interpretera była sama w sobiewymagającym zadaniem. Dużym sukcesem zakończyło się zastosowanie na wieluetapach jego realizacji biblioteki sd3, wprowadzającej do języka C++ model danychoparty na listach i rekordach. Stanowi to praktyczne potwierdzenie przydatności tegomodelu, co jest bardzo istotne ze względu na to, że jest on integralną częścią językaHarpoon.

Uzyskane wyniki przekonują, że kierunek badań był wybrany właściwiei dalsze prace powinny być kontynuowane. Autor ma zamiar dalej rozwijaćprzedstawiony język i odpowiednie narzędzia. Pozostało jeszcze dużo do zrobienia –na początek trzeba dokończyć generator kodu pośredniego, aby móc przejść do fazyimplementacji podstawowych bibliotek i prac nad pierwszymi większymiprogramami. W międzyczasie potrzebne zapewne będą drobne cykliczne rewizjeprzyjętych rozwiązań. Ostatecznym celem jest stworzenie języka programowania,którego używać będzie większość programistów na całym świecie – jednego języka,który zastąpi je wszystkie (one language to rule them all).

Page 107: Tylko książka (pdf)

Literatura1. Abelson Harold, Sussman Gerald Jay, Sussman Julie, Struktura i interpretacja

programów komputerowych, Warszawa 2002.

2. Aho Alfred V., Sethi Ravi, Ullman Jeffrey D., Kompilatory. Reguły, metodynarzędzia, Warszawa 2001.

3. Aldrich Jonathan, Kostadinov Valentin, Chambers Craig, Alias Annotations forProgram Understanding, 2002.

4. Bracha Gilad, Generics in the Java Programming Language, 2004.

5. Chailloux Emmanuel, Manoury Pascal, Pagano Bruno, Developing ApplicationsWith Objective Caml, Paris 2000.

6. Clarke Dave, Wrigstad Tobias, External Uniqueness is Unique Enough, 2002.

7. Eckel Bruce, Thinking in C++, 2nd ed, 2000.

8. Eckel Bruce, Thinking in Java. Edycja polska, wyd. 3, Gliwice 2003.

9. Finkel Raphael, Advanced Programming Language Design, Menlo Park 1995.

10.Gamma Erich, Helm Richard, Johnson Ralph, Vlissides John, Design Patterns.Elements of Reusable Object-Oriented Software, 1994.

11.Gosling James, Joy Bill, Steele Guy, Bracha Gilad, The Java LanguageSpecification, wyd. 3, DRAFT.

12.Graham Paul, Hakerzy i Malarze. Wielkie idee ery komputerów, Gliwice 2004.

13.Hunt Andrew, Thomas David, Pragmatyczny programista. Od czeladnika domistrza, Warszawa 2002.

14.Leotiev Yuri, Szafron Duane, On Type Systems for Object-Oriented DatabaseProgramming Languages.

15.Liskov Barbara H., Wing Jeannette M., A Behavioral Notion of Subtyping, 1994.

16.Parnas David L., O modnym powiedzonku – struktura hierarchiczna, [w:]Podstawy oprogramowania. Zbiór artykułów Davida L. Parnasa, pod red.Daniela M. Hoffmana i Davida M. Weissa, Warszawa 2003.

17.Rogers Paul, Encapsulation is not information hiding, [na:]http://www.javaworld.com/javaworld/jw-05-2001/jw-0518-encapsulation.html,wrzesień 2005.

18.Stroustrup Bjarne, Język C++, Warszawa 2000.

19.Stroustrup Bjarne, Projektowanie i rozwój języka C++, Warszawa 1996.

20.Torgersen Mads, Hansen Christian P., Ernst Erik, von der Ah Peter, Bracha Gilad,Gafter Neal, Adding Wildcards to the Java Programming Language, 2004.

21.Boost Library: Smart Pointers, [na:]http://www.boost.org/libs/smart_ptr/smart_ptr.htm, wrzesień 2005 .

22.C# Language Specification, version 0.17b, Microsoft Corporation 2000.

23.C# Version 2.0 Specification, Microsoft Corporation, 2004.

24.The D Programming Language, DMD 0.109, Digital Mars, 2004.

25.Smart Pointers - What, Why, Which?, [na:]http://ootips.org/yonat/4dev/smart-pointers.html, wrzesień 2005.

Page 108: Tylko książka (pdf)

26.Standard Template Library Programmer's Guide, Silicon Graphics ComputerSystems, Inc., Hawlett-Packard Company, 1999.

27.Subtyping, Subclassing, and Trouble with OOP, [na:]http://okmij.org/ftp/Computation/Subtyping/, wrzesień 2005.

28.Transparent Proxy [na:]http://blogs.msdn.com/cbrumme/archive/2003/07/14/51495.aspx, wrzesień 2005.

29.Vault. a programming language for reliable systems, Microsoft Research, [na:]http://research.microsoft.com/vault/, wrzesień 2005.

30.Wikipedia, [na:] http://en.wikipedia.org.

Page 109: Tylko książka (pdf)

ZałącznikiStruktura drzewa składniowego języka Harpoon--

// -------- COMMON DEFINITIONS --------------------------------------------

class Namespace over List[ String ]class CharLiteral over Stringclass StringLiteral over Stringclass NumericLiteral over Stringclass SymbolicLiteral over String

enum Ownership { "val", "unq", "own", "ref" }enum Access { "const", "readonly", "mutable" }

// -------- IDENTIFIERS ----------------------------------------------------

record Ident has value : String namespace : Namespaceend

record IdentAndParams has ident : Ident params : (TupleExpr | StructExpr)*end

// -------- VARIABLES ------------------------------------------------------

record VarDecl has name : String type : TypeExpr* initializer : Expr*end

record VarMultiDecl has names : List[String] type : TypeExpr* initializer : Expr*end

// -------- ROUTINES -------------------------------------------------------

enum RoutineKind has "routine" "query" "mutator" "method" "getter" "setter"end

record RoutineDecl has name : Ident par_t : StructTypeExpr arg_t : StructTypeExpr ret_t : TypeExpr* result : Expr* // postcondition for the returned value kind : RoutineKindend

record RoutineImpl has decl : RoutineDecl body : CodeBlockend

// -------- MODULE ---------------------------------------------------------

record Module has name : Ident imports : List[ Ident ] items : List[ ModuleItem ]end

Page 110: Tylko książka (pdf)

union ModuleItem has RoutineDecl RoutineImpl TypeDecl TypeDef VarMultiDecl // (global data)end

// -------- STATEMENTS -----------------------------------------------------

class CodeBlock over List[ CodeItem ]

union CodeItem has VarMultiDecl Assignment RoutineInvocation MethodInvocation IfStmt WhileLoop RepeatLoop ForLoop ReturnStmt YieldStmt BreakStmt ContinueStmtend

record Assignment has dst : Expr src : Exprend

record RoutineInvocation has address : IdentAndParams args : (StructExpr | TupleExpr)*end

record MethodInvocation has object : Expr address : IdentAndParams args : (StructExpr | TupleExpr)*end

record IfStmt has cases : List[ IfCase ] default : CodeBlock*end

record IfCase has cond : Expr body : CodeBlockend

record WhileLoop has cond : Expr body : CodeBlockend

record RepeatLoop has cond : Expr body : CodeBlockend

record ForLoop has _var : VarDecl range : Expr body : CodeBlock kind : enum { "in", "over" }end

record ReturnStmt has ret : Exprend

Page 111: Tylko książka (pdf)

record YieldStmt has ret : Exprend

class BreakStmt over Nullclass ContinueStmt over Null

// -------- EXPRESSIONS ----------------------------------------------------

union Expr has

Ident // np. nazwa zmiennej IdentAndParams

StringLiteral NumericLiteral SymbolicLiteral

RoutineInvocation MethodInvocation

ListExpr TupleExpr StructExpr

StaticCast DynamicCast

end

class ListExpr over List[Expr]

class TupleExpr over List[Expr]

class StructExpr over List[KeyValue]

record KeyValue has key : String value : Exprend

// 'w : ListBox'record StaticCast has expr : Expr type : TypeExprend

// 'w as ListBox'record DynamicCast has expr : Expr type : TypeExprend

// -------- TYPE EXPRESSION ------------------------------------------------

// Note:// Union-with-Null-expression (A*) is being converted// to UnionExpr by a parser.

// Interface or union of interfaces specificationunion InterfaceExpr has IdentAndParams // Array[ Int, 16 ] / Object UnionTypeExpr // A | B | C AllSubtypesUnionExpr // A+ EnumTypeExpr // enum { `a, `b } StructTypeExpr // (a : A, b : B) TupleTypeExpr // (A, B), A * B RoutineTypeExpr // routine(a:A, b:B), method(a:A, b:B) : Cend

Page 112: Tylko książka (pdf)

// complete contract for a variablerecord TypeExpr has ifc : IntefaceExpr access : Access* ownership : Ownership*end

record AllSubtypesUnionExpr has root : InterfaceExprend

class UnionTypeExpr over List[ IdentAndParams ]

class EnumTypeExpr over List[ Expr ]

class StructTypeExpr over List[ VarMultiDecl ]

class TupleTypeExpr over List[ TypeExpr ]

record RoutineTypeExpr has arg_t : StructTypeExpr ret_t : TypeExpr* kind : RoutineKindend

// -------- TYPE DECLARATIONS ----------------------------------------------

enum TypeKind { "interface", "class", "record", "struct" }

record TypeDecl has name : String params : List[ VarMultiDecl ] interfaces : List[ IdentAndParams ] kind : TypeKindend

record TypeAliasDef has parent : TypeExprend

// e.g.:// class FrameIndex over Int// class A[T:Type] inherits B over C[T]*record StrongTypedef has decl : TypeDecl parent : TypeExprend

// -------- TYPE DEFINITIONS -----------------------------------------------

record InterfaceDef has decl : TypeDecl items : List[ InterfaceItem ]end

record ClassDef has decl : TypeDecl // must be: decl.kind == "class" items : List[ ClassItem ]end

record RecordDef has decl : TypeDecl items : List[ VarDecl ]end

record StructDef has decl : TypeDecl items : List[ VarDecl ]end

record UnionDef has name : String items : List[ IdentAndParams ]end

Page 113: Tylko książka (pdf)

record EnumDef has name : String items : List[ Expr ]end

union InterfaceItem has RoutineDecl VarDeclend

union ClassItem has RoutineDecl VarDecl DelegateDefend

record DelegateDef has routine : RoutineDecl receiver : IdentAndParamsend

record PropertyDecl over VarDecl

// ------------------------------------------------------------------------