142 47 5MB
Polish Pages [256] Year 2007
Jon Bentley
Więcej perełek oprogramowania Wyznania programisty Z angielskiego przełożyła
Jowita Koncewicz-Krzemień
O Autorze: Jon Bentley jest członkiem zespołu technicznego w Computing Sciences Research Center przy Bell Laboratories w Murray Hill, w stanie New Jersey. Zajmuje się programowaniem, opracowywaniem algorytmów, a także projektowaniem narzędzi wspomagających i interfejsów. Brał udział w pracach nad Computer Telephone 8130 i Project instantAnswers, prowadzonych w AT&T. Ma stopień bakałarza (Stanford University 1974 r.), a także magistra i doktora (University of North Carolina 1976 r.). Przed podjęciem pracy w Bell Laboratories piastował stanowisko profesora na Wydziale Informatyki i Matematyki Camegie-Mellon University. Współpracuje z United States Military Academy w West Point, a także z Princeton University. Od 1998 roku pisuje do Dr. Dobb's Journal. Jego stały felieton pt. „Programming Pearls” w Communications of the ACM, na podstawie którego powstała ta książka, a także jej poprzedniczka „Perełki oprogramowania” (WNT 1992, 2001), zdobył mu uznanie wielu czytelników i należał do najpopularniejszych działów tego pisma.
PRZEDMOWA
Programowanie jest zabawne. Czasami bywa wyrafinowaną sztuką. Pole ga również na tworzeniu i używaniu nowych narzędzi oprogramowania. Dotyczy też ludzi, kiedy próbujemy odpowiedzieć na specyficzne pytania: jakie to właściwie zadanie klient chce rozwiązać lub jak można by ułatwić komunikowanie się użytkowników z naszym programem. Programowanie przywiodło mnie do zgłębiania szerokiego wachlarza tematów —od chemii organicznej po kampanię Napoleona. Omawiam tu wszystkie te aspekty programowania, a nawet jeszcze więcej. Książka jest zbiorem esejów. Każdy rozdział można czytać niezależnie, chociaż poszczególne eseje, które były logicznie powiązane ze sobą, zebra łem w odrębnych częściach. Rozdziały 1—4 dotyczą technik działania na programach. W rozdziałach 5-8 omawiam pewne sztuczki programistycz ne; to najmniej techniczna część książki. Projektowaniem wejścia i wyjścia zajmuję się w rozdziałach 9-12. Rozdziały 13—15 zawierają opisy trzech użytecznych podprogramów. Bardziej szczegółowo omawiam te tematy we wprowadzeniach do poszczególnych części. Większość rozdziałów książki jest oparta na felietonach zamieszcza nych w rubryce Perełki oprogramowania w czasopiśmie Communications o f the Association fo r Computing Machinery (w skrócie CACM). Historię tych pu blikacji opisuję we wprowadzeniach do różnych części książki. Dlaczego zadałem sobie trud napisania tej książki, skoro wcześniejsza wersja tych esejów już pojawiła się w druku? Tekst tamtych felietonów w istotny sposób zmienił się od czasu ich pierwszego ukazania się w czasopiśmie. Wprowadziłem do nich dosłownie tysiące niewielkich poprawek: pojawiły się nowe zadania i rozwiązania, usunąłem drobne pomyłki i dołączyłem uwagi wielu czytelników. Niektóre stare punkty usunąłem, żeby uniknąć powtórzeń, a dodałem wiele nowych. Jeden z esejów jest zupełnie nowy.
VIII
PRZEDMOWA
Najważniejszym powodem napisania tej książki była wszakże chęć przedstawienia rozmaitych esejów jako pewnej spójnej całości - pragnąłem ukazać sznur perełek. Moja książka wydana w 1986 roku pt. Perełki opro gramowania,x jest podobnym zbiorem trzynastu esejów zbudowanym wokół głównego tematu wydajności, który odgrywał ważną rolę w pierwszych dwóch latach ukazywania się tej rubryki w piśmie CACM. Temat efek tywności pojawia się też w kilku z tych esejów, lecz ta książka obejmuje znacznie szerszy obszar ogólnego pejzażu programowania. Gdy zaczniesz czytać te eseje, nie śpiesz się zbytnio. Każdy z nich prze czytaj uważnie. Spróbuj rozwiązać przedstawione zadania; czasem wcale nie są takie łatwe, na jakie wyglądają. W zamieszczonych na końcu niektó rych rozdziałów omówieniach lektur uzupełniających nie odnoszę się do zwyczajowej naukowej bibliografii. Polecam kilka ważnych książek, które mam w swojej bibliotece. Miło mi wyrazić wdzięczność wielu osobom za okazaną mi cenną pomoc. Al Aho, Peter Denning, Brian Kernighan i Doug Mcllroy dro biazgowo skomentowali każdy esej. Sporo bardzo cennych uwag, za któ re dziękuję, udzielili mi: Bill Cleveland, Mike Garey, Eric Grosse, Gerard Holzmann, Lynn Jelinski, David Johnson, Arno Penzias, Ravi Sethi, Bjarne Stroustrup, Howard Trickey i Vic Vyssotsky. Kilka osób wyraziło zgodę na przytoczenie ich listów. Są to: Peter Denning, Bob Floyd, Frank Starmer, Vic Vyssotsky i Bruce Weide. Bardzo im jestem zobowiązany. Szczegól nie wdzięczny jestem stowarzyszeniu ACM za zachętę do opublikowania felietonów w tej postaci oraz wielu czytelnikom Communications, których uwagi spowodowały, że ukazanie się tej rozszerzonej wersji tekstów stało się niezbędne i możliwe. Firma Bell Labs, zwłaszcza zaś jej Computing Science Research Center, zapewniły mi wspaniałe warunki podczas pisania tych esejów. Wszystkim bardzo dziękuję. M urray Hill, N ew Jersey
1 W arszawa, W N T 1992 - pr^yp. tłum.
J. B.
SPIS TREŚCI
Część I. Metody programowania ..................................................
1
Rozdział 1. System y pro filo w an ia.........................................................................................
3
1.1. 1.2. 1.3. 1.4. 1.5. 1.6. 1.7.
Znajdowanie liczb pierwszych........................................................................... Systemy profilowania używ ania......................................................................... Specyfikowany system profilowania................................................................... Tworzenie systemów profilowania..................................................................... Zasady........................................................................................................................ Z adania..................................................................................................................... Literatura uzupełniająca......................................................................................
Rozdział 2. Tablice aso cjacyjn e............................................................................................. 2.1. 2.2. 2.3. 2.4. 2.5. 2.6.
3 9 11 13 15 15 17 19
Tablice asocjacyjne w Awku................................................................................ Symulator automatu skończonego..................................................................... Sortowanie topologiczne...................................................................................... Zasady....................................................................................................................... Z adania..................................................................................................................... Literatura uzupełniająca......................................................................................
20 23 25 29 30 32
Rozdział 3. W yznania p ro gram isty......................................................................................
33
3.1. 3.2. 3.3. 3.4. 3.5.
Przeszukiwanie dwójkowe.................................................................................... W ybór........................................................................................................................ Biblioteka podprogram ów.................................................................................. Zasady....................................................................................................................... Zadania.....................................................................................................................
34 37 40 43 45
Rozdział 4. Sam oopisujące się dane ..................................................................................
47
4.1. 4.2. 4.3. 4.4. 4.5.
Pary nazwa-wartość............................................................................................... Pochodzenie w programowaniu......................................................................... Laboratorium sortowania.................................................................................... Zasady........................................................................................................................ Zadania.....................................................................................................................
47 51 53 55 56
X
SPIS TREŚCI
Część II. Zawodowe sztuczki ....................................................... 59 Rozdział 5. Przecinanie w ęzła g o rd y jsk ie g o .................................................................. 5.1. 5.2. 5.3. 5.4. 5.5. 5.6. 5.7.
61
Q uiz........................................................................................................................... Niektóre rozwiązania............................................................................................ W skazówki.............................................................................................................. Zasady....................................................................................................................... Z adania.................................................................................................................... Literatura uzupełniająca ...................................................................................... Uruchamianie [iVa marginesiĄ.............................................................................
61 62 63 68 69 70 71
Rozdział 6. Inform atyczne s lo g a n y ...................................................................................
73
6.1. K odowanie.............................................................................................................. 6.2. Interfejsy użytkownika ........................................................................................ 6.3. Wyszukiwanie błędów.......................................................................................... 6.4. Wydajność................................................................................................................ 6.5. D okum entacja....................................................................................................... 6.6. Oprogramowanie zarządzania........................................................................... 6.7. Rozmaite rady ....................................................................................................... 6.8. Zasady....................................................................................................................... 6.9. Z adania..................................................................................................................... 6.10.Literatura uzupełniająca......................................................................................
74 76 76 78 80 80 83 84 84 85
Rozdział 7. Powrót k o p e r ty ................................................................................................... 7.1. 7.2. 7.3. 7.4. 7.5. 7.6. 7.7.
87
Podgrzewanie chłodnych um ysłów.................................................................. Reguły dotyczące wydajności oparte na doświadczeniu............................. Prawo Little’a .......................................................................................................... Z asady....................................................................................................................... Z adania..................................................................................................................... Literatura uzupełniająca...................................................................................... Szybkie obliczenia w życiu codziennym [Na marginesie] ............................
87 89 92 94 94 96 96
Rozdział 8. M em orandum F u rb elo w a...............................................................................
99
8.1. Memorandum.......................................................................................................... 100 8.2. Z a sa d y -J.B ............................................................................................................. 102 8.3. Literatura uzupełniająca...................................................................................... 103
Część III. Wejście-wyjście dostosowane do ludzi ..................... 105 Rozdział 9. M ałe ję z y k i............................................................................................................ 107 9.1. 9.2. 9.3. 9.4. 9.5. 9.6. 9.7.
Język P ic ................................................................................................................... Perspektywa............................................................................................................ Preprocesory języka P i c ...................................................................................... Małe języki do implementowania języka P ic ................................................ Z asady....................................................................................................................... Z adania..................................................................................................................... Literatura uzupełniająca......................................................................................
108 112 116 119 124 126 128
XI
SPIS TREŚCI
Rozdział 10. Projektowanie d o ku m en tu........................................................................... 129 10.1. 10.2. 10.3. 10.4. 10.5. 10.6. 10.7. 10.8. 10.9.
T abele..................................................................................................................... Trzy zasady projektowania................................................................................ Rysunki................................................................................................................... T ekst........................................................................................................................ Właściwy środek wyrazu .................................................................................. Zasady..................................................................................................................... Zadania................................................................................................................... Literatura uzupełniająca.................................................................................... Katalog ulubionych utrapień [Na marginesie] ...............................................
130 133 134 137 140 143 144 145 145
Rozdział 11. W yjście g r a f ic z n e ............................................................................................. 147 11.1. 11.2. 11.3. 11.4. 11.5. 11.6.
Studium sposobu obrazowania....................................................................... Przykłady rodzajów obrazowania.................................................................. Zasady..................................................................................................................... Zadania................................................................................................................... Literatura uzupełniająca.................................................................................... Marsz Napoleona na Moskwę [Na marginesie] .............................................
147 151 154 156 158 159
Rozdział 12. 12.1. 12.2. 12.3. 12.4. 12.5.
Sondaż s o n d a ż y ............................................................................................... Problemy związane z prowadzeniem sondaży............................................ Języki........................................................................................................................ O b razy................................................................................................................... Zasady..................................................................................................................... Zadania...................................................................................................................
163 163 164 169 173 173
Część IV. Algorytmy ..........................
175
Rozdział 13. Przykład b ły sk o tliw o ści................................................................................ 177 13.1. 13.2. 13.3. 13.4. 13.5. 13.6.
Próbkowanie algorytmów próbkowania........................................................ Algorytm F loyda................................................................................................. Permutacje losowe............................................................................................... Zasady..................................................................................................................... Zadania................................................................................................................... Literatura uzupełniająca....................................................................................
177 179 181 183 183 184
Rozdział 14. N arodziny n u m eryk a....................................................................................... 187 14.1. 14.2. 14.3. 14.4. 14.5. 14.6. 14.7. 14.8.
P ro b lem ................................................................................................................. Iteracja N ewtona................................................................................................. Dobry początek.................................................................................................... K od.......................................................................................................................... Zasady..................................................................................................................... Zadania................................................................................................................... Literatura uzupełniająca.................................................................................... Historia wielkiego sukcesu[Na marginesie].....................................................
187 189 191 193 196 197 200 200
Rozdział 15. W y b ó r ................................................................................................................... 203 15.1. P ro b lem ................................................................................................................. 203 15.2. P rogram ................................................................................................................. 204
XII
SPIS TREŚCI
15.3. 15.4. 15.5. 15.6.
Analiza czasu wykonywania............................................................................ Zasady.................................................................................................................... Zadania................................................................................................................. Literatura uzupełniająca..................................................................................
208 213 214 217
D odatek 1. Język C i język A w k .......................................................................................... 219 1.1. 1.2.
Język C ................................................................................................................. 219 Język Awk .......................................................................................................... 220
D odatek 2. B iblioteka podprogram ów stan d ard o w y c h ............................................ 223 R ozw iązania w ybranych z a d a ń .............................................................................................. 231
Skorowidz
257
CZĘŚĆ i.
METODY PROGRAMOWANIA
Nie mam cierpliwości, aby odkładać najlepsze na koniec. W pierwszych czterech rozdziałach zajmuję się najlepszą częścią pracy programisty: tymi błogimi godzinami, które spędzasz przy klawiaturze, wpatrując się w ekran komputera. W rozdziale 1 pokazuję, jak system profilowania może ułatwić wgląd w dynamikę programu. Rozdział 2 dotyczy tablic asocjacyjnych, potęż nej struktury danych. Rozdział 3 zawiera opis platformy uruchomieniowej służącej do testowania małych podprogramów i usuwania z nich błędów. W rozdziale 4 omawiam sposoby tworzenia samoopisujących się plików danych. Metody te działają na prawdziwych programach; muszą być zatem przedstawione w rzeczywistym języku dla istniejącego systemu. W roz dziale 1 korzystam z języka C, a rozdziały 2 i 3 zawierają kilka programów w języku Awk. Wszystkie przykłady są napisane z użyciem prostych pod zbiorów tych języków. Dla wygody czytelników, którzy mogą czuć się niepewnie, w dodatku 1 zamieściłem krótkie opisy obu języków, C i Awk. Wybór tych języków nie ma jednak większego znaczenia, ponieważ oma wiane metody można stosować w dowolnym systemie. Rozdział 1 ukazał się w lipcu 1987 roku w Communications o f the ACM. Rozdziały 2 i 3 były opublikowane w czerwcu i lipcu 1985 roku wraz ze wcześniejszą wersją dodatków 1 i 2. Rozdział 4 znalazł się w numerze z czerwca 1987 roku.
ROZDZIAŁ 1.
SYSTEMY PROFILOWANIA
Stetoskop jest prostym narzędziem, które zrewolucjonizowało praktykę medyczną: zapewnia lekarzowi efektywny sposób monitorowania ludzkie go ciała. To samo dla Twoich programów może uczynić system profilo wania. Jakich teraz używasz narzędzi do badania tworzonych programów? Wymyślne systemy analizowania są obecnie szeroko dostępne, poczynając od interaktywnych systemów uruchomieniowych, na systemach służących do animacji programowej kończąc. Ale jak tomografy komputerowe nigdy nie zastąpią stetoskopów, tak i złożone oprogramowanie nigdy nie zastąpi najprostszych narzędzi, które nam, programistom, służą do monitorowania pisanych przez nas programów. System profilowania ujawnia, jak często jest wykonywany dany fragment programu. Na początku tego rozdziału korzystam z dwóch rodzajów systemów profilowania w celu przyspieszenia działania małego programu (ale pamię tajmy, że w rzeczywistości zmierzam do przedstawienia systemów profi lowania). W następnych podrozdziałach podaję zarys różnych zastosowań tych narzędzi, omawiam system profilowania dla języka nieproceduralnego i metody tworzenia tych systemów.
1.1. Znajdowanie liczb pierwszych Program PI jest napisany w języku ANSI Standard C i służy do druko wania wszystkich kolejnych liczb pierwszych, które są mniejsze niż 1000 (zob. dodatek 1, jeśli nie znasz języka C). int primeCint n) {
int i;
4
1. SYSTEMY PROFILOWANIA
999 78022 831 168
for (i = 2; i < n; i++) if(n 7, i == 0) return 0; return 1;
> 1 1 999 168
mainO { int i, n; n = 1000; for (i = 2; i int prime(int n) { int i;
999 5288
for (i = 2; i < = root(n); i++) if (n '/, i == 0) return 0; return 1;
831 168
> mainO i int i, n;
1 1
n = 1000; for (i = 2; i print i
> > exit
} function prime(n, i) { < « 9 9 8 » > for (i=0; x [i] *x[i]
14
1. SYSTEMY PROFILOWANIA
if (n ’ /, x [i] == 0) { < « 8 3 1 » > return 0
} } { } x[xc++] = n return 1
} Liczba w nawiasach kątowych występujących po lewym nawiasie klamro wym mówi nam, ile razy był wykonany dany blok. Na szczęście te liczby są zgodne z licznikami podanymi przez system profilowania programów w języku C. Mój system składał się z dwóch pięciowierszowych programów w ję zyku Awk. Pierwszy program czyta program źródłowy napisany w Awku i tworzy nowy program, w którym wyróżniony licznik jest zwiększany na początku każdego bloku, a nowa czynność END (zob. dodatek 1) zapisuje wszystkie liczniki w pliku po wykonaniu programu. Podczas wykonywa nia programu wynikowego powstaje plik liczników. Drugi program czyta te liczniki i dołącza je do programu źródłowego. Profilowany program działa około 25 procent wolniej niż jego wersja pierwotna i nie wszystkie programy w języku Awk są wykonywane poprawnie - musiałem zmie nić pojedyncze wiersze w kilku programach. Ale poświęcenie paru godzin na wszystkie jego wady było niewielką inwestycją, dzięki której powstał działający prototyp systemu profilowania. Szczegółowy opis podobnego systemu profilowania znajduje się w p. 7.2 książki The AWK Programming Tanguage, o której wspominam w p. 2.6. Częściej się tworzy szybkie systemy profilowania, niż pisze się o nich. Oto kilka przykładów: W miesięczniku BYTE z sierpnia 1983 roku Leas i Wintz opisują system profilowania zaimplementowany jako 20-wierszowy program napisany w języku asemblera 6800. Howard Trickey z Laboratoriów Bella w ciągu godziny zaimplemen tował zliczanie funkcji w Lispie, zmieniając instrukcję defun, tak aby zwiększała licznik przy każdym wejściu do funkcji. Rob Pike w 1978 roku napisał 20-wierszowy program w Fortranie, któ ry był systemem profilowania zliczającym czas wykonywania progra mów. Po wykonaniu instrukcji CALL PROFIL (10) czas jednostki cen tralnej jest dodawany do wartości licznika 10.
1.5. ZASADY
15
Takie i wiele innych systemów profilowania można napisać w jeden wieczór. Utworzony w ten sposób system profilowania mógłby z łatwością zaoszczędzić Ci więcej czasu, niż ten jeden wieczór, który przeznaczysz na jego napisanie.
1.5. Zasady W tym rozdziale przedstawiłem jedynie zarys zagadnienia, jakim jest pro filowanie programów. Zająłem się tylko podstawami, pomijając oryginalne sposoby gromadzenia danych (takie jak monitory sprzętowe) i oryginalne obrazowanie wyników (takie jak systemy animacji). Podstawowe są również następujące zasady: Korzystaj %profilowania. Uczyń ten miesiąc Miesiącem Profilowania. Pro filuj przynajmniej jeden fragment programu przez kilka następnych tygodni i zachęcaj do tego swoich kolegów. Pamiętaj, że dobry programista może zawsze skorzystać z pomocy małego programu. Stwory system profilowania. Jeżeli nie masz pod ręką systemu profilowa nia, to go zaimprowizuj. Większość systemów komputerowych pozwala korzystać z podstawowych operacji profilowania. Programiści, którzy 25 lat temu musieli interpretować światełka na konsoli, mogą teraz te same informacje odczytać z okna graficznego na osobistej stacji roboczej. Czę sto wystarczy posłużyć się małym programem, w którym są upakowane właściwości oprogramowania systemowego, tworząc wygodne narzędzie.
1.6. Zadania 1. Przypuśćmy, że tablica X [ l . . 1000] jest zapełniona losowymi liczbami rzeczywistymi. Poniższa procedura oblicza minimalną i maksymalną wartość w tablicy: Max := Min := X[l] for I := 2 to 1000 do if X [I] > Max then Max := X [I] if X [I] < Min then Min := X [I]
Pan B. C. Dull zauważył, że jeżeli jakiś element tablicy jest nowym maksimum, to nie może być minimum. Dlatego napisał te porównania w nowy sposób: if X[I] > Max then Max := X [I] else if X[I] < Min then Min := X [I]
16
1. SYSTEM Y PROFILOWANIA
Ile porównań oszczędza się przeciętnie? Najpierw zgadnij odpowiedź, później napisz program, który będzie to profilował. Jaki był wynik zgadywania? 2. Poniższe zadanie dotyczy obliczania liczb pierwszych. a.
Program P6 zmniejszył czas wykonywania o dwa rzędy wielkości w stosunku do programu PI. Czy możesz jeszcze bardziej skrócić czas przy tym podejściu do zadania?
b. Zaimplementuj proste sito Eratostenesa w celu obliczania wszyst kich liczb pierwszych mniejszych niż n. Pierwotną strukturą danych dla tego programu jest tablica n bitów, które na początku wszystkie mają wartość prawda. Za każdym razem, gdy wykrywa się liczbę pierwszą, wtedy wszystkim jej wielokrotnościom nadaje się wartość fałsz. Następną liczbą pierwszą jest następny bit tej tablicy mający wartość prawda. c. Jaki jest czas wykonywania programu sita Eratostenesa jako funkcji zmiennej n w punkcie 2.b? Znajdź algorytm, którego czas wyko nywania jest równy 0 (n). d. Dana jest bardzo duża liczba całkowita (powiedzmy, kilkaset bi tów); jak można by sprawdzać, czy jest to liczba pierwsza? 3. Prosty system profilowania określający liczniki instrukcji zwiększa licz nik dla każdej instrukcji. Opisz, jak zmniejszyć zajętość pamięci i czas wykonywania dzięki zastosowaniu mniejszej liczby liczników. (Używa łem kiedyś systemu Pascala, w którym profilowanie programu sto krotnie spowalniało jego wykonanie; opisany w tym rozdziale system profilowania zliczający wiersze korzysta z takich chwytów, spowalniając program tylko o kilka procent). 4. Prosty system profilowania określający czas wykonywania procedur ocenia czas zużyty przez każdą procedurę, badając licznik rozkazów w stałych przedziałach (w moim systemie 60 razy na sekundę). Ta informacja określa czas zużyty w każdej części tekstu programu, lecz nie podaje, które wywołania procedur są pożeraczami czasu. Niektó re systemy profilowania podają koszt wszystkich funkcji i ich dyna micznych potomków. Pokaż, jak można uzyskać więcej informacji ze stosu podczas wykonywania programu, przydzielając czas funkcjom wywołującym i wywoływanym. Mając te dane, jak można je użytecznie zobrazować?
1.7. LITERATURA UZUPEŁNIAJĄCA
17
5. Dokładne liczby są przydatne do interpretowania profili programu na jednym zbiorze danych. Kiedy wszakże jest dużo danych, wtedy wielka liczba cyfr może zaciemnić komunikat ukryty w tych liczbach. Jak moż na by zobrazować profile programu lub potoku dla 100 różnych danych wejściowych? Rozważ specjalne graficzne przedstawienie danych. 6. Omawiany w p. 1.4 program P6 jest poprawny, lecz trudno tego do wieść. Na czym polega problem i jak można go rozwiązać?
1.7. Literatura uzupełniająca Artykuł Dona Knutha „Empirical Study of Fortran Programs” ukazał się w czasopiśmie Software — Practice and Experience w roku 1971 (t. 1, s. 105-133). W części 3 tego artykułu, dotyczącej statystyk dynamicznych, Knuth omawia zarówno systemy profilowania zliczające wiersze, jak i sys temy obliczające czas wykonywania procedur, a także analizuje utworzone statystyki. W części 4 Knuth opisuje doskonalenie kodu siedemnastu we wnętrznych pętli, których współczynniki przyspieszenia wynoszą od 1,5 do 13,1. W ciągu ostatniego dziesięciolecia czytałem tę klasyczną pracę co najmniej raz na rok i wciąż wydaje mi się ona coraz lepsza. Gorąco ją polecam.
ROZDZIAŁ 2.
TABLICE ASOCJACYJNE
Antropolodzy uważają, że język wywiera głęboki wpływ na obraz świata. To spostrzeżenie, znane zazwyczaj jako hipoteza Whorfa, streszcza się często, mówiąc że „język człowieka nadaje kształt jego myśli”. Jak wielu programistów, tak i ja poglądy na programowanie ukształ towałem pod wpływem mojego algolopodobnego języka. Dla takich jak ja programistów języki: PL/1, C i Pascal wyglądają bardzo podobnie i nie sprawia nam trudności tłumaczenie kodu zapisanego w którymś z nich na kod w Cobolu lub Fortranie. Nasze stare, wygodne wzorce myślowe można z łatwością wyrażać w tych językach. Ale inne języki rzucają wyzwanie naszemu sposobowi myślenia o pro gramowaniu. Zdumiewają nas sztuczki magiczne, które wyczyniają pro gramiści, korzystając z działań na S-wyrażeniach i z rekurencji w Lispie. Zaskakują nas wielbiciele APL-u, którzy modelują uniwersum jako produkt zewnętrzny pary długich wektorów, czy też ludzie programujący w Snobolu, którzy zdają się każdy problem przekształcać w wielki napis. Dla nas, algolowych programistów, uczenie się tych obcych kultur może od bywać się w bólach, lecz zazwyczaj takie doświadczenie pogłębia nasze rozumienie programowania. W tym rozdziale zajmiemy się właściwością języka nienależącą do dzie dzictwa Algolu: tablicami asocjacyjnymi. W znanych nam tablicach używa się liczbowych indeksów, podczas gdy tablice asocjacyjne dopuszczają sto sowanie takich odniesień, jak com t["cat"]. Tego rodzaju struktury danych występują w takich językach, jak Snobol i Rexx (interpretator rozkazów w maszynach IBM); pozwalają one wyrażać złożone algorytmy w prostych programach. Te tablice są na tyle bliskie Algolowi, że można je szybko zrozumieć, lecz dostatecznie nowe, by stanowiły wyzwanie dla naszego sposobu myślenia.
20
2. TABLICE ASOCJACYJNE
Zanalizujemy tablice asocjacyjne, którymi można się posługiwać, pro gramując w Awku. Większość konstrukcji tego języka wywodzi się z trady cji algolowej, lecz na zbadanie zasługują tablice asocjacyjne i jeszcze kilka innych właściwości Awku. Najpierw poznamy tablice asocjacyjne. W na stępnych podrozdziałach omówimy dwa programy, które są kłopotliwe w większości języków algolopodobnych, można zaś je elegancko napisać w Awku.
2.1. Tablice asocjacyjne w Awku Dodatek 1 zawiera zarys języka Awk. Przeanalizujemy ten język pobież nie, omawiając program, który ma znajdować podejrzane pozycje w pliku nazw. Każdy wiersz tego programu jest parą „wzorzec-czynność”. Dla każdego wiersza wejściowego, który jest zgodny z wzorcem występującym jako pierwszy element pary, będzie wykonana czynność ujęta w nawiasy klamrowe i będąca drugim elementem tej pary. Pełny program w Awku może składać sięz tylko trzech wierszy kodu: length($l) > NF != 1 END
10 { e++; print "długa nazwa w wierszu", NR } { e++; print "zły licznik nazw w wierszu", { if (e > 0) print "ogólne błędy: ", e }
NR }
Pierwszy wzorzec wychwytuje długie nazwy. Jeżeli pierwsze pole (nazwa ne $1) ma więcej niż 10 znaków, to czynność zwiększa wartość zmiennej e i drukuje ostrzeżenie, używając wbudowanej zmiennej NR (oznaczającej licznik rekordów lub licznik wierszy). Zmienna e jest licznikiem błędów. Wszystkie zmienne otrzymują wartość zero podczas inicjacji programu w Awku. Druga para wychwytuje wiersze, które nie zawierają tylko jednej nazwy (zmienna wbudowana NF zawiera liczbę pól w wierszach wejścio wych). Trzecia czynność jest wykonywana po zakończeniu pobierania da nych wejściowych. Sprowadza się ona do drukowania liczby błędów, które wystąpiły w programie. Tablice asocjacyjne nie znajdują się w pamięci operacyjnej Awku; wie le programów napisanych w tym języku ich nie używa. Lecz te tablice można z łatwością wcielać do języka: jak inne zmienne, tak i one nie są deklarowane i są automatycznie inicjowane przy pierwszym ich użyciu. Teraz zajmiemy się drugim problemem związanym z nazwami: dany jest plik n nazw i mamy wygenerować wszystkie n2 par tych nazw. Znam kilku ludzi, którzy użyli takiego programu, aby pomógł im wybrać pierwsze
2.1. TABLICE ASOCJACYJNE W AWKU
21
i drugie imiona dla ich dzieci. Jeżeli plik wejściowy zawiera imiona: Billy, Bob i Willy, to wynik może skłonić rodziców do wyboru miłego dla ucha takiego zestawu, jak Billy Bob, odrzucenia zaś pary Billy Willy. W tym programie zmienna n służy do zliczania znalezionych dotych czas nazw. Jak wszystkie zmienne w Awku, otrzymuje na początku wartość zero. Pierwsza instrukcja jest wykonywana dla każdego wiersza danych wejściowych; zauważ, że wartość zmiennej n jest zwiększana przed jej użyciem. { name [++n] = $1 } END { for (i = 1; i Tablica asocjacyjna name przedstawia zbiór nazw. Wszystkie elementy ta blicy name mają wartość 1; informacja jest zawarta w indeksach tej tablicy. Tę informację pozyskuje się za pośrednictwem następującej pętli: for (i in name) instrukcja
W tej pętli in s tr u k c ja jest powtarzana dla wszystkich wartości /, które są indeksami tablicy name, a właściwie nazwami należącymi do pliku wejścio wego. W pętli występują kolejno wszystkie nazwy w dowolnym porządku; nazwy będą zazwyczaj nieposortowane. (Awk pozwala korzystać z wygod nego interfejsu do funkcji sortowania w systemie UNIX, ale nie będziemy się tym zajmować w tym rozdziale). Następny program przenosi nas z pokoju dziecięcego do kuchni. Wo lelibyśmy, aby lista zakupów w rodzaju krakersy 3 wafelki 2 krakersy 1 piwo 5 wafelki 1
była skrócona do wygodniejszej postaci: krakersy 4 piwo 5 wafelki 3
Wykona to poniższy program. i count[$l] = count[$l] + $2 } END { for (i in count) print i, count[i] }
W podrozdziale 1.3 omawialiśmy program, który zliczał wszystkie wy stąpienia każdego słowa w dokumencie. Poniższy program wykona to zadanie, nakładając pola Awku na prostą definicję słowa: ciąg znaków rozdzielonych znakami pustymi. Tak więc trzy napisy: „Słowa”, „słowa” i „słowa;” są trzema różnymi słowami. { for (i = 1; i
23
2.2. SYMULATOR AUTOMATU SKOŃCZONEGO
Powyższy program wymagał 40 sekund pracy procesora maszyny VAX-11/750, aby przetworzyć 4500 słów oryginału tego rozdziału. Trzy naj częściej używane słowa to: „the” (213 razy), „to” (110) i „o f” (104). Wy konaniem tego programu zajmiemy się ponownie w p. 11.1. Oto banalny program zapisujący w pliku wejściowym wszystkie słowa, które nie występują w pliku słownika wyrazy. Na początku instrukcja g e t lin e języka Awk służy do umieszczenia słownika wyrazy w tablicy dobrewyrazyBEGIN { while (getline < "wyrazy") { for (i = 1; i Sym W y
to są spełnione następujące równości: trans [Stani, SymWe] == Stan2 out[Stani, SymWe] == SymWy
Nazwa tr a n s oznacza przejście do stanu, nazwa out zaś, symbol wyj ściowy. Opisany powyżej automat i jego wejście są zakodowane w następujący sposób.
2.3. SORTOWANIE TOPO LOGICZNE
25
owz o owz o OWZ 1 OWJ 0 OWJ 1 start
OWJ o OWZ 1 OWJ 1 OWZ
0 1 1 O
Pierwsze cztery wiersze reprezentują cztery wierzchołki automatu skoń czonego. Pierwszy wiersz oznacza, że jeżeli automat jest w stanie OWZ i na wejściu jest zero, to następnym stanem będzie OWZ i na wyjściu pojawi się zero. Piąty wiersz określa stan początkowy, następne wiersze są danymi wejściowymi. Oto program, który realizuje opisane powyżej automaty skończone. run == 1 { print out[s, $1]; s = trans[s, $1] > run == 0 { if ($1 == "start") {run = 1 ; s = $2 > else { trans [$1, $2] = $3; out[$l, $2] = $4 }
} Program działa w dwóch podstawowych trybach. Na początku zmienna run ma wartość zero. W tym trybie przejście w automacie będzie dodane do tablicy trans i do tablicy out. Kiedy w pierwszym polu wiersza znajdu je się napis „start”, wtedy program zapamiętuje w zmiennej s pożądany stan początkowy i przechodzi do trybu run. W każdym kroku wykony wania programu jako wynik funkcji aktualnych danych wejściowych ($1) i bieżącego stanu (r) powstają dane wyjściowe i następuje zmiana stanu. Ten miniaturowy program ma wiele wad. Jego reakcja na przykład na niezdefiniowane przejścia będzie katastroficzna. Program w tej postaci nadaje się w najlepszym razie do osobistego użytku. Z drugiej strony, jest wygodną podstawą, na której można zbudować solidniejszy program (zob. zadanie 2).
2.3. Sortowanie topologiczne Danymi wejściowymi dla algorytmu sortowania topologicznego jest graf skierowany bez cykli, taki jak poniższy:
26
2. TABLICE ASOCJACYJNE
E «e-
B
'I -3*- F
w h
'
Jeżeli graf ma krawędź od A do B, to mówimy, że A jest poprzednikiem B oraz B jest następnikiem A. Algorytm musi porządkować węzły, tak by wszystkie poprzedniki występowały przed swymi następnikami; oto jedno z możliwych uporządkowań. »H
Algorytm musi poradzić sobie z taką możliwością, że graf wejściowy za wiera cykl, a więc nie może być posortowany. Takiego algorytmu można na przykład użyć, rysując trójwymiarową scenę obiektów. Obiekt A poprzedza obiekt B, jeżeli na obrazie B jest z przodu przed A , ponieważ A musi być narysowane przed B. Ze sceny dla czterech prostokątów po lewej stronie rysunku wywodzi się uporząd kowanie częściowe przedstawione po prawej stronie.
Istnieją dwa poprawne uporządkowania tych wierzchołków: A B C D oraz A C B D. Każdy z tych porządków ustanawia właściwe nakładanie się obiektów. Do innych zastosowań sortowania topologicznego należy wykładanie (ang. laying out) dokumentu technicznego (terminy muszą być zdefiniowane przed użyciem), a także hierarchiczne przetwarzanie projek tów o bardzo dużej skali integracji (algorytm musi najpierw przetwarzać części składowe danej części). Zanim będziesz dalej czytać, pomyśl teraz przez chwilę o tym, jak można by napisać program sortowania topolo gicznego dla grafu skierowanego.
27
2.3. SORTOWANIE TOPOLOGICZNE
Przeanalizujemy algorytm sortowania topologicznego zamieszczony w książce Knutha Sztuka, programowania. Tom 1: Algorytmy podstawowe2. Krok iteracyjny w tym algorytmie można tak przedstawić: wybierz węzeł T, który nie ma poprzedników, umieść T w pliku wyjściowym i następnie usuń z grafu wszystkie krawędzie wychodzące z T. Na rysunku widzimy przebieg algorytmu dla grafu zawierającego cztery węzły. Kolejne etapy są przedstawione na rysunku od lewej ku prawej jego stronie; w każdym etapie węzeł T jest ujęty w kółeczko. W wyniku powstaje lista A B C D.
C
® Powolna implementacja tego algorytmu analizuje cały graf na każdym kroku, aby znaleźć węzeł bez poprzedników. Teraz zbadamy prostszy algo rytm, który jest też wydajniejszy. Algorytm zapamiętuje dla każdego węzła liczbę jego poprzedników i zbiór jego następników. Narysowany powyżej czterowęzłowy graf jest reprezentowany jako tabela: W
ęzeł
L ic z n ik
p o p r z e d n ik ó w
A B C D
0 1 1 2
N
a s t ę p n ik i
B C D D
W kroku iteracji algorytmu jest wybierany węzeł, którego licznik poprzed ników jest zerem, węzeł ten jest odsyłany na wyjście i jest zmniejszany licznik poprzedników wszystkich jego następników. Musi być jednak zapa miętana kolejność przyjmowania wartości zero przez liczniki; w tym celu zastosowano kolejkę węzłów. (Jeżeli kolejka będzie pusta, zanim wszystkie węzły będą mieć zero w liczniku poprzedników, to program poinformuje, że graf zawiera cykl). Podany na następnej stronie pseudokod oparto na założeniu, że graf jest przedstawiany programowi jako ciąg par (poprzednik, następnik), po jednej parze w każdym wierszu. 2 Warszawa, WNT 2002 - p n y p . tłum.
28
2. TABLICE ASOCJACYJNE
gdy każda para (poprzednik, następnik) jest pobierana zwiększ licznik poprzedników następnika dopisz następnik do następników poprzednika na końcu pliku wejściowego zainicjuj pustą kolejkę dla każdego węzła i jeżeli licznik poprzedników i jest zerem to dopisz i do kolejki dopóki kolejka nie jest pusta usuń t z przodu kolejki; drukuj t dla każdego następnika s węzła t zmniejsz licznik poprzedników s jeżeli powstanie zero to dodaj x do kolejki jeżeli jakieś węzły nie znalazły się na wyjściu to informuj o cyklu
Czas wykonywania algorytmu jest proporcjonalny do liczby krawędzi w grafie i nie różni się od optymalnego z dokładnością do stałej. (Każ da krawędź jest dwukrotnie przetwarzana: najpierw, kiedy jest pobierana i ponownie, gdy jest usuwana z kolejki). W programie napisanym w Awku kolejka jest zrealizowana jako tablica z indeksami z przedziału 1.. n. Zmienna całkowita qlo jest indeksem pierw szego elementu, zmienna zaś qhi, indeksem ostatniego elementu w kolejce. Zbiór następników jest reprezentowany przez dwie tablice. Jeżeli węzeł A ma następniki: B, C oraz D, to zachodzą następujące relacje: succcnt["A"] == 3 succlist["A", "1"] == "B" succlist["A", "2"] == "C" succlist["A", "3"] == "D"
Danymi wejściowymi dla poniższego programu w Awku jest plik par (po przednik, następnik). Danymi wyjściowymi jest albo posortowana lista wę złów, albo ostrzeżenie, że taka lista nie istnieje. { ++predct[$2] # zapisz węzły w predct, predct[$l] = predct[$1] # nawet te bez poprzedników succlist[$1, ++succcnt[$1]] = $2
> END { qlo = 1 for (i in predct) { n++; if (predct [i] == 0) q[++qhi] = i
} while (qlo if (qhi != n) print "błąd sortowania: cykl w pliku wejściowym"
} Drugi wiersz programu zapewnia, że wszystkie węzły, nawet te, które nie mają poprzedników, występują jako indeksy tablicy predct. Tablice asocjacyjne w tym programie reprezentują kilka różnych abs trakcyjnych typów danych: tabelę symboli nazw węzłów, tablicę rekordów, dwuwymiarowy ciąg zbiorów następników i kolejkę węzłów. Ponieważ ten program jest krótki, więc można go z łatwością zrozumieć, ale nierozróżnienie abstrakcyjnych typów danych mogłoby sprawić, że większy program będzie nie do rozszyfrowania.
2.4. Zasady Programując w Awku, można wiele zrobić małymi środkami. Większość omawianych przez nas programów byłaby o jeden rząd wielkości obszer niejsza, gdybyśmy je napisali w konwencjonalnym języku. Rozmiar tej re dukcji zawdzięczamy przede wszystkim kilku właściwościom języka Awk: niejawnemu ¿terowaniu wierszy wejściowych, automatycznemu rozdziele niu pól, inicjacji i przekształceniom zmiennych oraz tablicom asocjacyj nym. Te tablice są jedynym mechanizmem, który w Awku służy do powiąza nia jego pierwotnych typów danych - napisów i liczb. Na szczęście, tablice asocjacyjne mogą całkiem naturalnie reprezentować wiele struktur danych. Tablice. Bezpośrednio implementuje się tablice jedno- i wielowymiarowe oraz rozrzedzone. Struktury sekwencyjne. Używając tablicy asocjacyjnej i jednego lub dwóch indeksów można tworzyć kolejki i stosy. Tablice symboli. Tablice symboli zapewniają odwzorowanie nazwy na wartość. W tablicach symboli w Awku utrzymano zasadę: tabsym\naywa\ —wartość. Jeżeli wszystkie nazwy mają tę samą wartość, to tablica symboli reprezentuje zbiór. Grafy. Automaty skończone i sortowanie topologiczne przetwarzają grafy skierowane. Program automatu skończonego korzysta z repre zentacji macierzy dla grafu, podczas gdy w sortowaniu topologicznym używa się reprezentacji ciągu krawędzi.
30
2. TABLICE ASOCJACYJNE
Pomijając kwestie edukacyjne, jaką wartość praktyczną przedstawia ję zyk Awk i jego tablice asocjacyjne? Napisane w Awku programy są krót kie. Nie zawsze jest to zaletą (podobnie jak jednowierszowe programy w APL-u mogą być okropnie hermetyczne), lecz dziesięć wierszy kodu to prawie zawsze lepiej niż sto. Niestety, kod w Awku na ogół jest wy konywany powoli. Tablice symboli w tym języku są względnie wydajne, lecz tablice indeksowane liczbami całkowitymi są o rząd wielkości powol niejsze niż konwencjonalne implementacje tablic. Kiedy krótkie, powolne programy są użyteczne? Koszt wykonywania wielu programów jest bez znaczenia w porówna niu z kosztem ich opracowania. Dla niektórych zadań program sorto wania topologicznego napisany w Awku ma prawie produkcyjną jakość; powinien być bardziej odporny na błędy. Proste programy służą jako użyteczne prototypy. Niech użytkownicy najpierw spróbują małego programu. Jeśli im się spodoba, to później będzie można stworzyć jego wersję przemysłową. Ja używam Awku do tworzenia środowiska testowania podprogramów wymagających wyjątkowej precyzji; w następnym rozdziale powrócimy jeszcze do tego tematu.
2.5. Zadania 1. Wybierz któryś z przedstawionych w tym rozdziale programów napi sanych w Awku i napisz go w innym języku. Porównaj wielkość kodu źródłowego i efektywność wykonywania obydwu programów. 2. Spróbuj w rozmaity sposób rozbudowywać symulator automatu skoń czonego. Rozważ możliwość uzupełnienia go o wyszukiwanie błędów (niepoprawne stany, niepoprawne dane wejściowe itd.), dodania do niego domyślnych przejść ze stanu w stan i wprowadzenia klas zna ków (takich jak liczby całkowite i litery). Napisz program analizatora leksykalnego dla prostego języka programowania. 3. Omawiany w tym rozdziale program sortowania topologicznego in formuje o wystąpieniu cyklu. Zmień tak program, by sam cykl został wydrukowany. Uczyń ten program bardziej odpornym na pojawianie się błędów.
31
2.5. ZADANIA
4. Wykaż, że graf indukowany przez obraz trójwymiarowy może zawierać cykle. Sformułuj ograniczenia, które nie dopuszczą do powstawania cykli. 5. Zaprojektuj programy dla następujących zadań. Jak można zastosować tablice asocjacyjne, aby uprościć programy? a. Drzewa. Napisz program tworzenia drzew binarnych oraz ich prze szukiwania. b. Grafy. Napisz od nowa program sortowania topologicznego, korzy stający z przeszukiwania zstępującego. Dany jest graf skierowany i wyróżniony węzeł x\ przedstaw wszystkie węzły, które można osiągnąć z węzła x. Dany jest graf ważony i dwa węzły, x oraz j ; przedstaw najkrótszą drogę od x do y . c.
Dokumenty. Użyj prostego słownika do przekładu z jednego języ ka naturalnego na drugi (wiersz w słowniku angielsko-francuskim mógłby zawierać takie na przykład dwa słowa: „hello bonjour”). Sporządź listę odsyłaczy dla tekstu lub pliku programu ze wszyst kimi odniesieniami do każdego słowa przedstawionego z numerem wiersza. Aby uzyskać bardziej realistyczną definicję słowa, progra miści piszący w Awku mogliby używać separatorów pól w wier szach i instrukcji zastępowania.
d. Generowanie %dań losowych. Danymi wejściowymi dla tego programu jest gramatyka bezkontekstowa, taka jak poniższa. S —>NP VP NP —>AL N | N N —♦Jan | Marek AL —> A | A AL A —>Duży | Mały | Chudy VP -*■ V AvL V —>biegnie | idzie AvL —>Av | AvL Av Av —>szybko | wolno
Program powinien tworzyć zdania losowe, takie jak „Jan idzie szyb ko” oraz „Duży Mały Marek biegnie wolno szybko wolno”. e. Filtry. Inna nazwa programu, który odsiewa z pliku powtarzające się w nim słowa; program wyszukiwania błędów ortograficznych od siewa słowa, które są w słowniku. Napisz inne filtry słów, takie jak program usuwający słowa, których nie ma w „zatwierdzonej liście”, lecz pozostawiający zatwierdzone słowa w tym samym porządku. (Te zadania są łatwiejsze, jeśli dane wejściowe są posortowane).
32
2. TABLICE ASOCJACYJNE
£
6.
Gry tablicowe. Zaimplementuj „Grę o życie” Conwaya. Do usu wania starych pozycji z tablicy możesz użyć instrukcji Awku d e le te x [ i ] ,
Opisz różne implementacje tablic asocjacyjnych i zanalizuj czas dostę pu i zajętość pamięci każdej implementacji.
2.6. Literatura uzupełniająca Aho, Kernighan i Weinberger zaprojektowali i stworzyli pierwotną wersję języka Awk w roku 1977. (W żadnym razie nie zmieniaj kolejności inicja łów ich nazwisk!) Obecną postać języka i jego smakowite zastosowania opisali w książce The A W K Trogramming Tanguage, którą Addison-Wesley wydało w 1988 roku. Jak użytecznym narzędziem jest ten język, jeśli uży wa się go do eksperymentowania na algorytmach, można się dowiedzieć, czytając rozdz. 7 ich książki; w podobny sposób będziemy używać Awku w rozdz. 3, 13 i 14. Autorzy Awku opisali w rozdz. 6 książki jego zastoso wanie jako procesora dla małych języków; my posłużymy się Awkiem do tego celu w rozdz. 9. Pozostałe rozdziały wspomnianej książki zawierają przewodnik i poradnik oraz przykłady zastosowania Awku do rozwiązy wania zadań z przetwarzania danych, baz danych i przetwarzania tekstów. Książka o Awku jest doskonałym wprowadzeniem do interesującego i po żytecznego języka.
ROZDZIAŁ 3.
WYZNANIA PROGRAMISTY
Większość programistów spędza mnóstwo czasu na testowaniu progra mów i wyszukiwaniu błędów, lecz nie poświęca tym zajęciom zbytniej uwagi podczas pisania programu. W tym rozdziale opiszę, w jaki spo sób testowałem i uruchamiałem kilka trudnych podprogramów; zwró cę też w nim szczególną uwagę na platformę uruchomieniową, której wów czas używałem. Platforma uruchomieniowa pozwala programiście korzy stać z takich składowych systemu, którymi w innym razie nie mógłby się posłużyć. Oprogramowanie platformy uruchomieniowej tworzy się z tymczasowych programów i danych zapewniających programiście do stęp do składowych systemu. Platformy uruchomieniowej nie przekazuje się klientowi, lecz jest ona niezbędna podczas testowania i uruchamiania programów. Tyle o podstawach, teraz przejdźmy do dwóch bolesnych historii. Wygnanie 1. Kilka lat temu musiałem skorzystać z procedury wybo ru w 500-wierszowym programie. Ponieważ wiedziałem, że to trudny problem, skopiowałem 20-wierszowy podprogram z doskonałej książ ki z algorytmami. Mój program zazwyczaj działał poprawnie, ale od czasu do czasu zawodził. Po dwóch dniach uruchamiania wyśledziłem błąd w procedurze wyboru. Podczas większości sesji uruchomienio wych ta procedura pozostawała poza podejrzeniami —na podstawie opisanego w książce nieformalnego dowodu jej poprawności byłem o tym przekonany i kilkakrotnie sprawdzałem, czy mój kod jest na pewno taki sam, jak zamieszczony w książce. Niestety, okazało się później, że występujący w książce znak „ $1=="wstaw"
{ n =
$2; for (i= 1; i
> Poprawność tego kodu można było łatwo wykazać; od razu przeszedł wszystkie testy. W programie korzysta się z „rekurencji końcowej”: ostatnią instruk cją w tej procedurze jest wywołanie rekurencyjne. Taką rekurencjęmożna zawsze przekształcić w iterację, zastępując wywołanie procedury przez przypisanie i instrukcje tworzące pętlę. W następnej wersji procedura rekurencyjna jest zastąpiona przez pętlę w h ile i w ten sposób wracamy do mojego drugiego wyznania. Moim pierwszym błędem było oczywiście to, że zastanawiałem się, czy trzeba testować ten kod. Każdy autor, który tak często myli się, jak ja, powinien albo przetestować program, albo opatrzyć go etykietą: „UWAGA - KOD NIEPRZETESTOWANY”. Drugi błąd znajduje się w samej procedurze wyboru; masz może jakiś pomysł? function swapCi, j,
t) { t = x[i] ; x[i] = x[j] ; x[j] = t }
function selectCk, 1, u, i, m) { 1 = 1; u = n while (1 < u) { print 1, u swap(l, l+int((u-l+l)*rand()))
m= 1 comps = comps + u-1 for (i = 1+1; i k) u = m-1
> y $l=="wstaw" $l=="n" $l=="x" $l=="drukuj"
{ n = $2; for (i = 1; i
{ n = $2 }
{ x[$2] = $3 > { for (i = 1; i $$ = $1 / $3; > { $$ = $2; }
I ’(’ expr ')’
Program Yacc konstruuje analizator składni dla takiego opisu jak powyż szy. Kiedy ten analizator znajdzie napis expr+expr, wtedy przekazuje (jako obiekt $$) sumę pierwszego wyrażenia ($1) i drugiego wyrażenia (które jest trzecim obiektem, $3). Pełna definicja zawiera opisy reguł pierwszeń stwa operatorów (operator * wiąże mocniej niż operator +), operatorów porównania (takich jak < oraz >), funkcji i kilku innych mniej ważnych spraw. Na program Pic można patrzeć jak na ciąg pierwotnych obiektów geometrycznych. Te obiekty pierwotne są zdefiniowane następująco: primitive: BOX attrlist I CIRCLE attrlist I ELLIPSE attrlist I ARC attrlist I LINE attrlist
{ boxgen($l); } { elgen($l); > { elgen($1); >
{ arcgen($l); } i linegen($l); >
Kiedy analizator syntaktyczny napotyka instrukcję e lli p s e , wtedy anali zuje listę atrybutów i następnie wywołuje procedurę elgen . Przesyła do tej procedury pierwszy składnik frazy, leksem ELLIPSE. Procedura elgen używa tego leksemu, aby zdecydować, czy ma generować ogólną elipsę czy też okrąg (specjalny przypadek elipsy, której długość równa się szerokości). Wszystkie pierwotne obiekty geometryczne w języku Pic mają taką samą listę atrybutów; jednak w wypadku niektórych z nichpomija się pewne atrybuty. Lista atrybutów jest albo pusta,albo poprzedzajeden atrybut: attrlist: attrlist attr I /* puste */ t
A oto mała część definicji atrybutu: attr: DIR expr I DIR
{ {
storefattr($1, !DEF, $2); } storefattr($1, DEF, 0.0); >
9.4. MAŁE JĘZYK I DO IMPLEMENTOWANIA JĘ ZY K A PIC
123
I FROM position { storeoattr($1, $2); } I TO position { storeoattr($1, $2); } I AT position { storeoattr($1, $2); J-
>
Po przeanalizowaniu wszystkich atrybutów odpowiednia procedura zacho wuje ich wartości. Jest to elegancka implementacja par nazwa—wartość, które omawialiśmy w p. 4.1. Te narzędzia rozprawiają się z dobrze zbadanymi problemami. W książ ce o kompilatorach, na którą powołuję się w p. 9.7, poświęcono 80 stron analizatorom leksykalnym i 120 stron analizatorom składni. Programy Lex i Yacc są oparte na tych metodach —programista definiuje strukturę lek sykalną i syntaktyczną przy użyciu prostych małych języków, a programy generują wysokiej jakości procesory. Przede wszystkim nie tylko można ła two generować te opisy, lecz równie łatwe jest teżmodyfikowaniejęzyka. Napisany przez Stu Feldmana program Make zajmuje siębardziej pro zaicznym problemem, który jednakże jest trudny i ma wielkie znaczenie w wypadku dużych programów: utrzymywaniem aktualnych wersji plików zawierających kod nagłówkowy, kod źródłowy i kod wynikowy, a także dokumentację, testy, przypadki itd. Oto skrócona wersja pliku, którego Kernighan używa do opisywania plików związanych z językiem Pic: OFILES = picy.o pici.o main.o print.o \ misc.o symtab.o blockgen.o \ CFILES = main.c print.c misc.c symtab.c \ blockgen.o boxgen.c circgen.c \ SRCFILES = picy.y picl.l pic.h $ (CFILES) pic: $ (OFILES) cc $ (OFILES) -lm $(0FILES): pic.h y.tab.h manual: pic manual I eqn I troff -ms >manual.out backup: $(SRCFILES) makefile pictest.a manual push safemachine $? /usr/bwk/pic touch backup bundle: bundle $ (SRCFILES) makefile README
Na początku tego pliku występuje definicja trzech nazw plików: nazwa OFILES oznacza pliki wynikowe, nazwa CFILES oznacza pliki, które zawie rają kod w języku C, natomiast nazwa SRCFILES określa pliki źródłowe w języku C, to jest pliki p ic y .y , zawierające opisy w języku Yacc, pliki
124
9. MAŁE JĘZYKI
p i c i . 1, zawierając opisy w języku Lex, oraz plik nagłówkowy. Następny wiersz oznacza, że procesor Pic musi mieć aktualną wersję plików wyni kowych (wewnętrzne tablice programu Make informują, jak uzyskać pliki wynikowe na podstawie plików źródłowych). Następny wiersz określa, jak trzeba to włączyć w bieżącą wersję języka Pic. Kolejny wiersz informuje, że pliki wynikowe zależą od dwóch nazwanych plików nagłówkowych. Gdy Kernighan napisze polecenie make p ic , wówczas program Make sprawdzi, czy wszystkie pliki wynikowe są bieżące (plik f i l e . o jest bieżący, jeśli jego czas modyfikacji jest późniejszy niż czas modyfikacji pliku f i l e . c ) , po nownie skompiluje nieaktualne moduły i następnie wprowadzi potrzebne moduły z odpowiednimi bibliotekami funkcji. Następne dwa wiersze określają, co się stanie, kiedy Kernighan napisze polecenie make manual: plik zawierający podręcznik użytkownika będzie przetworzony przez program Troff i dwa preprocesory. Polecenie backup zachowuje wszystkie zmienione pliki w s a f emachine, a polecenie bundle zapakuje nazwane pliki do pakietu nadającego się do wysłania. Chociaż pierwotnie program Make specjalnie projektowano z myślą o kompilo waniu, elegancki mechanizm Feldmana zręcznie wykonuje te wszystkie dodatkowe funkcje porządkujące.
9.5. Zasady Małe języki są ważną częścią ogółu popularnych języków czwartej i piątej generacji oraz generatorów programów użytkowych, lecz ich wpływ na technikę komputerową jest szerszy. Małe języki często ułatwiają tworzenie eleganckich interfejsów, którymi posługują się ludzie przy sterowaniu zło żonymi programami, lub interfejsów do modułów oprogramowania, które komunikują się ze sobą w dużym systemie. Chociaż większość przykładów omawianych w tym rozdziale to duże programy-systemy działające w sys temie UNIX, przekonamy się w p. 12.2, że te idee zastosowano w dość prozaicznym systemie przetwarzania danych, który zaprogramowano w ję zyku Basic dla mikrokomputera. Zebrane poniżej zasady projektowania języka są dobrze znane projek tantom dużych języków programowania. Dotyczą one także projektowania małych języków. Cele projektowania. Przed projektowaniem języka starannie przeanalizuj problem, który próbujesz rozwiązać. Czy nie powinno się raczej stworzyć biblioteki procedur lub systemu interaktywnego? Pewna stara, pochodząca
9.5. ZASADY
125
z praktyki zasada powiada, że pierwsze 10 procent wysiłku włożonego w programowanie owocuje 90 procentami efektywności; czy możesz osią gnąć to, co zamierzasz, korzystając z takich języków, jak Awk, Basic lub Snoboł, które tanio zapewnią pierwsze 90 procent, czy też musisz użyć silniejszych narzędzi, takich jak Lex, Yacc i Make, aby zyskać 99,9 procent? Prostota. Niech Twój język będzie możliwie jak najprostszy. Mniejszy język łatwiej daje się projektować, dokumentować i utrzymywać, a także szybciej można się go nauczyć i łatwiej go używać. Podstawom abstrakcje. Typowe języki programowania są zbudowane zgodnie z koncepcją maszyny von Neumana: rozkazy działają na ma łych porcjach danych. Projektant małego języka musi być bardziej twór czy: obiektami podstawowymi mogą być symbole geometryczne, struktu ry chemiczne, języki bezkontekstowe lub pliki w programie. Operacje na obiektach też są bardzo urozmaicone: od syntezy dwóch pierścieni ben zenu do powtórnego kompilowania programu. Identyfikowanie tych klu czowych graczy jest doskonale znane programistom; obiekty podstawowe są abstrakcyjnymi typami danych w programie, operacje są kluczowymi procedurami. Struktura lingwistyczna. Gdy znasz już podstawowe obiekty i operacje, pozostaje jeszcze wiele sposobów zapisania ich wzajemnego oddziały wania. Wyrażenie arytmetyczne w notacji wrostkowej 2+3+4 można za pisać w notacji przyrostkowej jako 234++ lub przy użyciu funkcji jako p lu s ( 2 ,r a z y ( 3 ,4 ) ) ; często trzeba znaleźć kompromis miedzy naturalno ścią a ułatwieniem implementacji. Ale cokolwiek mogłoby się znaleźć lub nie znaleźć w Twoim języku, na pewno trzeba w nim zezwolić na wcięcia i komentarze. Miary projektowania języka. Zamiast prawić kazanie o projektowaniu ze smakiem, wolałem przedstawić przykłady użytecznych języków, które od zwierciedlają dobry smak. A oto kilka rad uwypuklających pożądane wła ściwości języka. Ortogonalność: nie kojarz niezwiązanych ze sobą właściwości. Ogólność: używaj jednej operacji do wielu celów. Oszczędność: usuwaj niepotrzebne operacje. Zupełność: czy język pozwala opisać wszystkie brane pod uwagę obiek ty? Podobieństwo: spraw, by język był możliwie jak najbardziej sugestywny. Rozszerzalność: zapewnij możliwość rozbudowy języka.
126
9. MAŁE JĘZYK I
Proces projektowania. Jak inne duże oprogramowanie, tak i obszerne małe języki rozwija się, zamiast je konstruować. Zacznij od solidnego, prostego projektu, wyrażonego w notacji, takiej jak notacja Backusa-Naura. Przed implementowaniem języka, przetestuj jego projekt, opisując dużą rozma itość obiektów proponowanego języka. Gdy już język jest gotowy i działa, wówczas iteruj projekt, dodając do języka takie właściwości, jakie wynikają z potrzeb jego użytkowników. Spostrzeżenia ruynikające z konstruowania kompilatorów. Tworząc procesor dla małego języka, nie zapominaj o naukach zdobytych przy konstruowaniu kompilatorów. Tak bardzo, jak to możliwe, wyodrębnij analizę leksykalną w części czołowej, oddzielając ją od przetwarzania w de; dzięki temu będzie można łatwiej budować procesor i łatwiej się go przeniesie do nowego systemu lub zastosuje do nowego języka. A kiedy będzie trzeba, wtedy użyj narzędzi do budowania kompilatorów, takich jak Lex, Yacc i Make.
9.6. Zadania 1. Większość systemów zawiera pakiety oprogramowania przeznaczone go do sortowania plików; interfejs do nich jest zazwyczaj małym ję zykiem. Oceń język, z którego możesz korzystać w Twoim systemie. Na przykład sortowanie w systemie UNIX wywołuje się jednym po leceniem, takim jak poniższe: sort - t : +3n
Wiersz ten oznacza: użyj znaku V jako separatora plików i sortuj plik, tak aby czwarte pole (omiń trzy pierwsze pola) wystąpiło w kolejności numerycznej. Zaprojektuj mniej enigmatyczny język i zaimplementuj go, być może jako preprocesor, który generuje polecenia dla Twojej systemowej procedury sortowania. 2. Procesor Lex używa małego języka dla wyrażeń regularnych w celu opisywania analizatora leksykalnego. Jakie inne programy w Twoim systemie posługują się wyrażeniami regularnymi? Jak się różnią i dla czego? 3. Zbadaj przykłady różnych języków służących do opisywania informa cji bibliograficznych. Jak języki różnią się między sobą w zależności od zastosowań, takich jak systemy wyszukiwania dokumentów i progra
127
9.6. ZADANIA
my bibliograficzne w systemach produkowania dokumentacji? Jakich małych języków użyto do obsługi zapytań w każdym z tych systemów? 4. Zbadaj, które języki mogłyby być najmniejsze ze wszystkich: asemble rowe, opisywania formatów czy obsługi stosu? 5. Wielu ludzi potrafi postrzegać obraz trójwymiarowy, krzyżując wzrok i zlewając dwie połówki stereogramów:
Po przeprowadzeniu sondażu doszedłem do wniosku, że mniej więcej co drugi czytelnik tego rozdziału będzie mógł postrzegać te trójwy miarowe sceny; reszta nabawi się bólu głowy podczas takich prób.
1 \
✓
..... ..............’j
k..
f
/
s
V
(C Jr (C Jr >
f
,
i u i
^
C_____________________a
Te obrazki narysował program napisany w 40 wierszach języka Pic. Zaprojektuj i zaimplementuj język trójwymiarowy do opisywania ste reogramów. 6. Programuj i implementuj małe języki. Do interesujących obszarów za stosowań obrazowania należą: diagramy elektryczne, struktury danych, takie jak tablice, drzewa, grafy (szczególnie ciekawe jest rysowanie au tomatów skończenie-stanowych, takich jak omawiane w p. 2.2) oraz obrazowe przedstawianie wyników gier, takich jak kręgle i baseball.
128
9. MAŁE JĘZYKI
Innym interesującym obszarem zastosowań jest zapisywanie muzycz nych partytur. Rozważ zarówno interpretację nut na kartce papieru, jak i odegranie ich na generatorze muzycznym. 7. Zaprojektuj mały język do obsługi popularnych formularzy w Twojej firmie, takich jak sprawozdanie z kosztów podróży. 8. Jak procesory małych języków mogą reagować na błędy lingwistycz ne? (Rozważ opcje dostępne dla kompilatorów dużych języków). Jak konkretne procesory reagują na błędy?
9.7. Literatura uzupełniająca Być może nigdy nie słyszał(a)eś o książce Compilers: Principles, Techniques, and Tools, którą napisali Aho, Sethi i Ullman, lecz przypuszczalnie rozpozna jesz okładkę „New Dragon Book” (opublikowaną przez Addison-Wesley w 1986 roku). I mo^es^ ocenić tę książkę po jej okładce: to doskonałe wprowadzenie do zagadnień związanych z kompilatorami, w którym po łożono zdrowy nacisk na małe języki. Ponadto w tej książce w znacznym stopniu korzystano z języka Pic, przedstawiając opisywaną historię w obra zach. (Większość zamieszczonych w tym rozdziale rysunków dotyczących kompilatora była zainspirowana obrazami występującymi w tamtej książce). Kernighan i Pike opisali studium przypadku małego języka w rozdz. 8 książki The UNIX Programming Environment. Zaczynają od małego języka do obliczania wartości wyrażeń, następnie dodają do niego zmienne i funkcje, a na końcu dołączają konstrukcje sterujące i funkcje zdefiniowane przez użytkownika, aby uzyskać język, w którym można wiele wyrazić. Przez cały czas używają opisanych w tym rozdziale narzędzi systemu UNIX do projektowania, rozwijania i dokumentowania ich języka. W rozdziale 6 książki AWTC Programming Eanguage, wspomnianej w p. 2.6, opisano, jak Awk pozwala z łatwością przetwarzać bardzo małe języki.
ROZDZIAŁ 10.
PROJEKTOWANIE DOKUMENTU
PRZEZ DŁUGI CZAS WYDRUKI KOMPUTEROWE WYGLĄDAŁY TAK JAK TO. Czas p ły n ą ł, d ru k a rk i m iały małe i duże l i t e r y oraz zn aki s p e c ja ln e !*#&%?! Następnie pojawiły się małe, powolne drukarki rozetkowe, które drukowały tak pięknie, że nazywano to „drukiem maszyno wym”. Drukarki miały nowe znaki, umożliwiające na przykład drukowanie kursywą, a także pozwalały korzystać z innych subtelności typograficz nych, takich jak inde^y. Drukarki mechaniczne zachowują obrazy liter w kawałkach metalu; drukarki laserowe przechowują kształty liter jako bity (w końcu coś, z czym my, programiści, potrafimy sobie poradzić!). Toteż drukarki laserowe mają zazwyczaj wielką rozmaitość fontów, J e d n y c h bardziej egzotycznych od drugich i dają możliwość powiększania lub zmniejszania druku. Pierwsze drukarki laserowe były bardzo drogie i ogromne, lecz dzięki postępowi technicznemu obniżył się ich koszt do kilku tysięcy dolarów i zmalał też ich rozmiar, tak że zajmowały niewielką powierzchnię na biurku. Systemy tworzenia dokumentów, takie jak Scribe, Tj?X i Troff, umożliwiały programistom korzystanie z tych urządzeń. Te udogodnie nia jeszcze bardziej rozprzestrzeniły się dzięki komputerom osobistym. Prasa popularna rozpropagowała rewolucję, która się dokonała w „małej poligrafii”. Ponieważ programiści są stałymi ekspertami w obrębie wszyst kich narzędzi komputerowych, więc ostatnio wielu z nich przeobraziło się w amatorskich składaczy druku, posługując się o wiele lepszymi na rzędziami od tych, których zaledwie dziesięć lat temu używali zawodowi składacze. To zaś pociągnęło za sobą wiele złych i dobrych konsekwencji. Dobre skutki są oczywiste: my, programiści, możemy używać tych potężnych na rzędzi do tworzenia dokumentów, które są przyjemne dla oka i łatwe do
130
10. PROJEKTOWANIE DOKUMENTU
czytania. Obecnie wielu programistów rutynowo tworzy skład do druku dokumentacji programu, notatek do wykładów, sprawozdań technicznych i artykułów zamieszczanych w materiałach z konferencji. Niestety, złe skut ki są czasami jeszcze bardziej oczywiste: większość programistów niewiele poświęca uwagi projektowaniu dokumentu, a «• “ potężne narzędzia mogą być czasami potężnie NADUŻYWANE“»)!!! Jak większość programistów, nie uczyłem się projektowania książek. Pierwszą wersję tego rozdziału napisałem wkrótce po tym, jak w 1986 ro ku złożyłem moją książkę Perełki oprogramowania. W trakcie tego przedsię wzięcia nauczyłem się wielu rzeczy od zawodowych projektantów książek w Addison-Wesley i od kilku kolegów z Bell Labs, którzy w tym czasie pisali i składali książki. W tym rozdziale próbuję przekazać kilka z wielu ważniejszych lekcji, jakich nauczyłem się podczas tego ćwiczenia. W poprzednim rozdziale omawiałem pewne małe języki przeznaczone do sterowania programami typograficznymi. W tym przechodzę od me chanizmu tworzenia dokumentu do wyglądu produkowanych przez nas dokumentów. Rozdział ten jest skierowany do programistów, którzy pro jektują i składają do druku własne dokumenty. Programistów, którzy nie zajmują się składaniem tekstów do druku, mogą zainteresować fragmenty tego rozdziału, ponieważ w dokumentach znajdują zastosowanie ogólne zasady projektowania, dotyczące oprogramowania. Poniższy podrozdział zawiera szczegółowe omówienie jednej drobnej dziedziny: tabel typograficznych. W następnym przedstawiam trzy zasady projektowania tych tabel oraz innej typografii. W dwóch kolejnych pod rozdziałach zajmuję się opracowywaniem rysunków i tekstu, a w jeszcze następnym rozważam kwestie wyboru właściwego środka wyrazu dla pre zentowanej koncepcji.
10.1. Tabele Można opisywać rozmiary i zaludnienie różnych kontynentów ciągiem ta kich zdań, jak: „Azja zajmuje obszar 44 400 000 kilometrów kwadratowych, co stanowi 29,8% powierzchni lądów na Ziemi; jej ludność wynosi 2 297 milionów, czyli 59,8% ludności na świecie”. Taka informacja staje się bar dziej komunikatywna, gdy umieścimy ją w tabeli, którą łatwo utworzyć za pomocą wielu systemów produkowania dokumentów. Jak w wypad ku wszystkich zamieszczonych w tej książce tabeli, tak i do utworzenia tej tabeli posłużył program Tbl Mike’a Leska. (Tbl jest małym językiem
131
10.1. TABELE
służącym do opisywania tabel; dotychczas implementowano go innym pre procesorem dla języka Troff). Kontynent
Powierzchnia
Azja Afryka Ameryka Północna Ameryka Południowa Antarktyda Europa Australia
44400000 30 300000 24200000 17 800000 13200000 10 500000 8600000
% Lądów 29,8 20,3 16,3 12,0 8,8 7,1 5,7
Ludność
% Ogółu ludności
2897000000 551000000 400000000 271000000 0 702 000000 16000000
59,8 11,4 8,3 5,6 0 14,5 0,3
Resztę tego podrozdziału przeznaczam na przećwiczenie projektowa nia tabeli. Pozostawimy stałe liczby i nazwy kontynentów, które stanowią treść tej tabeli, lecz będziemy zmieniać inne parametry projektu w trzech dodatkowych tabelach. W następnej wersji tej tabeli użyjemy kroju pisma zwanego helwetiką1, wprowadzimy bardziej opisowe nagłówki kolumn, wypośrodkujemy napisy w kolumnach i dodamy linie poprzeczne (zwane linijkami). Ponieważ liczby są wyrażone w naturalniejszych jednostkach, więc następna tabela jest węższa, tak że może się zmieścić w jednej ko lumnie Communications o f the A CM (kiedy poprzednia tabela ukazała się najpierw w tym czasopiśmie, wtedy trzeba było ją rozciągnąć na dwie kolumny; jak w programowaniu, przestrzeń typograficzna jest często bez płatna, lecz czasami drogo kosztuje). Kontynent Azja Afryka Ameryka Północna Ameryka Południowa Antarktyda Europa Australia
Powierzchnia Min km kw. % 29,8 44,400 20,3 30,300 16,3 24,200 12,0 17,800 8,8 13,200 10,500 7,1 5,7 8,600
Ludność Min % 2897 59,8 551 11,4 400 8,3 271 5,6 0 0 702 14,5 16 0,3
Linijki ułatwiają czytelnikowi wodzenie wzrokiem po tabeli, ale po wyższa tabela ma zbyt wiele dobrych rzeczy. W następnej tabeli będziemy 1 Przyjęty styl czasopisma Communications o f the ACM, w którym ten rozdział pojawił się pierwotnie jako felieton, narzuca, że wszystkie tabele są drukowane helwetiką. Jednym z powodów jest to, że tabele są składane małą czcionką (8-punktową), a drobny druk czyta się łatwiej, gdy jest złożony helwetiką.
132
10. PROJEKTOWANIE DOKUMENTU
mieć mniej linijek, lecz niektóre z nich podkreślimy podwójnie, aby zwrócić uwagę na ważne informacje. Centralne umieszczanie nazw jest przesadne, więc dosuniemy je do lewego brzegu kolumny. Zastosujemy drobniejszą czcionkę (9 punktów zamiast 10), a odstępy w pionie zmniejszymy z 12 punktów do 11. Nagłówki kolumn przeredagujemy (jak w innych tabelach tej serii) oraz podkreślimy je, używając czcionki półgrubej. K ontynent
P ow ierzchnia
Ludność
106
%
km kw.
Pow. lądów
Azja
44,400
29,8
2897
Afryka
30,300
20,3
551
59,8 11,4
Ameryka Północna
24,200
16,3
Ameryka Południowa
17,800
12,0
400 271
8,3 5,6
Antarktyda
13,200 10,500
8,8
0 702
14,5
16
0,3
Europa Australia
7,1 5,7
8,600
%
M iliony
O g ó łu lud.
0
Następna wersja tej tabeli jest przedstawiona w mojej ulubionej posta ci. Powstała pod wpływem wskazówek zawartych w rozdz. 12 książki The Chicago Manuał o f Style, cytowanej w p. 10.8. Zastosowałem w niej moż liwie jak najmniejszą liczbę linijek. Tylko górna linia jest podwójna, aby tabela odcinała się od tekstu, lecz czcionka półgruba jest zbyt agresyw na. Dlatego w następnej tabeli główne nagłówki kolumn są wydrukowane M a ł y m i K a p it a l i k a m i . Z podobnych powodów podstawowy krój pisma z powrotem zmieniamy na antykwę zamiast helwetiki.
P o w ie r z c h n ia K ontynent
Azja Afryka Ameryka Północna Ameryka Południowa Antarktyda Europa Australia
L u d n o ść
Miliony km kw.
Procent
Miliony
44,400 30,300 24,200 17,800 13,200 10,500 8,600
29,8 20,3 16,3 12,0 8,8
2897 551 400 271 0 702 16
7 ,1
5,7
Procent 59,8 11,4 8,3 5,6 0 14,5 0,3
Chociaż te cztery tabele zawierają te same dane, ich wygląd jest całkiem odmienny. To, która postać tabeli jest najlepsza dla danego dokumentu, zależy od pewnej liczby czynników, począwszy od możliwości systemu
10.2. TRZY ZASADY PROJEKTOWANIA
133
produkującego dokumenty (co można łatwo lub w ogóle zrobić?), a skoń czywszy na tym, w jakim celu tworzy się dokument (reklama musi się rzucać w oczy i przykuwać wzrok, podczas gdy podręcznik powinien być wygodny w użyciu). Omawiając powierzchowny wygląd tabel, zaniedbaliśmy wiele podsta wowych kwestii dotyczących ich projektowania. Ogólna postać wszystkich czterech tabel jest do przyjęcia; mogłoby nawet być gorzej (na przykład w wyniku wymiany wierszy i kolumn albo zmiany kolejności niektórych wierszy lub kolumn). Projektowanie układu tabel mających bardziej skom plikowaną strukturę może być ambitnym zadaniem. Wszelkie tabele są jednak zawodne ze względu na opisywanie danych. Skąd się wzięły? Jak i kiedy zbierano liczby? Dobre tabele mówią: kto, co, gdzie, kiedy, dla czego i jak. W tych rozważaniach całkowicie pominęliśmy najważniejszy aspekt tabeli: co znaczą dane liczby? Co z nimi robimy? Tak ważne kwestie wykraczają jednak poza temat tego rozdziału.
10.2. Trzy zasady projektowania Ale zaczekajmy chwilkę! Ten esej był przeznaczony dla programistów, a my wiemy, co prawdziwi programiści czują wobec wszelkiego rodzaju doku mentacji: odwalić to wszystko razem tak szybko, jak się da, aby można było powrócić do radochy programowania. Nie próbuję przekonywać za gorzałych „narkoderów” (ang. hard-core code junkies), że dokumentacja jest ważna, lecz myślę, że nawet oni mogliby czegoś nauczyć się z projekto wania dokumentów. Każdy powinien przeczytać klasyczną książeczkę Strunka i W hite’a The Elements o f Style, której trzecie wydanie opublikował Macmillan w 1979 ro ku. Co Strunk i White robią dla tekstów w języku angielskim, to Kernighan i Plauger robią dla programów w książce The Elements o f Trogramming Style (wydanie drugie, McGraw-Hill, 1978 r.). Niektóre z ogłoszonych przez nich zasad stosują się też do projektowania dokumentów. Oto trzy funda mentalne zasady tworzenia lepszych tekstów, programów lub dokumentów. Iteracja. Strunk i White radzą autorom: „poprawiaj i przepisuj”. Dobrzy programiści znają to od dawna; książka The Elements o f Trogramming Style Kernighana i Plaugera jest zbudowana wokół poprawiania programów po chodzących z podręczników. Utworzenie na podstawie pierwszej tabeli jej ostatniej wersji wymagało sporo pracy, lecz atrakcyjny dokument wart jest czasami włożonego wysiłku.
134
10. PROJEKTOWANIE DOKUMENTU
Konsekmnc/a. Można całe życie poprawiać i przepisywać jeden doku ment. Strunk i White unikają tego, doradzając nam: „wybierz odpowied ni projekt i trzymaj się go”. Projekty niektórych programistów powstają pod wpływem ogólnofirmowego standardu kodowania. Dobry standard to przyjemność, a słaby standard jest często lepszy niż żaden. Ekspery mentuj, aby znaleźć najlepszy styl dla konkretnego rodzaju dokumentów i później się go trzymaj. Minimalista. Ponieważ „pełne wigoru pisanie jest zwięzłe”, Strunk i White mówią nam: „nie używaj niepotrzebnych słów”. Programy so lidne, wydajne i nadające się do utrzymywania są również zwięzłe; dobrzy programiści nie używają niepotrzebnych wierszy, zmiennych i procedur. Słyszałem kiedyś o programiście, którego chwalono za to, że „dodaje funkcje, usuwając kod”. Staraj się możliwie jak najwięcej usuwać z do kumentów takich rzeczy, jak %będne zmiany czcionki i nadmiar linijek, nie zubożając treści informacji.
10.3. Rysunki
26 26 31 31 32 38 38 41 43 46 50 53
r
Eh 00
Zastosujmy te zasady do składania rysunków. Zaczniemy od przeszukiwa nia dwójkowego posortowanej tablicy, opisanego w p. 3.1. Na rysunku 1 przedstawiono wyszukiwanie dwójkowe liczby 50 w 16-elementowej tabli cy: w pierwszym badaniu porównuje się liczbę 50 ze środkowym (ósmym) elementem (41), w drugim —z dwunastym i tak dalej, aż za czwartym ra zem znajduje się liczbę 50 na jedenastej pozycji. Wielkość, rozmieszczenie, czcionka i legenda są typowe dla wielu obrazów tworzonych w kompute rach osobistych.
59 79 97
m
Rysunek 1. W yszukiwanie dwójkowe w tablicy
Kilka eksperymentów dało następną wersję rysunku, w której użyto cieńszych linii oraz zmniejszono klatki, tak że tekst nie wyszedł poza margines. Zmienia się też długość strzałek wskazujących na badania, tak że strzałki są coraz krótsze w miarę zbliżania się do celu.
135
26 26 31 31 32
W 00
10.3. RYSUNKI
38 41 43 46 50 53 58 59 79 97
Usuwając brzydki podpis, oszczędzamy miejsce, a umieszczając rysu nek wewnątrz danego ustępu, oszczędzamy czytelnikowi wysiłku szukania numeru rysunku. Na stronie 391 w numerze Communications o f the A CM z maja 1986 roku opisano system zapisywania nut, zamieszczając taki rysunek w polu o powierzchni około 6 X 6,5 cala:
Użytkownikp •
U żytkow nik p -
Zmniejszając kilka figur geometrycznych i obracając wykres, tak aby moż na było umieścić go poprzecznie zamiast pionowo, uzyskano taki sam efekt na około jednej dziesiątej powierzchni w stosunku do oryginału (przestrzeń w tym czasopiśmie kosztuje). Kurczenie rysunków nie tylko oszczędza miejsce, lecz powoduje, że produkt końcowy wygląda bardziej profesjonalnie niż jego większy pierwowzór. Przećwicz to. Następną omawianą klasą rysunków są wykresy, powszechnie występu jące w pracach technicznych. Na przykład na poniższym wykresie przed stawiono drogę poprowadzoną poprzez pole kierunkowe, która jest roz wiązaniem równania różniczkowego.
136
10. PROJEKTOWANIE DOKUMENTU
0
0,1 0,2 0,3 0,4 0,5 0,6 0,7 0,8 0,9
1
i I i 1 i 1 i I i I i I i I i I i I i _ j -
z*'
s
-
J —► J
u
s
—»
/ / ^ / z ' s / / " — *» r S / / / " > T~ t t f t t i |i | i | i | i |i | i ^
0
/ / / / f t |i
/ / / / t t t t | i
— 0,9
/ / / / / / t t t | i
0,1 0,2 0,3 0,4 0,5 0,6 0,7 0,8 0,9
-
0,8
-0 ,7 -
0,6
- 0 ,5 -0 ,4 -0 ,3 -0 ,2 -
0,1
0 1
Rozwiązanie równania różniczkowego Na drugiej wersji tego rysunku przedstawiono te same dane z kilko ma zmianami. Wprowadzono więcej oznaczeń informacyjnych, na osiach współrzędnych zaznaczono mniej punktów podziału (cztery punkty w tej wersji mówią nam tyle samo co 84 w wersji pierwotnej) i cały wykres zmniejszono. Agresywne strzałki zastąpiono delikatniejszymi kreskami. 1
j = V ( 2 x 3 + l)/ 3
0
0 1 Pole kierunkowe j ' = x2/j
Wiele programów tworzenia obrazów pozwala korzystać z wyszuka nych wzorców cieniowania, które często przyciągają uwagę i są czasami bardzo sugestywne. Zbyt często jednak zaciemniają informację i mogą wy woływać zawroty głowy. Niektóre systemy produkują kolorowe rysunki, które mogą być niezwykle wyraziste (jeśli się z tym nie zgadzasz, spróbuj
10.4. TEKST
137
czytać czarno-białe reprodukcje kolorowych map drogowych). Ale odtwa rzanie koloru jest prawie zawsze kosztowne i bywa czasami nadużywane. Ostrożnie korzystaj z tych środków wyrazu. Grubość linii na rysunku może zmienić jego charakter. Oto graf prze pływu narysowany trzema liniami różnej grubości:
Cienkie linie znikają, podczas gdy grube wyglądają niezręcznie. Dąż do uzyskania równowagi. Przedstawiłem w p. 10.2 trzy zasady projektowania: iterację, konse kwencję i minimalizm. Na podstawie omawianych w tym podrozdziale pierwotnych i końcowych wersji obrazów można uznać, że iteracja jest równie ważna przy tworzeniu obrazów, jak w wypadku wszelkiego ro dzaju projektowania. Oto kilka minimalistycznych wskazówek, do których staram się konsekwentnie stosować, projektując rysunki. Twórz małe rysunki, lecz dostatecznie duże, aby można je było wy godnie czytać. Umieszczaj rysunki blisko tekstu, którego treści ilustrują. Kiedy to jest możliwe, łącz rysunki z tekstem oraz unikaj podpisów i numerowania rysunków. Oszczędnie używaj koloru i cieniowania tła. Stosuj delikatną kreskę.
10.4. Tekst Tabele i rysunki to świetna, stosowana od czasu do czasu przyprawa, lecz mięso i warzywa każdego dokumentu to tekst: akapity składają się ze zdań zbudowanych ze słów. Oto kilka wskazówek na temat składania tekstu, często niedocenianych przez programistów piszących dokumenty. Zmianyfontów i wielkości druku. Wiesz, jaki jest temat tego akapitu, ponie waż jego pierwsze słowa są wydrukowane kursywą. Taki krój pisma dobrze
138
10. PROJEKTOWANIE DOKUMENTU
nadaje się do wyróżniania definicji terminów technicznych i przedstawiania zmiennych matematycznych (jak x , y , ^). Zmiana wielkości druku też jest czasami pożyteczna2. Strzeż się pokusy nadużywania tych środków wyra zu: trudno wodzi się wzrokiem po stronie, na której roi się od różnych krojów pisma i zmian wielkości druku. Wyliczenia. Tekst nie zawsze bywa ujęty zgrabnie w akapity; istnieje wiele innych sposobów przekazywania słów. 1. Tworząc ciąg podobnych myśli, staraj się zapisać je jako ciąg akapitów z wcięciami. 2. Ten mechanizm przyciąga uwagę czytelnika do podobieństw i różnic występujących w kolejnych kwestiach. 3. Nie stosuj nadmiernych ozdobników. Liczby w tym przykładzie są bezużyteczne. Można było je zastąpić kropkami lub, jeszcze lepiej, niczym. To wyliczenie jest nadużyciem; równie dobrze można było te treści ująć w jeden akapit. Wyliczenie na końcu p. 10.3 jest właściwsze. Odstępy. Używaj odstępów w celu oddzielania składowych dokumen tu: akapitów, pozycji w wyliczeniach, rysunków lub tablic. Umiejętne roz mieszczenie odstępów w dokumencie jest tak samo ważne, jak wykorzysta nie ciszy w trakcie opowiadania na głos jakiejś historii. Za mało odstępów w tekście to jakhistoriabezanijednejpauzy, podczas gdy stosowanie zbyt wielu odstępów jest, ech, taak, eee, hmm, no wiesz, nieznośne. Format strony. To pierwsza rzecz, którą postrzega czytelnik Twego do kumentu. Tytuły rozdziałów i podrozdziałów powinny ukazywać wyraźny zarys dokumentu, lecz nie powinny być przytłaczające, aby nie rozbijały dokumentu na nieskładne części. To samo dotyczy podpisów pod ry sunkami i programami, nagłówków w tabelach itp. Nagłówki powinny informować czytelnika, zamiast zaśmiecać informację. Rozplanowanie strony. Gdy masz już sformułowaną treść i ustalony for mat strony, wówczas ostatnim krokiem jest skompletowanie dokumentu. Staraj się umieszczać tabele i rysunki blisko opisującego je tekstu; jeśli nie można umieścić rysunku na tej samej stronie, ta raczej spróbuj dać go na stronie sąsiedniej, niż na odwrocie danej strony. Do innych subtelności 2 Przypisy są składane drobnym drukiem, ale nadal czytelnym. Ich nawiasowy charakter odbija się na zmniejszonym rozmiarze strony. Drobny druk stosuje się też często w dłu gich cytatach, rozwiązaniach zadań, spisach bibliografii i innych tekstach pomocniczych.
10.4. TEKST
139
należy wyrównanie stron, tak aby strony sąsiednie miały taką samą dłu gość, oraz usuwanie tzw. „wdów” i „sierot” (pojedynczych słów w ostatnim wierszu akapitu lub pojedynczego wiersza na początku strony) oraz „rzek” (ciągów odstępów ustawiających się w kolumny w środku tekstu). Proces publikacji. Zanim artykuł ukaże się na stronach typowego cza sopisma, zajmuje się nim wiele osób. Redaktor techniczny i recenzenci analizują pracę autora od strony technicznej i często ją ulepszają. Redak tor merytoryczny poprawia styl pisania, starając się go dopasować do stylu przyjętego w wydawnictwie, drukarz składa czcionki, przygotowując ko rektę. Jednocześnie zawodowy grafik wykonuje rysunki. Po sprawdzeniu artykułów przez redaktora, są one rozkładane do postaci, w której wy konuje się korektę stron. Samotnemu programiście brakuje doświadczenia i wyczucia tych wyszkolonych pracowników, lecz ma przewagę, mogąc rozważać problem z wielu punktów widzenia (jeżeli rysunek nie mieści się na stronie, to programista może przenieść go na następną stronę, trochę go ścieśnić, inaczej rozmieścić tekst otaczający rysunek itd.). Elastycznie korzystaj z tej przewagi, lecz starannie rozważaj każdą radę, którą możesz otrzymać od profesjonalistów. Logiczna struktura tekstu. Po ukazaniu się tego eseju w Communications o f the ACM Leslie Lamport z Digital Equipment Corporation3 przesłał mi taką wiadomość pocztą elektroniczną: „Autor powinien bardziej skon centrować się na strukturze logicznej tekstu niż na jego wizualnej pre zentacji. Miłą cechą tych wymienionych z nazwy systemów służących do opracowywania składu tekstów do druku jest to, że pozwalają ich użyt kownikowi posługiwać się definiowanymi poleceniami. Na przykład autor książki kucharskiej może zdefiniować struktury logiczne, takie jak Przepis, Spis składników i Etap przygotowania. Autor może łatwo zmieniać forma towanie tych struktur, ponownie definiując te polecenia. (Poważną wadą wielu systemów tworzenia składu tekstów do druku jest to, że zachęcają ich użytkownika, aby tego nie robił, lecz w zamian dodał polecenia, takie jak dodaj odstęp w pionie i lista dwukolumnowa^’’. Dalej Lamport pisze: „Pewne zalety takiego podejścia są oczywiste. Na przykład zmiana formatowania artykułu zgodnego z formatem jednego 3 Leslie Lamport jest autorem oprogramowania LATpX, zbioru makroinstrukcji, które tworzą bardziej strukturalny interfejs do systemu składania tekstów, TęX, Knutha. Lam port przedstawił swoją koncepcję w przypisie „Document production: visual or logical” do artykułu pt. „Mathematical text processing”, który ukazał się w numerze z czerwca 1987 roku czasopisma Notices o f the American Mathematical Society, s. 621—624.
140
10. PROJEKTOWANIE DOKUMENTU
czasopisma na format drugiego czasopisma jest szybka i prawie bezbolesna. Mniej oczywiste jest, jak to wpływa na ulepszenie pisania, zmuszając autora do zdawania sobie sprawy ze struktury dokumentu (lub jej braku)”.
10.5. Właściwy środek wyrazu Dotychczas koncentrowaliśmy się w tym rozdziale na ulepszaniu danego rodzaju prezentacji. W pewnym istotnym sensie takie typograficzne szlifo wanie jest z natury powierzchowne. Ładna typografia nie ochroni artykułu przed fatalną ortografią, błędami gramatycznymi, kiepską organizacją czy niedostatkiem treści. Teraz zajmiemy się tym, jak w zasadniczy sposób typografia wpływa na przejrzystość artykułu. Wiele pomysłów można re prezentować za pomocą rozmaitych środków, takich jak równania, rysunki lub tabele; nowoczesne systemy produkowania dokumentów dają progra mistom wielką swobodę wyboru najlepszej formy wyrazu myśli. Przy wyborze właściwego środka wyrazu my, programiści, mamy po dwójną przewagę nad zawodowymi typografami: Szybkość. Gdy redaktor chce poeksperymentować ze zmianami styli stycznymi w tekście, wówczas musi wysłać polecenie do drukarki (często w innym mieście), a następnie poczekać, aż drukarka wykona zlecone zadanie i odeśle mu wyniki. To wszystko wymaga dni, podczas gdy pro gramistom może to zająć kilka minut. Elastyczność. Redaktor musi na początku tej zabawy wybrać ostateczną formę przedstawienia koncepcji, następnie zlecić pracę ekspertowi w danej dziedzinie (takiemu jak zawodowy grafik). Wielu programistów ma narzę dzia, które pozwalają im eksperymentować z różnymi środkami wyrazu. W pozostałej treści tego podrozdziału będziemy eksperymentować z różnymi formami przekazywania myśli. Stare książki do geometrii zawierają takie zdania, jak: „Kwadrat przeciwprostokątnej trójkąta prostokątnego jest równy sumie kwadratów dwóch przyprostokątnych”. Tę wiadomość możemy przekazać, zestawiając rysunek
b z równaniem a2 = b2 + c2.
141
10.5. W ŁAŚCIW Y ŚRODEK W YRAZU
Istnieje wiele sposobów dowodzenia twierdzenia Pitagorasa. Możemy skorzystać z notacji Euklidesa występującej w klasycznych tekstach o geo metrii (typograficzne wyzwanie, lecz nie wydaje się nie do pokonania), możemy też zastosować podejście bardziej algebraiczne lub przedstawić taki rysunek: c
c
W obu kwadratach mamy te same cztery trójkąty (których łączne pole po wierzchni wynosi 2bc). Pozostałe pola powierzchni wynoszą odpowiednio a2 w kwadracie po lewej stronie rysunku i b2 + c 2 w kwadracie po jego prawej stronie. Jednak nie zawsze rysunki powodują zaoszczędzenie powierzchni. Stanley Rice w książce Book Design - Systematic Aspects (Bowker, 1978 r.) poświęca całą stronę 97 na wykres przedstawiający stosunek liczby znaków w rękopisie do liczby stron w końcowej wersji książki. Mógłby zastąpić tę stronę równaniem p = 0,318r, w którym p jest liczbą stron, c zaś liczbą tysięcy znaków, albo jednym zdaniem „3145 znaków na stronę”. W takich okolicznościach najlepszy wybór silnie zależy od kosztu dodatkowej stro ny i wygody czytelników książki zawierającej wykresy w odróżnieniu od równań. W podrozdziale 1.3 omawialiśmy profilowany potok w systemie UNIX, służący do znajdowania najczęstszych wyrazów w dokumencie; podobny potok wystąpi jeszcze w p. 11.1. Sercem tego potoku jest popularny idiom w języku Shell systemu UNIX: sort
I uniq -c I sort -rn
Pierwsze polecenie so rt sortuje dokument, skupiając wszystkie identyczne wyrazy, program u n ią usuwa powtórzenia i poprzedza każdy wyraz liczni kiem jego wystąpień (opcja -c), drugie polecenie s o rt ustawia wszystkie wyrazy w kolejności malejącej ich liczników (-rn oznacza odwrotny po rządek sortowania i porównywanie liczb). Oto ten potok zilustrowany na przykładzie strumienia wejściowego “this is this and that is not this”:
142
10. PROJEKTOWANIE DOKUMENTU
this
and
!f . this ,
is
., .
!S
sort
> Jat
1S not this
la n d „. 2 is
. unią
-c
,.S this this
3this „. 2 is
1 not sort -rn
1 that
lthat ----------- > lnot 3 this
la n d
W rozdziale 15 omawiam algorytm wyboru K-tego z kolei najmniej szego elementu zbioru. Korzysta się w nim z procedury podziału tabli cy X [ L . . U]; niezmiennik pętli można formalnie przedstawić za pomocą równania: X[L] = T
A
V
X[ j ] < T
A
V
X[ j ] > T
Można by użyć popularnego zapisu skrótowego dla tablic, aby wyrazić te same fakty jako komentarz w programie: X[L] = T oraz X[L+1..M]
< T oraz X [M+l. .1-1] >= T
Ale uważam, że w najbardziej przejrzysty sposób można przekazać tę myśl za pośrednictwem takiego rysunku tej tablicy: T
< T
L
p
> T M
I
U
Rozwiązania 3.1, 3.2 i 3.3 dotyczą struktury danych w postaci kopca i algorytmu sortowania przez kopcowanie, które zależą od tego, czy tablica X[L. .U] ma własność H eap(L, U), zdefiniowaną matematycznie wzorem V X [zd iv2] < X[i] 2L^i^U Oto tablica, w której wszystkie podtablice mają tę własność: |12 20 15 29 23 1
17 22 35 40 26 51
19 | 12
W algorytmie sortowania przez kopcowanie (ang. Heapsort) traktuje się tablicę jak drzewo binarne, w którym lewym potomkiem węzła X[7] jest węzeł X [2J], a prawym potomkiem —węzeł X[ 2I + 1]:
143
10.6. ZASADY
12 20
15
/ \ 29
/\
/ \ 23
/\
17
/
22
35 40 26 51 19
To drzewo binarne jest kopcem, ponieważ każdy węzeł ma wartość mniejszą niż wartość jego (zero, jednego czy dwóch) potomków. Zada nie 2 zawiera dodatkowe pytania na temat algorytmu sortowania przez kopcowanie. W rozdziale 2 przedstawiłem hipotezę Whorfa, który powiada, że „Język człowieka nadaje kształt jego myśli”. Przez długi czas kształt mo ich dokumentów zależał od systemu, w którym je pisałem. Był on prze znaczony tylko do pisania tekstu, więc ciężko pracowałem nad tym, aby moje pomysły wcisnąć w słowa. Teraz korzystam z systemu, który pozwala kształtować treść dokumentów: pomysły można wyrazić za pomocą tekstu, równań, tabel, rysunków, programów, wykresów i wielu innych środków. To oprogramowanie pobudza mnie do tego, abym był lepszym autorem. A utworzone dokumenty, chociaż zapełnione różnego rodzaju ilustra cjami, odnoszą się delikatnie do czytelnika. W naturalny sposób opis prze chodzi w odpowiednich miejscach od tekstu do rysunków, równań lub programów, wszystko zaś jest podporządkowane wewnętrznej logice oma wianego tematu. W niektórych czasopismach wymaga się, by wszystkie rysunki były umieszczone na końcu artykułu, co wielkim kosztem czytel ności ułatwia pracę wydawnictwu. Wyobraźmy sobie czytanie pracy mate matycznej, w której wszystkie równania byłyby ponumerowane, opatrzone podpisami i zgromadzone na końcu. Albo tekst programu, w którym cały kod znalazłby się w dodatku 3! Projektowanie dobrze zharmonizowanej pracy jest bardziej uciążliwe dla programisty (autora), lecz wielce pomocne dla czytelnika, dla którego jest ona przede wszystkim przeznaczona.
10.6. Zasady Niezależnie od swych upodobań wielu programistów stało się teraz lokal nymi ekspertami projektowania dokumentów. Może to nie być tak całkiem niedorzeczne, jak się wydaje. Fred Brooks elokwentnie opisuje radości i smutki rzemiosła programisty w rozdz. 1 książki Mityczny osobomiesiąc^. 4 Warszawa, WNT 2000 - p isgp . tłum.
144
10. PROJEKTOWANIE DOKUMENTU
Produkcja dokumentów zajmuje wiele miejsca na jego liście doświadczeń. Oba zadania wymagają „robienia rzeczy pożytecznych dla innych ludzi” i oba „muszą być wykonane doskonale”. Największą radość sprawia mi to, że jedne i drugie przynoszą „czystą przyjemność tworzenia”. Projektowanie dokumentów wymaga kreatywności. Biblioteka, w której wszystkie dokumenty są do siebie podobne, byłaby okropnie nudna, jak świat, w którym wszyscy ludzie są ubrani tak samo i wszystkie samochody mają ten sam kształt i kolor (przypuszczalnie czarny). Najlepszy projekt zależy od wielu atrybutów dokumentu; opakowanie musi być skrojone odpowiednio do treści. Ale strzeż się nadmiernej kreatywności. Strunk i White radzą autorom: „Pozostań w cieniu”. Dobry styl dokumentu, jak dobry styl programowania lub dobry styl pisarski, jest niewidoczny. Treść jest podstawowym celem dokumentu; jego styl jest tylko środkiem wiodącym do celu.
10.7. Zadania 1. W wielu dowodach matematycznych szeroko korzysta się z rysunków. Wybierz dowód, który można łatwo przedstawić na tablicy, i zapisz go w Twoim systemie produkowania dokumentów. Spośród wielu kandy datur można wybrać sumę szeregu arytmetycznego 1 + 2 + . .. + N albo trójkąt Pascala czy inne dowody twierdzenia Pitagorasa. 2. W algorytmie sortowania przez kopcowanie po utworzeniu kopca z ta blicy X [ l . . N] używa się takiego niezmiennika: K opiec, +
Posortowany, >
N Znaki nierówności są skrótem formuły X [ l . . J] < X [I + 1. .N ], In deks pętli I przyjmuje wartości od N do 2; rozmiar posortowanych części tablicy wzrasta od 0 do równego całej tablicy. Narysuj przebieg algorytmu sortowania przez kopcowanie, a także innego algorytmu sortowania opartego na wybieraniu największego elementu tablicy. 3. Hugh Williamson w książce Methods o f Book Design (wydanie 3, Yale Univerisity Press, 1983 r.) określa dla dokumentów trzy podstawowe cele: poprawność, konsekwencja i przejrzystość. Jak powinno się skła dać programy komputerowe, aby spełniały te trzy cele?
10.8. LITERATURA UZUPEŁNIAJĄCA
145
10.8. Literatura uzupełniająca Wydawnictwo University of Chicago Press opublikowało w 1982 roku trzynaste wydanie książki The Chicago M anual o f Style. Oprócz przedstawie nia stylu przyjętego w tym wydawnictwie opisano w tej książce zasady leżące u podstaw konkretnych wyborów. Ta praca wyznacza standard dla wielu wydawnictw. Dobrze by było, gdyby każdy programista zajmujący się publikacjami miał tę książkę na biurku.
10.9. Katalog ulubionych utrapień [Na marginesie] Kilku czytelników przysłało mi tego rodzaju uwagi: „Nie ostrzegałeś przed budzącym postrach... ”. Wiele z tych ostrzeżeń przewijało się w tekście, ale teraz przedstawię kilka mniej ważnych kwestii do rozważenia. Za długie wiersze tekstu. Staraj się, aby w wierszu było co najwyżej 75 znaków łącznie z odstępami i znakami interpunkcyjnymi. Ludzie, czytając dłuższe wiersze tekstu, zdają się tracić wątek, kiedy ich wzrok przeno si się (w lewo) do początku następnego wiersza. Często ten błąd po pełniają naukowcy składający komputerowo swoje prace, którzy używają 10-punktowej czcionki na papierze o szerokości 8,5-cala z 1-calowym mar ginesem. Ur^ąd^enia o małej ro^dsfelc^ości. Jakość drukowanych dokumentów jest coraz lepsza w zależności od urządzeń - poczynając od drukarek igłowych, przez drukarki rozetkowe, drukarki laserowe, kończąc na naświetlarkach. Ceny tych urządzeń wynoszą od kilkuset do kilkudziesięciu tysięcy dola rów. Gdy już czytelnik przywyknie do jednego poziomu jakości, wówczas trudno mu się przestawić na niższy poziom. Słowa podkreślone. Zazwyczaj delikatniej spełni to zadanie kursy wa, lecz czy w ogóle trzeba to robić? Tylko kilka notacji jest bardziej irytujących niż podkreślenia. S k ł a d a ć Pewien szczególnie sfrustrowany czytelnik wyobrażał sobie, że w tym rozdziale powinien się znaleźć przykład druku utworzonego na popularnej drukarce laserowej wraz z krytyczną uwagą: „Ta drukarka laserowa ma rozdzielczość około 300 punktów na cal. Fonty były zaprojek towane przez inżynierów, zamiast przez projektantów krojów pisma. Ten przykład dość trudno się czyta, ponieważ jest w nim za dużo czerni, pro porcje między wysokością a szerokością są zachwiane, światło wewnątrz liter nie jest zrównoważone ze światłem między literami, brakuje rytmu.
146
10. PROJEKTOWANIE DOKUMENTU
Jednak możliwość używania rozmaitych fontów w ramach jednego wiersza sprawia, że jest to urządzenie idealnie dopasowane do generowania listów z żądaniami okupu”. Prawda w reklamie. Dawnymi czasy wygląd dokumentu dokładnie od zwierciedlał jego status. Ponieważ wygląd stawał się coraz lepszy, poczyna jąc od odręcznych notatek, przez robocze brudnopisy maszynowe, druko wane sprawozdania techniczne, na artykułach zamieszczanych w czasopi smach kończąc, więc zawartość nabrała więcej blasku. Jeżeli Twój pięknie wydrukowany dokument zawiera kilka pomysłów, które tego ranka przy szły Ci do głowy przy śniadaniu, to proszę, pamiętaj opatrzyć go adnotacją „roboczy szkic”. Pominięte %asoby. Leslie Lamport z DEC przesłał mi pocztą elektro niczną taką wiadomość: „Pominąłeś najprostszą i najpożyteczniejszą radę dla początkujących projektantów: podejdź do regału i zobacz, jak to robią prawdziwi projektanci książek. (Dzisiaj może musisz obejrzeć niekomputerowe książki naukowe, aby znaleźć taką, której skład nie był dziełem amatora). To niewiarygodne, jak niewielu ludzi myśli, żeby tak zrobić”. Obrady na ekranie komputera. Inny czytelnik pisze: „To może stwarzać pozory rzeczywistości, ale niewiele poza tym. Ekrany są zazwyczaj pełne nieistotnych rzeczy. Ponadto ekrany są z natury tanie, szybkie i ulotne. Jeże li taki jest też Twój dokument, to przede wszystkim po co go napisałeś?”. Głupie przenoszenie. Większość programów dzielenia słów w celu ich przeniesienia działa poprawnie, lecz ochroń swoich czytelników przed takim ciekawym dzieleniem słów, jak elektr-on, pis-anka, graf-iczny lub kor-zeń. Znaki cudzysłowu. "Te" znaki cudzysłowu na klawiaturze służą do ozna czania napisów w programie, lecz w tekście używa się „tych” znaków. Skrót R)'a. Ten powszechnie stosowany skrót oszczędza trzy i pół znaku kosztem brzydkiego wyglądu (ojej, znaczy się, brz.). Choroba M allory’ego. Mallory wspinał się na Mt. Everest „Ponieważ on tam jest”. To świetny powód na to, aby zdobywać góry, lecz fatalna przy czyna podkreślania słów 24-punktową bezszeryfową, podwójnie paskudną czcionką.
ROZDZIAŁ 11.
WYJŚCIE GRAFICZNE
Z każdym rokiem powstają coraz większe i lepsze systemy komputero we: mają większą pojemność pamięci, szybsze procesory i większe bazy danych. To dobra wiadomość —komputery mogą przechowywać więcej danych i wykonywać więcej pracy związanej z przetwarzaniem danych. Ale gdy już system wykonał masowe obliczenia, jak możemy z tej góry danych wyprowadzić jakieś ogólne wnioski? Odpowiedź na to pytanie zależy w dużej mierze zarówno od danych, jak i od upodobań czytelnika. Z akapitów tekstu i tabel liczb można czę sto wysnuć świetne streszczenia. Jednak w tym rozdziale skoncentrujemy się na graficznych reprezentacjach danych, które sprawnemu systemowi ludzkiego wzroku pozwolą przetworzyć dane. Są już szeroko dostępne niedrogie drukarki laserowe i graficzne drukarki uderzeniowe; dzięki ist nieniu pakietów oprogramowania większość programistów może używać metod graficznych. W tym rozdziale pokażę, jak my, programiści, mo żemy zastosować tę technologię do przedstawiania bardziej użytecznego (i bardziej graficznego) wyjścia.
11.1. Studium sposobu obrazowania Użyjemy teraz pewnej prostej metody graficznej do zbadania jednego zbio ru danych. W rozdziałach 1 i 2 rozważaliśmy problem stworzenia listy wszystkich słów w pliku wraz z licznikami ich powtórzeń. Program napi sany w Awku dla tego zadania przytoczyłem w p. 2.1: { for (i = 1; i 1) print " » » TEST SIĘ NIE UDAŁ « « "
>
ROZWIĄZANIA WYBRANYCH ZADAŃ
Rozwiązania zadań z rozdziału 1 1. Zadanie można inaczej sformułować, pytając, ile wykona się przypisań w tej procedurze po tym, jak w tablicy X [ l . . IV] zostaną rozmieszczo ne liczby rzeczywiste wybrane losowo z przedziału [0.. 1], Max := X [1] for I := 2 to N do if X [I] > Max then Max := X[I]
Zwyczajnie rozumując, można założyć, że mniej więcej przez połowę czasu są wykonywane instrukcje i f , więc w programie będzie wyko nanych około N /2 przypisań. Profilowałem program dziesięciokrotnie dla N = 1000 i otrzymałem uporządkowane poniżej liczby przypisań: 4
4
5
5
6
7
8
8
8
9
Knuth pokazuje w p. 1.2.10 książki A lgorytmy podstaw om , że ten algo rytm powoduje wykonanie przeciętnie — 1 przypisań, przy czym H n = 1 + 1/2 + 1/3 + . . . + 1 /N jest iV-tą liczbą harmoniczną. Dla N — 1000 ta analiza daje oczekiwaną liczbę 6,485; liczba średnia dla dziesięciu doświadczeń wynosi 6,4. 2. Poniższy program napisany w języku C realizuje algorytm, zwany si tem Eratostenesa, służący do obliczania wszystkich liczb pierwszych mniejszych niż n. Podstawową strukturą danych dla tego programu jest tablica x złożona z n bitów, które na początku mają wartość 1. Po zna lezieniu każdej liczby pierwszej wszystkim jej wielokrotnościom w tej tablicy nadaje się wartość zero. Jako następną liczbę pierwszą przyjmu je się następny w tej tablicy bit równy 1. Po wykonaniu profilowania
232
ROZW IĄZANIA W YBRANYCH ZADAŃ
okazuje się, że istnieje 9592 liczb pierwszych mniejszych niż 100000, a ponadto, że wykonało się około 2 ,57N przypisań. Mówiąc ogól nie, ten algorytm powoduje wykonanie około IV log log iV przypisań; analiza wymaga zagęszczenia liczb pierwszych i liczb harmonicznych, wspomnianych w rozwiązaniu 1.1. Oto sprofilowany kod: mainO { int i, p, n; char x [100002];
1
n = 100000;
1 100000 1 1 9593 9592 9592 256808 9592 99999 99999
for (i = 1; i Ison [p] = insert(Ison[p], x) } else if (x > val [p] ) { < « 5 9 9 3 » > rson[p] = insert(rson [p], x) > else { < « 3 6 8 » > > return p
}
234
ROZW IĄZANIA WYBRANYCH ZADAŃ
function traverse (p) { < « 1 2 6 5 » > if (p != nuli) { < « 6 3 2 » > traverse (lson[p]) print val[p] traverse(rson[p])
> > Opisany w p. 1.4 system profilowania zaprogramowany w Awku utwo rzył liczby. W bloku BEGIN funkcja in s e r t jest wywoływana 1000 razy; umieszcza ona w drzewie 632 nowych liczb; powrót z funkcji, który jest spowodowany tym, że dana liczba już się znajduje w drzewie, odbywa się 368 razy. Każde umieszczenie liczby wymaga przeciętnie wykonania 11,8 wywołań rekurencyjnych funkcji. 5b. W tym programie napisanym w Awku zastosowano przeszukiwanie zstępujące do rozwiązania zadania określenia osiągalności węzłów w grafie. Typowy wiersz wejściowy zawiera parę (poprzednik, następ nik); ciąg par definiuje graf skierowany. (W programie sortowania to pologicznego użyto tego samego formatu). Kiedy w wierszu wejścio wym wystąpi napis reach x, wtedy program wydrukuje wszystkie węzły, do których można przejść od x, stosując rekurencyjne przeszukiwanie zstępujące. function visit(node, i) { if (visited[node] == 0) { visited[node] == 1 print " " node for (i = 1; i $1 != "reach" { succlist[$1, ++succct [$1]] = $2 succct[$2] = 0 + succct[$2] # niech istnieje
} W książce AWK Programming Language, której autorami są Aho, Weinberger i Kernighan (wspomnianej w p. 2.6), znajdują się algorytmy służące do losowego generowania zdań (w p. 5.7) oraz do implemen tacji przeszukiwania zstępującego podczas sortowania topologicznego (w p. 7.3).
ROZW IĄZANIA W YBRANYCH ZADAŃ
235
6. Tablice asocjacyjne można implementować za pomocą struktur danych dla „tablic symboli”. Odpowiednie struktury zawierają drzewa prze szukiwania dwójkowego oraz zdania posortowane i nieposortowane. Jednak w większości systemów wybiera się struktury danych używa ne w Awku: są to tablice rozproszone. W rozwiązaniach 13.2 i 13.6 przytaczam kilka implementacji tablic symboli.
Rozwiązania zadań z rozdziału 3 Rozwiązania zadań 1, 2 i 3 odwołują się do napisanego w Awku zestawu testów służących do eksperymentowania na kopcach. Więcej informacji o tym programie można znaleźć w rozdz. 12 mojej książki Perełki oprogra mowania, wydanej w 1986 roku2. function maxheapCl, u, i) { # 1, jeżeli kopiec for (i = 2*1; i u) break if (c+1 x[c]) C + + if Cx[i] >= x[c]) break t = x[i]; x[i] = x[c]; x[c] = t # zamień i z c i = c
> assert(maxheapCl, u),
"siftdown postcondition")
> - Wyd. 1, W arszawa, W N T 1992 - p rvgp. tłum.
236
ROZW IĄZANIA W YBRANYCH ZADAŃ
function draw(i, s) { if (i } sl == "draw" sl sl sl sl
== == == ==
"down" "assert" "x" "n"
{ draw(l, "") } { siftdown($2, $3) > { assert(maxheap($2, $3), "cmd") > { x[$2] = $3 } { n = $2 >
1. W procedurze rekurencyjnej draw stosuje się wcięcia w celu drukowa nia niejawnej struktury drzewiastej kopca (drugi argument tej proce dury jest napisem s określającym wcięcie; każde wywołanie dodaje do tego napisu cztery spacje). 2. Zmodyfikowana procedura a s s e r t zawiera zmienną napisową infor mującą o tym, która asercja nie jest spełniona. W niektórych systemach istnieje udogodnienie, które automatycznie informuje o tym, jaki jest plik źródłowy i numer wiersza niepoprawnej asercji. 3. Procedura s i f tdown używa procedury a s s e r t oraz procedury maxheap do testowania warunków początkowych i końcowych przed i po za kończeniu działania. Wykonanie procedury maxheap wymaga czasu 0 (U — .L), więc wywołania procedury a s s e r t powinno się usunąć z wersji produkcyjnej programu. 4. Testy zamieszczone w dod. 2 nie zawierają błędu, który popełniłem w pierwszej wersji procedury s i f tup. Pomyłkowo zmienną i inicjowa łem niepoprawnie przypisaniem i=n, zamiast i=u. Jednak we wszyst kich moich testach wartości u były równe n, więc błąd się nie pojawiał. 6. Badanie czasu wykonywania algorytmu Hoare’a znajdowania k -teg o z kolei najmniejszego elementu zbioru opisałem w p. 15.3. 8. Aby sprawdzić, że procedura sortowania permutuje swoje dane wej ściowe, możemy je skopiować do odrębnej tablicy, posortować za po mocą niezawodnej metody i po zakończeniu wykonywania nowej pro cedury porównać te dwie tablice. Alternatywna metoda używa tylko kil ku bajtów pamięci, ale czasami popełnia błąd: używa sumy elementów tablicy jako sygnatury tych elementów. Istnieje duże prawdopodobień stwo, że zmiana podzbioru elementów zmieni tę sumę. (Sumowanie
ROZWIĄZANIA W YBRANYCH ZADAŃ
237
pociąga za sobą powstawanie problemów związanych z rozmiarem sło wa i brakiem łączności operacji dodawania zmiennopozycyjnego; inne sygnatury, takie jak alternatywa wyłączająca, pozwala ich uniknąć).
Rozwiązania zadań z rozdziału 4 2. W książce Kernighana i Pike’a The UNIX Programming Environment znaj duje się w p. 3.9 program, zwany bundle. Polecenie bundle filet file2 file3
tworzy plik powłoki systemu UNIX. Wykonanie tego polecenia po woduje wypisanie kopii wszystkich plików należących do tej wiązki plików. 3. W każdym uniwersalnym modelu obliczeniowym istnieje program, który sam się reprodukuje. W dowodzie korzysta się z twierdzenia o rekurencji oraz z twierdzenia s-m-n z teorii funkcji rekurencyjnych. Hakerom sprawia wiele radości pisanie samoreprodukujących się pro gramów w prawdziwych językach; szczególnie popularnymi językami wydają się C i Fortran. Zadanie będzie łatwiejsze, jeśli pozwolisz, by program sam się reprodukował w razie błędu w danych wyjściowych. Jeżeli zaczynasz od małego pliku (zawierającego, powiedzmy, jedno bzdurne słowo) i następnie drukowany przez komputer komunikat o błędzie wielokrotnie podajesz jako dane wejściowe dla kompilatora, to zazwyczaj taki proces jest szybko zbieżny. 4. Uniksowy system plików nie klasyfikuje plików na podstawie typu, lecz w kilku programach używa się zawartości plików jako niejawnego ich opisu. Na przykład polecenie f i l e bada plik i zgaduje, czy jego zawar tością jest tekst w kodzie ASCII, tekst programu, polecenia powłoki itd. Kernighan i Pike w książce wspomnianej w rozwiązaniu 2 przed stawiają program zwany doctype, który czyta plik wejściowy systemu Troff i wnioskuje, jakiego należy użyć do niego preprocesora języka (takiego jak Pic, Tbl itd.). 5. Do przykładów par nazwa-wartość należy instrukcja GET DATA w ję zyku PL/1 oraz instrukcja NAMELIST w Fortranie. Tablice i funkcje odwzorowują nazwy na wartości. 7. Ogólna zasada głosi, że dane wyjściowe programu powinny być od powiednie do tego, aby mogły być danymi wejściowymi do programu.
238
ROZW IĄZANIA W YBRANYCH ZADAŃ
Jest to szczególnie ważne w odniesieniu do programów będących po tokami, oraz w systemach okien, w których można wybierać dane wyjściowe i kilkoma ruchami myszki ponownie je wprowadzać jako dane wejściowe.
Rozwiązania zadań z rozdziału 5 1. Ponieważ plik był tak mały, zaproponowałem wprowadzanie danych z łatwo dostępnego wydruku. Mogę nawet wprowadzać po jednej cyfrze na sekundę, co przekłada się na trzy rekordy na minutę lub dwieście rekordów na godzinę. Mając pozycję danych, urzędnik wpi suje dane przy użyciu znanych narzędzi, powinno to zatem trwać mniej niż dwie godziny i kosztować mniej niż pięćdziesiąt dolarów. Zautoma tyzowane rozwiązanie wymagałoby zarówno istnienia solidnego opro gramowania w obu komputerach osobistych (uważnie przeszukałbym pakiety przed napisaniem własnego kodu), jak i zakupu modemów. Chociaż dla dużych wolumenów danych lepsze będzie oczywiście naj nowocześniejsze rozwiązanie, proste rozwiązanie było pierwszorzędne dla zadania pod ręką. 2. Lynn Jelinski otrzymała następującą notatkę od swego ojca, Geoffreya Woodarda: Według legendy praktykant zaczynający pracę u hydraulika otrzy muje zadanie znalezienia lewoskrętnego klucza francuskiego do rur. Edison przydzielił nowemu pracownikowi zadanie wyznaczenia objętości żarówek, którą niezwykle trudno obliczyć, mierząc ża rówki, lecz można to łatwo zrobić, po prostu umieszczając je w cylindrze z podziałką. Słyszałem gdzieś, że w Laboratoriach Bella pierwszym przydzie lanym zadaniem było ulepszenie skręconego kabla słuchawki te lefonicznej. Puenta jest taka, że jednocentowa zmiana (zrobienie nowego projektu, w którym odrzucono sznur, zbliżając mikrofon i słuchawkę do aparatu) przekłada się na oszczędności liczone w milionach dolarów. W każdym razie, miej się na baczności. Jakie podobne otrzęsinowe rytuały stosuje się (lub powinno) w Twojej firmie?
239
ROZWIĄZANIA W YBRANYCH ZADAŃ
3. Przedwcześnie porwałem się na skomputeryzowane rozwiązanie. Wkrótce po tym, jak odzyskałem rozsądek, zaproponowałem, by psy cholog napisał sześć permutacji liczb 1,2,3 na sześciu bokach drew nianej kostki i podobnie rozmieścił poziomy stresu na drugiej kostce. Kiedy obiekt wchodził do pokoju, wtedy eksperymentator mógł ge nerować losowe permutacje, rzucając te dwie kostki: 213 123
ŚNW 132
NWŚ NSW
Chociaż byłem zachwycony tym prostym, eleganckim i efektywnym rozwiązaniem, psycholog naprawdę chciał, aby za eksperymentem przemawiała jego „komputerowa” proweniencja. Napisałem program pod warunkiem, że będę mógł opisać całą historię w tej książce. 4. Ponieważ funkcja s q rt jest monofonicznie rosnąca, więc możemy ją usunąć z kodu wykonywanego w pętli i obliczyć pierwiastek po wy konaniu pętli. Wielu programistów ulega blokadzie koncepcyjnej i nie usuwa procedury pierwiastka kwadratowego.
Rozwiązania zadań z rozdziału 7 3. W wielu mikrokomputerowych interpretatorach języka Basic koszt do stępu do zmiennej jest proporcjonalny do jej pozycji w tablicy symboli. Dostęp do zmiennych używanych na początku wykonywania programu jest zatem tańszy niż do zmiennych, których po raz pierwszy użyto później w programie. W maszynach, które mają pamięć podręczną rozkazów, mała zmiana może spowodować, że jakaś wewnętrzna pęda znajdzie się poza pamięcią podręczną, powiększając ogólny czas wy konywania o 20 procent. Tydzień wcześniej, zanim po raz pierwszy napisałem ten felieton, pewien kolega dziesięciokrotnie skrócił mój program napisany w Awku, ponieważ znaki zapytania, w które ujęty był wzorzec, zastąpił znakami ukośnika (nie doceniałem tej subtelnej odrębności semantycznej). 4. Można oszacować lokalną śmiertelność, licząc nekrologi w gazetach i szacując liczbę mieszkańców na danym obszarze. Łatwiejszym spo sobem będzie skorzystanie z prawa Little’a i z oszacowania średniej
240
ROZW IĄZANIA W YBRANYCH ZADAŃ
długości życia; jeżeli średnia długość życia wynosi na przykład 70 lat, to każdego roku umiera 1/70 część lub 1,4% populacji. 5. Podany przez Denninga dowód prawa Little’a składa się z dwóch czę ści. „Najpierw zdefiniuj tempo przychodzenia jako A = A /T, przy czym A oznacza liczbę przyjść w okresie obserwacji T. Zdefiniuj tem po wychodzenia jako X — C/T, przy czym C oznacza liczbę wy konań w okresie obserwacji T. Niech n{t) oznacza liczbę wejść do systemu w chwili t należącej do przedziału [0, TJ. Niech W będzie obszarem, na którym jest określona funkcja n(f) w jednostkach ‘element-sekundy’, reprezentującym łączny czas oczekiwania wszystkich elementów w danym systemie w okresie obserwacji. Średni czas reak cji na wykonany element jest zdefiniowany jako R — W/C, mierzo ny w jednostkach (element-sekundy)/(element). Liczbą średnią w tym systemie jest przeciętna wielkość «(/) oraz mamy L = W/T w jednost kach (element-sekundy)/(sekundę). Jest teraz oczywiste, że L = RX. To sformułowanie określa tylko tempo wychodzenia. Nie ma żądania ‘zbilansowanego przepływu’, tj. przepływu równego odpływowi (sym bolicznie A = X). Jeżeli przyjmiemy takie założenie, to otrzymamy formułę: L = A X R, która w tej postaci występuje w teorii kolejek i teorii systemów”. 6. Peter Denning pisze: „Przypuśćmy, że mamy sieć serwerów. Niech VJ oznacza średnią liczbę wizyt, które każde zadanie złoży serwerowi i (tzn. użyje go). Tak więc wzór V\ + . . . + LR oznacza ogólną liczbę zadań-kroków dla przeciętnego zadania. Ogólny przepływ w systemie, czyli Xq, jest związany z lokalnym przepływem dla serwera i na podsta wie prawa ‘przepływu wymuszonego’: Xj = L/ X Xq. Niech Rq ozna cza czas reakcji doświadczany przez zadanie i niech La oznacza średnią liczbę zadań w systemie. Formuła Little’a powiada, że czas reakcji sys temu jest określony wzorem: Rq = Lq/X0. Lecz Lq = Li + . . . + Ljy, przy czym L, jest średnią liczbą zadań dla serwera i; L, = R, X X,-, przy czym R jest średnim czasem reakcji na wizytę w serwerze i. Uży wając wzoru Xi/Xa — W, wynikającego z prawa stałego przepływu, otrzymujemy, że Ro = Ri X V\ + . .. + R;\ X LR. Jest to intuicyj nie prawdziwe, lecz można to łatwo i ściśle wyprowadzić, używając dwukrotnie prawa Little’a”. 7. Bruce Weide pisze: „W pierwotnym przypadku ‘system’ jest to kolejka plus serwer. Przyjmując taką notację, jak w rozwiązaniu zadania 5, R oznacza średni czas, który klient spędza w kolejce i w trakcie usługi,
ROZW IĄZANIA W YBRANYCH ZADAŃ
241
oraz L jest średnią liczbą klientów w kolejce i klientów obsługiwanych. Na podstawie prawa Little’a wiemy, że L = RX, przy czym X jest tempem wychodzenia z serwera. Ale X to także tempo wychodzenia z kolejki, ponieważ klient przechodzi bezpośrednio z kolejki do ser wera, kiedy tylko inny klient opuszcza serwer. Uważając samą kolejkę za ‘system’ i definiując Lq jako przeciętną liczbę w kolejce oraz Rg jako przeciętny czas spędzony w samej kolejce, widzimy, że Lq = Rg. Oczekiwana relacja jest zatem taka, że stosunki L/R oraz Lg/Rg są równe”. 8. Bruce Weide proponuje takie rozwiązanie. „Jednym sposobem rozwią zania tego problemu jest rozważenie dwóch systemów kolejek. Pierw szy to kolejka zadań oczekujących na wykonanie, drugi to sam system komputerowy. Na podstawie prawa Little’a tempo wychodzenia zadań w drugim systemie jest określone wzorem X = L/R. Tutaj mamy L = 10 zadań (ponieważ są tam zawsze zaległości w pracy, system bę dzie mieć zawsze maksymalnie 10 zaległych zadań, zatem 10 jest także przeciętną liczbą zadań w tym systemie). Średni czas R = 20 sekund, więc tempo wychodzenia X musi wynosić 1/2 zadania na sekundę. Jest to też tempo przychodzenia zadań do drugiego systemu - speł nione jest założenie przepływu zrównoważonego, ponieważ wartość L jest stała, a to oznacza, że każde zadanie, którego wykonanie się kończy, będzie natychmiast zastąpione przez następne zadanie. Teraz wychodzenie z pierwszego systemu też musi się odbywać w tempie 1/2 zadania na sekundę. Powinniśmy zatem oczekiwać, że 99 zadań przed nami powinno być załatwionych po 198 sekundach. Tak więc nasze zadanie zakończy się 20 sekund później, ponieważ ogólny czas oczekiwania wynosi 218 sekund.
Rozwiązania zadań z rozdziału 9 1. W książce The AW K Programming Language znajduje się w p. 6.3 opis małego języka służącego do generowania poleceń sortowania w syste mie UNIX. 2. W systemie UNIX używa się wyrażeń regularnych w edytorze ed oraz w programie grep przeznaczonym do porównywania wzorców. 3. Mały język służący do opisywania bibliografii omawialiśmy w p. 4.1.
242
ROZW IĄZANIA W YBRANYCH ZADAŃ
4. W podrozdziale 6.1 książki The AWK Programming Tanguage opisano asembler i interpretator zaprogramowany w kilkudziesięciu wierszach języka Awk. Stosów używa się w rozmaitych językach, poczynając od kodu maszynowego dla podręcznych kalkulatorów (takich jak maszyny HP), małych języków do składania tekstu (Postscript), języków ogólne go przeznaczenia (Forth), a na sprzęcie (maszyny Burroughsa) kończąc. 6. Kiedy Mark Kernighan miał 11 lat, użył takiej struktury do pisania programu muzycznego w języku Basic: 1 ’ 100 110 120 130 140 150 160 170
Graj melodię POKE 36874, 262 FOR 1=1 TO 1000: POKE 36875, 183 FOR 1=1 TO 2000: POKE 36875, 190 FOR 1=1 TO 1000: POKE 36874, 240 FOR 1=1 TO 1000:
NEXT I NEXT I NEXT I NEXT I
Wiersz 100 generuje ton, umieszczając (POKE) wartość w komórce pamięci 36874, wiersz 110 czeka, aż przebrzmi ten ton, wiersz 120 tworzy ton poprzez drugi generator, umieszczając wartość w komórce pamięci 36875. Poszturchiwany przez ojca, Briana Kernighana, Mark nabrał ochoty, by zastanowić się, czy nie obrał złej drogi. Napisał program od nowa, używając mikroskopijnego języka muzycznego, zdefiniowanego w uwagach. 1 ’1..99 => Opóźnienie 2 ’100. .999 => Ton 1 3 ’1000. .1999 => Ton 2 10 DATA 262, 1, 1183, 2, 1190, 1, 240, 1, ... 100 READ X 110 IF X >= 100 THEN GOTO 140 120 FOR 1=1 TO X*1000: NEXT I 130 GOTO 100 140 IF X >= 1000 THEN GOTO 170 150 POKE 36874, X 160 GOTO 100 170 ’ 1000 1) y = $2 ub = 10 if (NF > 2) ub = $3 for (i = 1; i y = newy
> > Jeżeli nie poda się liczby iteracji, to program przyjmuje liczbę 10 ja ko wartość domyślną. (Program zatrzyma się również wtedy, kiedy ciąg będzie zbieżny). Jeżeli nie ma określonej wartości początkowej, to program użyje w tym celu samej wartości x. 8. J. L. Blue w artykule pt. „A portable Fortran program to find the Euc lidean norm of a vector”, zamieszczonym w numerze z 1 marca 1978 roku czasopisma ACM Transactions on Mathematical Software 4 (s. 15-23), opisuje program, w którym unika się występowania nadmiaru i niedo miaru. 9. Zobacz rozwiązanie zadań 11 i 12. 10. Możemy usunąć pierwszą instrukcję przypisania, zastępując zmienną Max przez wyrażenie 2. 0*Max: Max := 0.5 * (2.0*Max + Sum/(2.0*Max))
a następnie przekształcić algebraicznie tę instrukcję w Max := Max + Sum/(4.0*Max)
zaoszczędzając jedno mnożenie i kilka procent czasu wykonywania tej procedury.
252
ROZW IĄZANIA W YBRANYCH ZADAŃ
11, 12. Andrew Appel zastąpił K wartości bezwzględnych jedną wartością, zachowując ścieżkę największego kwadratu, a następnie na zewnątrz pętli obliczał jego wartość bezwzględną. (Bob Floyd zauważył, że mo że taniej byłoby użyć wartości początkowej opartej na sumie wartości bezwzględnych). W tym programie stosuje sie przyspieszenie wprowa dzone przez Appela i dodatkowo korzysta się z przeszukiwania tablic w celu znalezienia dobrej wartości początkowej. Powstał program, któ ry jest około 10 procent szybszy niż Program 4. MaxT := T := A [1 ] - B [ l ] MaxT2 := Sum := T*T for J := 2 to K T := A[J] - B[J] T2 := T*T if T2 > MaxT2 then MaxT := T MaxT2 := T2 Sum := Sum + T2 if Sum = 0.0 then return 0.0 if MaxT < 0.0 then MaxT := -MaxT T := MaxT * DistTab[trunc(Scale*Sum/MaxT2)] T := 0.5 * (T + Sum/T) return 0.5 * (T + Sum/T)
W programie używa się wektora liczb zmiennopozycyjnych, zainicjo wanego przez kod: float DistTab[Scale..K*Scale] for I := Scale to K*Scale do DistTab[I] = sqrt((1+0.5)/Scale)
Dla wartości zmiennej Scale = 20 uzyskałem dokładny wynik w po jedynczej precyzji. W moim pierwotnym kodzie inicjującym tablicę użyłem systemowej procedury pierwiastka kwadratowego i czas wyko nywania dla wartości K = 16 wynosił 0,3 sekundy. Przyjmując ostat nio obliczoną wartość pierwiastka kwadratowego jako przypuszczalną wartość początkową oraz stosując trzy iteracje Newtona, otrzymałem program szybszy o jeden rząd wielkości. 14. Konspekt z wykładu pt. „Implementation of Algorithms” W Kahana ukazał się w Berkeley Computer Science Technical Report #20 oraz w National Technical Information Service Report AD-769 124. Na stronie 52 w p. 19 Kahan pokazuje, że wykonywanie wspomnianej procedury może się nie zakończyć w maszynie IBM 650; procedura może być zawodna także w innych maszynach.
ROZWIĄZANIA W YBRANYCH ZADAŃ
253
15. Bob Floyd z Uniwersytetu Stanforda pisze: „Jeżeli i -ta aproksymacja zawodzi z powodu współczynnika/ , to i + 1-wsza zawodzi z powodu współczynnika ę ( f ) = ( f + (l//))/2, przy czym (p(f) = (p{\/f), przy najmniejszych wartościach między / oraz 1//, większych wartościach na zewnątrz. Oczywiście nadaje się to do minimalizowania maksimum abs log/”.
Rozwiązania zadań z rozdziału 15 1. W artykule zamieszczonym w czasopiśmie Communications o f the A CM w marcu 1975 roku Floyd i Rivest pokazują, jak można użyć sonda żu w celu uzyskania algorytmów selekcji, które są wydajne zarówno w teorii, jak i w praktyce. 2. Ten kod rozwiniętej pętli sortuje tablicę X [\.. 3] przy użyciu tylko trzech porównań; instrukcje a s s e r t ukazują uporządkowanie tablicy osiągnięte po wykonaniu każdej instrukcji. if X[l] > X [2] then swap(X[l], X [2]) assert X [1] < X [2] if X [2] > X [3] then swap(X[2], X [3] ) assert X[l] < X [3] and X [2] < X[3] if X [1] > X [2] then swap(X[l] , X [2]) assert X[l] < X [2] < X[3]
Jest to zazwyczaj najszybszy sposób obliczenia mediany trzech ele mentów. Aby znaleźć 1000-ną z kolei najmniejszą liczbę zapisaną na taśmie zawierającej milion elementów, można czytać taśmę tak długo, aż 1000 najmniejszych liczb widzianych do tej pory będzie zachowa nych w kopcu z największą liczbą na wierzchołku. 3. Randomizowane wyszukiwanie dwójkowe znajduje wartość mediany N liczb na taśmie, używając kilku zmiennych i O(log N ) przebiegów taśmy. Zmienne L oraz U są dolną i górną granicą zakresu, o którym wiadomo, że zawiera medianę; początkowo są to najmniejszy i naj większy element zbioru. Na każdym etapie wykonywania algorytmu dokonuje się dwóch przebiegów taśmy. Podczas pierwszego przebie gu zachowuje się w zmiennej M losową liczbę z taśmy należącą do zakresu L . . U (pierwszą liczbę całkowitą z tego przedziału zawsze zachowuje się w zmiennej M , drugi element z tego przedziału jest
254
ROZW IĄZANIA W YBRANYCH ZADAŃ
zachowany z prawdopodobieństwem 1/2, trzeci - z prawdopodobień stwem 1/3 itd.). Podczas drugiego przebiegu taśmy zlicza się, ile na taśmie jest elementów mniejszych niż element zachowany w zmiennej M i ile elementów jest większych niż ten element; następnie wartość zmiennej M zachowuje się albo w zmiennej L , albo w zmiennej U. Ten proces kontynuuje się dopóty, dopóki w zmiennej M nie pojawi się mediana liczb znajdujących się na taśmie. Zazwyczaj wymaga to wykonania O(log N ) przebiegów taśmy, ponieważ ogólny czas średni wykonywania algorytmu wynosi 0 (N log N ). Drugi napęd taśmy pozwala zmniejszyć oczekiwany czas wykonywania do wartości 0 ( N) , dzięki temu, że na taśmie znajdują się tylko ele menty należące do bieżącego zakresu. Każdy przebieg taśmy składa się z trzech faz. W pierwszej fazie postępuje się tak jak powyżej, w drugiej fazie aktywne elementy kopiuje się na drugiej taśmie, a w trzeciej fazie z powrotem kopiuje się je na pierwszej taśmie. 4. Blum, Floyd, Pratt, Rivest i Tarjan odkryli na początku lat siedem dziesiątych algorytm wyboru mający liniowy czas wykonywania naj gorszego przypadku. Opisano go szczegółowo w większości książek o algorytmach. 6. Zmienna Cjv oznacza wartość średnią funkcji CCount(N), określającą liczbę porównań, których używa się w algorytmie wyboru w celu zna lezienia najmniejszego elementu tablicy mającej N elementów. W tym programie korzysta się z relacji rekurencji dla ć/y, określonej w in strukcji służącej do drukowania wartości C0, C\, . . . , Cm CEO] := CCI] := 0 print C[0], C[l] for N := 2 to M do Sum := 0 for X := 0 to N-l do Sum := Sum + C [I] C[N] := N-l + Sum/N print C[N]
Zachowując poprzednią wartość zmiennej Sum, można czas wykony wania tego programu, 0 (M 2), zredukować do wartości 0(M ). CEO] := C[l] := 0 print C[0], C[l] Sum := CEO] + C [1] for N := 2 to M do Sum := Sum + C[N-1]
ROZWIĄZANIA W YBRANYCH ZADAŃ
255
C [N] := N-l + Sum/N print C[N]
W następnym kodzie pozbywamy się tablicy C[0. .M ], zachowując war tość C[IV] w zmiennej LastC. Sum := 0 LastC := 0 print 0, 0 for N := 2 to H do Sum := Sum + LastC LastC := N-l + Sum/N print LastC
Można używać tego programu do eksperymentalnego badania zacho wania algorytmu. Ewentualnie struktura tego programu wskazuje na formułę sumowania dla zmiennej Cm - Przekształcenie skomplikowanej rekurencji w sumowanie jest zazwyczaj nazywane „składaniem telesko powym” (ang. telescoping). Rozwiązaniem jest funkcja = 2(N —HN), przy czym H\ oznacza N -tą liczbę harmoniczną 1 + 1/2+1/3 + . .. + 1/IV 7. Algorytm 410 zamieszczony w Communications o f the A CM w maju 1971 roku „sortuje częściowo” tablicę; napisał go John Chambers. 9. Pierwszy i drugi z kolei największy element zbioru można znaleźć, wy konując N + log2 N + 0(1) porównań. Knuth w p. 5.3.3 książki Sztuka programowania. Tom 3: Sortowanie i wystukiwanie* przedstawia ten i kilka innych fascynujących algorytmów obliczania porządku statystycznego przy użyciu optymalnej liczby porównań.
4 Warszawa, W N T 2002 - pryyp. tłum.
SKOROWIDZ
A
blokady koncepcyjne
Adams J. L.
70
A ho A. V. Algol
VIII, 32, 128, 220, 234
19
algorytm Floyda
1 7 9 -1 8 5
Hoare’a
-
sortowania szybkiego sortowania
- wyboru
254
205 177—185
3 7 -4 0 , 2 0 3 -2 17 , 254
analiza algorytmów
Buzen J. P.
41, 81, 82, 103, 143 250
92
c C
3, 8, 9, 13, 19, 9 0 -9 2 , 188, 195, 2 1 9 -2 2 0 , 231
208—217
Cadwallader-Cohen J. B.
3
Cargill T. A.
19
A p p e lA .W
Carlson R.
1 9 8 ,2 5 2
Chem
23
Awk 8, 9, 13, 2 0 -3 2 , 42, 43, 117, 119, 12 1, 125, 128, 147, 148, 180, 22 0-2 22, 2 3 3-2 35, 239, 242
81
118 , 11 9 VIII, 76, 156, 158
107
Condon J.
84
Conway J. H.
B
156, 216, 255
Cleveland W S. Cobol
32
Conyngham J.
Basic
108, 124, 125, 163, 239, 246
Bell G.
79
Bernstein L.
Crocker S.
74
198
czas wykonywania 73, 77, 81, 82
biblioteka podprogramów standardowych
40
223—230
99
75
Chambers J. M.
80
automaty skończone
70
164, 165, 173
Burstall R. M.
45, 53—57
ANSI Standard C
Ardis M.
Blum M.
Brooks F. P. Jr.
236
algorytmy próbkowania
APL
1 9 7 ,2 5 1
BPL
-
-
Blue J. L.
D Darlington J.
250
7 -9 , 212
258
SKOROWIDZ
Denning P.J.
VIII, 65, 92, 93, 95, 240
D ewdney A. K.
68
D ijk stra E .W D onner M.
Huber A.
77 33, 180, 232
drzewo przeszukiwania dwójkowego 233, 248 5 3 ,7 3 ,7 7 ,8 1 ,8 2
79
75
H uff D.
76
dowody poprawności
D u ff T.
Hopper G M .
1 5 5 ,1 5 8
I iteracja Newtona
1 8 9 -1 9 9
J Jackson M. A.
78
E
Jelinski L. W.
Edison T. A.
języki specjalizowane
69
eksperymentowanie
53, 90
F
Johnson D. S.
VIII
Johnson S. C.
121
Jones D.
Fairley R. E. Farrar R
80
VIII, 118, 119, 238
75
Jones D.W.
7 8 ,8 1
83
Feldman S. I. Flon L.
K
4 4 ,1 2 3 ,1 2 4
Kahan W
23
Floyd R. W VIII, 177, 1 7 9 -1 8 5 , 217, 248, 249, 2 5 2 -2 5 4 Fortran
252
Kernighan B. W
123, 124, 128, 133, 148, 219, 220, 237, 244
99
Knuth D. E.
G
9, 17, 27, 41, 78, 158,
178, 2 1 1, 217, 231, 233, 250, 255
Garey M. R.
VIII, 76
generowanie zdań losowych G erhart S. L.
25
148, 209
Gries D.
232
Grosse E. H.
31
L Lamport L.
83
g raf skierowany
VIII
139
Leas D.
14
Lemons E. W Lex
Halpern P.
75
139, 146
Lamp son B.W. LATe X
Lesk M. E.
H 23
Lisp
86
76 1 2 0 ,1 3 0
120, 12 1, 1 2 4 -1 2 6
liczby pierwsze
Hilfinger P. N. Hill R.
VIII, 9, 32, 42, 44,
50, 77, 84, 108, 109, 115, 118, 119,
9, 14, 85, 107
Furbelow G.
Grap
43
3—8, 2 3 1 -2 3 2
14, 19
81, 82
hipoteza W horfa histogram
19, 143
172
M Mairson H. G.
Hoare C. A. R.
3 7 ,2 0 4 ,2 1 7
Make
Hofstadter D.
88, 96, 97
M allory G. L.
Holzmann G.
VIII
małe języki
232
120, 1 2 3 -1 2 6 146 50, 10 7 -1 2 8 , 16 4 -1 6 9
259
SKOROWIDZ
Martin D.
75, 79, 80
Martin R. L.
68, 80
M cllroy M. D.
VIII, 182, 198
McKeeman W. M. mediana
profile czasu procedury 187
Misra J.
próbki losowe
159
— zstępujące
199
34—37
234
pseudokolumny
78
166—168
R
N Napoleon
raporty o błędach
159, 161
Nelson N. P.
189
o
Rice J.
200
Rice S.
141
Ritchie D. M.
odrętwienie numeryczne
88, 96
P Parker T.
4 7 -5 6 , 237
— wzorzec-czynność
221
2 1 7 ,2 5 3 ,2 5 4
Roueche B.
72
samoopisujące się dane
VIII
Schapira A. Scribe
Perlis A.J.
80
Sethi R. 181
Shell
14, 128, 237
Plauger P.J.
33—45
50, 84, 133
pochodzenie w programowaniu 5 1 -5 7 66 254
prawo Little’a Pritchard
10,13,82
232
Snobol
134—137
1 3 7 -1 4 0 21, 117, 125
sondaże
163—174 5 3 -5 5 , 6 1—62
— przez kopcowanie wstawianie
92—94, 239—241
8, 16, 2 3 1—232
składanie rysunków — tekstu
sortowanie
11, 14 1, 148
Pratt V.R.
23, 81, 83
116 , 14 1, 148
sito Eratostenesa
platforma uruchomieniowa
potoki
VIII, 128
Sites R. L.
19
75
129
Shaw M.
1 0 8 -12 4 , 148, 159
Polya G.
83
Schryer N. L.
204
permutacje losowe
4 7 -5 7
116 , 117 , 119
percentyle
Pike R.
7 6 , 2 1 9 ,2 4 4
Rivest R. L.
Scatter
1 9 ,1 0 7 ,1 1 3 ,2 2 0 ,2 2 1
Penzias A .A .
PL/1
38, 207
s
85
pary nazwa-wartość Pascal
44
rekurencja końcowa
79
Newton I.
Pic
62—63
przeszukiwanie dwójkowe
199
129—146
1 7 7 -1 8 5
próbkowanie
Morrison D. M orton M.
213
projektowanie dokumentu
232
Moler C.B.
5
programy szkieletowe
7 9 ,8 2
204
Minard C.J.
— zliczające wiersze
5, 6, 8, 10,
— topologiczne Starm er C. F.
142, 144, 243
243 25, 234
VIII, 51
260
SKOROWIDZ
Steele G.J. Jr. Storer D.
Y
79, 83
77
Vyssotsky V.
Stroustrup B.
VIII
Strunk W. Jr.
133,134,144
W
symulator automatu skończonego systemy profilowania
3—17, 232
— tworzenia dokumentów Szymański T. G.
129
9
tablice asocjacyjne
19 -3 2 , 235
254 33—45
129
transmisja danych
74 62—63
VIII, 14
112, 116, 124, 129, 131
T u fteE .R .
156,158,161,174
TukeyJ.W .
211,246,247
T ukeyP .A .
156,245
84
19,143
Williams H. H.
82, 83
Williamson H.
144
Wintz P.
14 23,85
wydajność systemu
90
wykres łodyga-liść
151, 157 153
— punktowe
153—154
— słupkowe
170
wykrywanie błędów
7 1—72
wyrażenia regularne
121, 241
Y Yacc
1 2 0 -1 2 3 , 125, 126
U Ullman J.D .
Z
128
uruchamianie programu 7 1-7 2
36, 44, 217
133,134,144
wykresy kołowe
Thom pson K. L. Trickey H. W.
13, 32, 75, 220, 234
77
W u lf W. A.
130
testowanie program ów
T ro ff
Weir W.
W h o rfB .
130—133
Tarjan R. E.
Te X
VIII, 92, 95, 240, 241
Whiteside B.
140—144
T
Tbl
Weide B. W.
Weinberger P.J.
W hite E.B.
s tabele
23
weryfikacja programu
r
środki wyrazu
VIII, 76, 85
33—45,
zero funkcji
189
Zerouni C.
75, 82
WNT. Warszawa 2007. Wyd. I Ark. wyd. 14,5. Ark. druk. 17,0 Symbol Et/83951/WNT Cieszyńska Drukarnia Wydawnicza