Matematyczne korzenie informatyki

Transkrypt

Matematyczne korzenie informatyki
Matematyczne korzenie informatyki
Stefan Sokołowski1
Gdańsk, 1 sierpnia 2009, poprawione 22 marca 2010
Instytut Informatyki, Uniwersytet Gdański, oraz Instytut Informatyki Stosowanej,
Państwowa Wyższa Szkoła Zawodowa w Elblągu, [email protected].
1
Spis treści
1 Matematyczne korzenie informatyki
1.1 Wstęp . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.2 Matematyczne modele obliczeń . . . . . . . . . . . . . . . . . . .
1.2.1 Automaty skończone . . . . . . . . . . . . . . . . . . . . .
1.2.2 Maszyny Turinga . . . . . . . . . . . . . . . . . . . . . . .
1.2.2.1 Określenie . . . . . . . . . . . . . . . . . . . . .
1.2.2.2 Maszyny Turinga jako akceptory . . . . . . . . .
1.2.2.3 Maszyny Turinga jako generatory . . . . . . . .
1.2.2.4 Znaczenie maszyn Turinga . . . . . . . . . . . .
1.2.2.5 Rozstrzygalność problemów i obliczalność funkcji
1.2.3 λ-rachunek . . . . . . . . . . . . . . . . . . . . . . . . . .
1.2.3.1 Określenia i konwersje . . . . . . . . . . . . . . .
1.2.3.2 λ-rachunek jako model obliczeń . . . . . . . . . .
1.3 O językach i gramatykach . . . . . . . . . . . . . . . . . . . . . .
1.3.1 Określenie gramatyk . . . . . . . . . . . . . . . . . . . . .
1.3.2 Przykłady . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.3.3 Hierarchia Chomsky’ego gramatyk formalnych . . . . . .
1.3.4 Generatory a akceptory . . . . . . . . . . . . . . . . . . .
1.3.5 Znaczenie praktyczne klas języków formalnych . . . . . .
1.4 Złożoność obliczeniowa . . . . . . . . . . . . . . . . . . . . . . . .
1.4.1 Czas działania programu jako funkcja wielkości danych . .
1.4.1.1 Definicja złożoności czasowej . . . . . . . . . . .
1.4.1.2 Przykłady . . . . . . . . . . . . . . . . . . . . .
1.4.2 Asymptotyczna klasyfikacja algorytmów . . . . . . . . . .
1.4.2.1 Asymptotyczna klasyfikacja funkcji . . . . . . .
1.4.2.2 Czas asymptotyczny a prędkość działania . . . .
1.4.3 Złożoność problemów . . . . . . . . . . . . . . . . . . . .
1.4.4 Hipoteza P =
6 N P i jej konsekwencje . . . . . . . . . . . .
1.5 Logiczne podstawy informatyki . . . . . . . . . . . . . . . . . . .
1.5.1 Logika klasyczna . . . . . . . . . . . . . . . . . . . . . . .
1.5.1.1 Język logiki pierwszego rzędu . . . . . . . . . . .
1.5.1.2 Interpretacja języka w modelu . . . . . . . . . .
1.5.1.3 Teoria aksjomatyczna . . . . . . . . . . . . . . .
1.5.2 Logiki nieklasyczne . . . . . . . . . . . . . . . . . . . . . .
1.5.2.1 Logiki wielowartościowe . . . . . . . . . . . . . .
1.5.2.2 Krótka informacja o logikach modalnych . . . .
1
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
1
2
3
4
7
7
10
11
11
12
14
14
16
16
17
18
21
23
23
24
24
25
26
26
27
29
29
30
32
33
34
35
38
41
41
42
1.5.2.3 Krótka informacja o intuicjonizmie i konstruktywizmie
1.6 Dowodzenie poprawności programów . . . . . . . . . . . . . . . . . . . .
1.6.1 Programowanie z niezmiennikami . . . . . . . . . . . . . . . . . .
1.6.2 Aksjomatyka Hoare’a poprawności częściowej . . . . . . . . . . .
1.6.2.1 Aksjomaty i reguły . . . . . . . . . . . . . . . . . . . .
1.6.2.2 Poprawność częściowa i całkowita . . . . . . . . . . . .
1.6.3 Problem zgodności i pełności reguł . . . . . . . . . . . . . . . . .
1.6.3.1 Zgodność . . . . . . . . . . . . . . . . . . . . . . . . . .
1.6.3.2 Pełność . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.6.4 Znaczenie metody niezmienników . . . . . . . . . . . . . . . . . .
1.7 Tajniki rekursji . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.7.1 Dopuszczalność definicji kołowych . . . . . . . . . . . . . . . . .
1.7.2 Stałopunktowa teoria rekursji . . . . . . . . . . . . . . . . . . . .
1.7.2.1 Zbiory łańcuchowo zupełne . . . . . . . . . . . . . . . .
1.7.2.2 Przekształcenia ciągłe i ich punkty stałe . . . . . . . . .
1.8 Co jeszcze? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.8.1 Klasyfikacja języków i problemów nierozstrzygalnych . . . . . . .
1.8.2 Klasyfikacja języków i problemów rozstrzygalnych . . . . . . . .
1.8.3 Formalna semantyka . . . . . . . . . . . . . . . . . . . . . . . . .
1.8.4 Teoria typów . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.8.5 Specyfikacje . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.8.6 Zastosowania teorii kategorii . . . . . . . . . . . . . . . . . . . .
1.8.7 Sieci Petri’ego . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.8.8 Topologia algebraiczna . . . . . . . . . . . . . . . . . . . . . . . .
Literatura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
43
44
45
46
47
48
51
51
52
53
54
54
55
56
57
60
61
61
61
61
62
62
62
63
63
Rozdział 1
Matematyczne korzenie informatyki
Stefan Sokołowski
Kontekst
Matematyczne podstawy informatyki to dziedzina bardzo szeroka i zróżnicowana. Nie
sposób jej zaliczyć do konkretnego działu matematyki, ponieważ jej zakres obejmuje: logikę
formalną, teorię kategorii, teorię mnogości, algebrę, topologię, działy analityczne i wiele zagadnień uzupełniających, z których korzystają specjalizowane rozwiązania i aplikacje informatyczne. Ponadto informatyka wytworzyła własne teorie, których motywacja jest komputerowa,
ale metodą badawczą jest dowodzenie twierdzeń z pełnym matematycznym rygoryzmem. W
ten sposób informatyka przyczynia się do rozwoju nauk matematycznych, tak jak matematyka
leży u podstaw wszelkich rozwiązań informatycznych.
W tak krótkim opracowaniu nie sposób omówić wszystkich obszarów zainteresowań matematyki komputerowej. Rozdział ten należy potraktować jako szkic rozważań na temat matematycznych podstaw informatyki, który może być punktem wyjścia do pogłębionych studiów
w tym obszarze, czemu powinny służyć odnośniki literaturowe wykorzystane w tekście.
Cel
Wprowadzenie do zagadnień i pojęć związanych z matematycznymi podstawami informatyki. Wskazanie źródeł literaturowych, dostępnych na polskim rynku księgarskim lub w
czytelniach, które rozszerzają naszkicowane w rozdziale tematy.
Plan zagadnień
• Matematyczne modele obliczeń
• O językach i gramatykach
• Złożoność obliczeniowa
• Logiczne podstawy informatyki
• Dowodzenie poprawności programów
• Tajniki rekursji
• Pozostałe dziedziny matematycznych podstaw informatyki
1
Znaczenie
Matematyka leży u podstaw nauk informatycznych. Nie jest możliwe zrozumienie funkcjonowania lub uczestniczenie w planowaniu przedsięwzięć informatycznych, bez poznania
chociażby podstawowych zagadnień prezentowanych w tym rozdziale. Zdecydowanie łatwiej
będzie też czytelnikowi zrozumieć ideę projektowania i implementacji zaawansowanych systemów informatycznych w kontekście logiki i modelowania matematycznego.
Zagadnienia poprzedzające
• brak
Prace klasyczne
• Barendregt H. P. [1984], The lambda calculus. Its syntax and semantics, Elsevier Science
B.V.
• Cormen T. H., Leiserson C. E., Rivest R. L. [2001], Wprowadzenie do algorytmów, Warszawa.
• Dembiński P., Małuszyński J. [1981], Matematyczne metody definiowania języków programowania, Warszawa.
• Hopcroft J. E., Motwani R., Ullman J. D. [2005], Wprowadzenie do teorii automatów,
języków i obliczeń, Warszawa.
• Manna Z. [1974], Mathematical theory of computation, McGraw Hill.
• Nielson H.R., Nielson F. [1992], Semantics with applications: a formal introduction,
John Wiley & Sons.
Matematyczne korzenie informatyki
1.1
Wstęp
Główną wartością wnoszoną przez matematykę do informatyki jest, tak jak i w innych jej
zastosowaniach, metoda abstrakcji. Dobra abstrakcja zniekształca rzeczywistość, ale czyni to
w taki sposób, że łatwiej jest poradzić sobie z tą rzeczywistością. Jako analogia może tu służyć
kartografia: dobra mapa daje znacznie lepsze możliwości radzenia sobie w terenie niż zdjęcie
lotnicze. Na zdjęciu kolory zależą od pory roku, dróg przechodzących przez las nie widać, bo
są zasłonięte drzewami, za to za każdym statkiem na morzu ciągnie się kilwater. Na mapie
używa się kolorów do celów ważniejszych, drogi „wyjmuje się spod drzew”, statki i ich ślady się
usuwa, a ponadto dodaje się nieistniejące w naturze napisy, poziomice i siatkę geograficzną.
W wyniku otrzymuje się obraz terenu całkiem abstrakcyjny, nierzeczywisty i umowny, ale
właśnie dlatego użyteczniejszy niż oryginalne zdjęcie. Coś podobnego czyni matematyka w
informatyce: dostarcza abstrakcyjnych map rzeczywistości obliczeniowej.
2
Na matematycznych mapach komputery są znacznie prostsze i bardziej idealne niż w rzeczywistości. Najczęściej mają nieograniczoną pamięć, dysponują nieograniczonym czasem na
wykonanie obliczeń, operacje arytmetyczne na dowolnie dużych liczbach wykonują bezbłędnie
i w czasie jednostkowym, itp. Ściśle biorąc, to wszystko jest nieprawda, ale chwilowe zapominanie np. o tym, że komputer wykonuje niedokładnie operacje na liczbach rzeczywistych,
często pozwala nam się skupić na problemach bardziej istotnych.
Matematyczne podstawy informatyki to obecnie dziedzina bardzo szeroka i zróżnicowana;
trudno o specjalistę, który orientowałby się w jej całości. Nie sposób jej zaliczyć do jakiegoś
konkretnego działu matematyki, bo czerpie po trochu ze wszystkiego, od logiki formalnej i
teorii kategorii, przez teorię mnogości, algebrę i topologię, do działów analitycznych. Ponadto
informatyka wytworzyła własne teorie, których motywacja jest komputerowa, ale metodą
badawczą jest dowodzenie twierdzeń z pełnym matematycznym rygoryzmem.
W tym krótkim opracowaniu nie ma miejsca na omówienie, choćby szkicowe, wszystkich
obszarów zainteresowań matematyki komputerowej. Również w dziedzinach wspomnianych
zieją wielkie nieomówione luki. Mam nadzieję jednak, że przekona ono czytelnika, że infomatyka potrzebuje rozważań matematycznych; jak też, że dostarcza matematyce problemów
badawczych.
W kolejnych rozdziałach omawiam szkicowo problemy matematycznych korzeni informatyki. W ostatnim rozdz. 1.8 tylko już wymieniam to, co nie znalazło miejsca we wcześniejszym
tekście.
Większość pozycji w cytowanej bibliografii stanowią nieliczne podręczniki, w miarę możności dostępne na polskim rynku księgarskim lub przynajmniej w czytelniach1 . Uznałem za
niewłaściwe zasypywanie czytelnika stosem literatury, po którą i tak nie sięgnie. Artykuły z
czasopism naukowych pojawiają się tylko w ostateczności. Ten wybór podyktowany został
względami na potrzeby osób, które chciałyby zapoznać się z matematycznymi korzeniami
informatyki, ale nie planują zostać specjalistami w tej dziedzinie.
Omówienia poruszanych tu tematów stosunkowo łatwo można znaleźć w internecie, choćby w Wikipedii. Jednak do uzyskanych stamtąd informacji należy podchodzić ostrożnie i
starannie oddzielać ziarno od plew.
1.2
Matematyczne modele obliczeń
Teoretycznego badania własności obliczeń dokonuje się na modelach matematycznych.
Stanowią one uproszczone idealizacje prawdziwych komputerów, oczyszczone ze szczegółów
odwracających uwagę.
W tym rozdziale zostaną omówione dwa spośród wielu rozważanych modeli:
• automaty skończone — bardzo proste z ograniczonymi możliwościami, oraz
• maszyny Turinga — model obejmujący „wszystkie”2 własności obliczeń.
Innego rodzaju modelem obliczeń, związanym z powyższymi, ale już niebędącym idealizacją urządzenia liczącego, jest
1
Sprawdziłem ich obecność w bibliotece Uniwersytetu Gdańskiego. Mam nadzieję, że skoro tam są, to w
innych bibliotekach uniwersyteckich też będą.
2
Matematyk nie może upierać się, że modelowane są naprawdę wszystkie własności fragmentu rzeczywistości; stąd cudzysłów. Niżej (podrozdz. 1.2.2.4) zostanie wyjaśnione, w jakim sensie została użyta ta przenośnia.
3
• λ-rachunek — czyli rachunek funkcji rekurencyjnych.
Zostanie on również pokrótce przedstawiony.
Jako podstawowy podręcznik do automatów skończonych (podrozdz. 1.2.1) i maszyn Turinga (podrozdz. 1.2.2), a także do rozdziału 1.3 może służyć Hopcroft, Motwani i Ullman [10].
Głównym podręcznikiem do λ-rachunku (podrozdz. 1.2.3) jest Barendregt [4].
1.2.1
Automaty skończone
W najprostszej swojej postaci automat skończony służy do definiowania języków, czyli do
odróżniania słów spełniających pewne kryteria od słów ich niespełniających.
Definicja 1.1
Deterministycznym automatem skończonym (DFA3 ) nazywamy piątkę
A = hQ, Σ, δ, q0 , F i
w której
• Q jest skończonym zbiorem t.zw. stanów ;
• Σ jest skończonym zbiorem t.zw. liter ; zbiór Σ jest zwykle nazywany alfabetem;
• δ : Q × Σ → Q to t.zw. funkcja przejścia automatu; nieformalnie mówiąc, wyznacza ona
stan, do jakiego automat ma przejść z danego stanu po wczytaniu danej litery;
• q0 ∈ Q jest wyróżnionym stanem początkowym;
• F ⊆ Q jest wyróżnionym zbiorem stanów końcowych lub akceptujących.
Należy zwrócić uwagę, że stan początkowy jest zawsze dokładnie jeden, podczas gdy stanów końcowych jest cały zbiór; w szczególnym (nieciekawym) przypadku może to być zbiór
pusty ∅, albo pełny zbiór Q.
3
Od ang. Deterministic Finite Automaton.
4
Przykład 1.2
Załóżmy, że automat A = hQ, Σ, δ, q0 , F i dany jest następująco: n
o
def
• Q=
1, 2, 3, Śm
def
• Σ=
{a, b, c}
• funkcja δ będzie dana tabelką
δ
1
2
3
Śm
a
b
.....
...... .........
...
...
...
...
..
.
...
.
..
....
..
...
..
..
...
.
.
...
..
c
^
- 1
-
a
I
a
c
c
2 Śm 1
Śm 3 Śm
2 Śm 1
Śm Śm Śm
2
b
3
b
N
a, c
b
?
Śm
• q0 = 1
def
• F =
{1}
def
...
...
...
..
..
..
..
....
..
..
..
...
.
...
..
.
.
....
.
.
.
...... ......
.......
a, b, c
Rysunek obok przedstawia ten właśnie automat.
Kółka to stany. Kółko ze strzałeczką to stan początkowy.
Kółka podwójne to stany końcowe — w tym przypadku jest tylko jeden stan końcowy, w
dodatku pokrywający się ze stanem początkowym.
Funkcja przejścia oznaczona jest strzałkami między stanami, na strzałkach stoją litery
alfabetu. Ponieważ to jest funkcja, musi mieć określony wynik dla każdej pary hstan, literai,
co rysunkowo oznacza, że z każdego kółka musi wychodzić po jednej strzałce dla każdej litery
alfabetu.
Stan Śm to „śmietnik” zbierający wszystkie niepotrzebne strzałki; wyjść od niego można
tyko z powrotem do śmietnika.
Automat można traktować jako planszę do „gry” wynikiem której jest akceptacja lub
odrzucenie danego słowa w złożonego z liter pochodzących z alfabetu Σ. Gra zaczyna się od
pionka stojącego w stanie początkowym (w przykładzie 1.2 — na polu 1); następnie pionek
jest przesuwany na kolejne pola po strzałkach odpowiadającym kolejnym literom w, aż do
wyczerpania wszystkich liter. Jeśli to doprowadziło pionek do stanu akceptującego, to słowo
„wygrało”, czyli zostało zaakceptowane; jeśli do stanu nieakceptującego, to słowo zostało
odrzucone4 .
Łatwo widać, że w przykładzie 1.2:
• słowo abba prowadzi do stanu Śm, więc jest odrzucone;
• słowo abca prowadzi do stanu 2, więc również jest odrzucone;
• słowo ababc prowadzi do stanu 1, więc jest akceptowane;
• słowo puste (nie zawierające żadnej litery) prowadzi do stanu 1, więc jest akceptowane;
takie słowo będziemy oznaczać przez ε.
4
Porządna matematyczna definicja akceptowania słowa przez automat wykorzystuje funkcję przejścia δ z
definicji automatu i nie wspomina o ruchach pionka.
5
Definicja 1.3
Przez język automatu skończonego A rozumiemy zbiór L(A) wszystkich słów akceptowanych przez ten automat.
Przykład 1.4
Językiem automatu A z przykładu 1.2 jest język L(A) złożony ze słów postaci
w1 c w2 c . . . wn c
gdzie każde wi ma postać
ab
. . ab}
| .{z
wielokrotnie powtórzone
Dla danego alfabetu Σ łatwo skonstruować automaty definiujące
• język pusty, niezawierający żadnego słowa (ozn. ∅);
• język pełny, zawierający wszystkie słowa dające się napisać literami z Σ (ozn. Σ∗ );
• dla każdego skończonego zbioru słów w1 , . . . , wn ∈ Σ∗ , język skończony, składający się
dokładnie ze słów w1 , . . . , wn (ozn. {w1 , . . . , wn }).
Jak widać z przykładu 1.4, język nieskończony różny od pełnego też może być definiowany
automatem skończonym; ale istnieją języki nieskończone, do których nie istnieją automaty
definiujące. Najprostszymi przykładami są te, w których istnieją jakieś nietrywialne zależności
arytmetyczne między liczbami występujących w nich liter.
Przykładn1.5 o
Język ak bk k ­ 0 5 , który składa się ze słów zaczynających się od ciągu liter a i kończących się ciągiem liter b tej samej długości, nie jest definiowany żadnym automatem skończonym.
Definicja 1.6
Językiem regularnym nazywamy zbiór słów definiowany jakimś automatem skończonym.
Języki programowania komputerów nie są regularne — do ich analizowania potrzeba narzędzi bardziej skomplikowanych niż automaty skończone. Ale do samego sprawdzenia, czy
podstawowe jednostki programu komputerowego, takie jak słowa kluczowe, identyfikatory,
liczby itp., są napisane poprawnie, wykorzystuje się automaty skończone, bo język, złożony z
dopuszczalnych jednostek podstawowych, jest regularny. Regularne są też języki definiowane
wzorcami, do których mają pasować słowa; do ich analizy też wystarczy używać automatów.
Górny indeks przy literze oznacza liczbę powtórzeń litery; np. b4 oznacza słowo bbbb. Konsekwentnie, bk
oznacza słowo b| .{z
. . }b.
5
k razy
6
1.2.2
Maszyny Turinga
Jeśli język jest na tyle skomplikowany, że dla rozpoznania jego słów nie wystarczy już
przymierzanie ich do wzorca i potrzebne są obliczenia, to korzystamy z innych modeli.
Najpotężniejszym prostym modelem obliczeń, jakim dysponują matematycy, jest t.zw.
maszyna Turinga, zaproponowany przez Alana Turinga wlatach 1930. Można powiedzieć,
że składa się ona z automatu skończonego oraz nieskończonego „notesu”, w którym można
zapisywać informacje potrzebne do rozpoznawania słów. Potęga maszyn Turinga bierze się
właśnie z nieskończoności tego notesu. Tradycyjnie mówi się o nieskończonej w obie strony
„taśmie”, którą maszyna może przewijać w przód i w tył, i na której może zapisywać i
odczytywać litery.
1.2.2.1
Określenie
Definicja 1.7
Maszyną Turinga nazywamy siódemkę
gdzie
M = hQ, Γ, ., Σ, δ, q0 , F i
• Q jest skończonym zbiorem t.zw. stanów ;
• Γ jest skończonym zbiorem t.zw. symboli pomocniczych; zbiór Γ jest zwykle nazywany
alfabetem pomocniczym; symbole pomocnicze mogą być zapisywane na taśmie maszyny;
• . ∈ Γ jest symbolem pustej klatki; jest to jedyny symbol, który może występować w
nieskończenie wielu klatkach taśmy;
• Σ ⊆ Γ r {.} jest skończonym zbiorem t.zw. liter ; zbiór Σ jest zwykle nazywany alfabetem; akceptowane słowa napisane są literami tego alfabetu;
• δ : Q × Γ→
˜ Q × Γ × {L, R} to częściowa funkcja przejścia maszyny; nieformalnie mówiąc, dla każdego stanu i litery na taśmie pod głowicą wyznacza ona
– stan wynikowy,
– symbol, jaki należy napisać na taśmie pod głowicą zamiast symbolu aktualnie
obserwowanego,
– informację, czy głowicę należy przesunąć po taśmie w lewo czy w prawo;
ale może też nie dać żadnego wyniku (na tym polega jej częściowość);
• q0 ∈ Q jest wyróżnionym stanem początkowym;
• F ⊆ Q jest wyróżnionym zbiorem stanów końcowych lub akceptujących.
Podobnie jak w przypadku automatu skończonego, akceptowanie słów przez maszynę Turinga może być traktowane jak gra, w której pionek porusza się po planszy takiej jak na
rys. 1.1. Słowo do sprawdzenia powinno być napisane na taśmie, pozostałe klatki taśmy powinny być puste (wypełnione symbolem .), a głowica ustawiona na pierwszym pustym symbolu po prawej stronie od tego słowa. Jeśli w którymś momencie dojdzie do stanu i litery na
taśmie, dla których funkcja przejścia δ nie daje wyniku, to gra się kończy. Jeśli pion pozostaje wtedy w stanie końcowym, to słowo jest zaakceptowane, w przeciwnym przypadku jest
niezaakceptowane.
7
......
...
. . ...........................................
. .
......
.
........
.....
.
a
a
a
b
...........
.......................
-
0
b
.
b
........................
...........
........
.......
...
.
.
..
........................
...........
........
.......
...
. → ., L
-
1
. → ., L
6
.
b → b, R
.................................
.......
....
...
..
.
..
...
.
.
.
.......
.................................
8
k
.....
.......
....
...
....
....
......
.......... .............
..............
3
?
2
a → a,
R
..............................
.
.....
......
....
...
....
....
......
.......... .............
.............
3
. → ., R
b → ., L
b → b,
L
................................
6
b → b, R
6
. → ., L
5
-
7
6
.....
..
......
....
...
...
....
.......
...................................
3
a → ., R
a → a, L
a → a,
L
..............................
?
3
. → ., R -
4
n
Rys. 1.1: Maszyna Turinga akceptująca język ak bk k ­ 0
Przykład 1.8
Niech
•
•
•
•
•
def
Q=
{0, 1, 2, 3, 4, 5, 6, 7, 8}
def
Γ = {a, b, .}
def
Σ=
{a, b}
def
q0 = 0
def
F =
{8}
8
o
(do przykładu 1.8).
• funkcja δ będzie dana tabelką
δ
0
1
2
3
4
5
6
7
8
a
b
.
h1, ., Li
h2, ., Li
h3, a, Li h2, b, Li
h3, a, Li
h4, ., Ri
h5, ., Ri
h5, a, Ri h6, b, Ri h7, ., Ri
h6, b, Ri h1, ., Ri
h8, ., Ri
W przeciwieństwie do automatów skończonych, tabelka dla funkcji przejścia nie jest
całkowicie wypełniona. Puste miejsca oznaczają nieokreśloność funkcji δ.
Ta maszyna Turinga jest przedstawiona na rys. 1.1. W jego górnej części pokazana jest
taśma maszyny z wpisanym słowem i głowicą ustawioną na pierwszym pustym symbolu za
słowem.
Główna część rysunku różni się od automatu skończonego tylko postacią etykiet na strzałkach. W automacie skończonym były to pojedyncze litery, w maszynie Turinga strzałka odpowiadająca zależności
δ(q, γ) = hq1 , γ1 , ki
(czyli trójce hq1 , γ1 , ki stojącej w wierszu q i kolumnie γ tabelki) ma postać
q
γ → γ1 , k -
q1
Nieformalnie oznacza ona, że jeśli maszyna jest w stanie q, a na taśmie pod głowicą stoi
symbol γ, to maszyna ma
• przejść do stanu q1 ,
• zastąpić na taśmie symbol pod głowicą symbolem γ1 , oraz
• przesunąć głowicę po taśmie w kierunku k.
Łatwo się przekonać, że powyższa maszyna wielokrotnie przesuwa głowicę po słowie napisanym na taśmie od prawej do lewej i z powrotem. Za każdym dojściem do początku wymazuje
jedną literę a (czyli zamienia ją na symbol pusty .), a za każdym dojściem do końca wymazuje
jedną literę b. Dojście do stanu końcowego 8, czyli zaakceptowanie słowa, możliwe jest tylko
wtedy, gdy tych wymazań jest tyle samo, czyli jeśli słowo na taśmie było postaci ak bk . Po
zakończeniu działania maszyny i zaakceptowaniu słowa, na taśmie pozostaje słowo puste.
Definicja 1.9
Przez język maszyny Turinga M rozumiemy zbiór L(M ) wszystkich słów akceptowanych
przez M .
9
1.2.2.2
Maszyny Turinga jako akceptory
Automat skończony w każdym kroku pracy „zjada” jedną literę z wejścia, a gdy „zje”
wszystkie, to się zatrzymuje. Natomiast krok pracy maszyny Turinga może zmniejszyć liczbę symboli na taśmie przez zamianę którejś na symbol pusty, ale może też ją powiększyć
przez zamianę symbolu pustego na niepusty. W dodatku jej zatrzymywanie nie jest związane
z wyczerpaniem liter — nawet po całkiem pustej taśmie6 maszyna może nadal się poruszać.
Wobec tego maszynie Turinga może się przydarzyć, że sprawdzanie słowa nigdy się nie zakończy („ślepa pętla”). Ten fakt trzeba wziąć pod uwagę przy klasyfikowaniu języków ze względu
na działania maszyn Turinga.
Definicja 1.10
Zbiór słów L ⊆ Σ∗ nazywamy językiem rekurencyjnie przeliczalnym, jeśli istnieje maszyna
Turinga M taka, że L = L(M ). Zbiór słów L ⊆ Σ∗ nazywamy językiem rekurencyjnym jeśli
istnieją maszyny Turinga M + i M − takie, że
L = L(M + ) i Σ∗ r L = L(M − )
O różnicy między językami rekurencyjnymi i rekurencyjnie przeliczalnymi zostanie powiedziane niżej. Na razie wystarczy zauważyć, że każdy język rekurencyjny jest rekurencyjnie
przeliczalny.
Zajmijmy się teraz związkami języków rekurencyjnych z językami regularnymi, czyli językami automatów. Poniższe twierdzenie i następujący po nim przykład pokazują, że maszyny
Turinga są potężniejszym narzędziem do akceptowania języków niż automaty skończone:
Twierdzenie 1.11
Dla każdego automatu skończonego A istnieją maszyny Turinga M + i M − takie, że
L(A) = L(M + ) i Σ∗ r L(A) = L(M − )
Przykład 1.12
Maszyna Turinga z przykładu 1.8 określa język
n
ak bk k ­ 0
o
o którego nieregularności już wiemy (por. przykład 1.5).
Nietrudno zmienić ją na maszynę akceptującą uzupełnienie tego języka (zamienić stany
akceptujące na nieakceptujące i odwrotnie). Wobec tego ten język jest rekurencyjny.
Wniosek 1.13
Każdy język regularny jest rekurencyjny. Nie każdy język rekurencyjny jest regularny.
6
Czyli wypełnionej symbolami pustymi .
10
1.2.2.3
Maszyny Turinga jako generatory
Ponieważ maszyny Turinga w trakcie swojej pracy zmieniają zapis na taśmie, można ich
używać nie tylko do akceptowania słów, ale również do wykonywania obliczeń.
-
0 → 1, R-
A
C
. → ., L-
D
R
1,
→
1 → 0, L
0
Przykład 1.14
Maszyna na rysunku obok wykonuje
dodawanie 1 do liczby binarnej. Zakładamy, że oryginalna liczba, ciąg zer i jedynek, jest zapisana na taśmie, a głowica
ustawiona jest na ostatniej (najmniej znaczącej) cyfrze. Maszyna zmienia ten zapis i
kończy z głowicą znowu na ostatniej znaczącej cyfrze — temu służy pętelka przy
stanie C oraz strzałka z C do D. Stan B
oznacza, że jest jeszcze jedynka „w pamięci” (przeniesienie) do dodania do następnej cyfry.
?
...
.
..
...
..
..
...
..
.
....
...
..
...
...
...
..
.
.
....
.
.
.
...... ......
......
0 → 0, R
1 → 1, R
B
...
...
..
..
..
...
..
....
...
...
..
...
..
.
.
...
..
.
....
.
..................
1 → 0, L
Konstrukcja maszyn Turinga, wykonujących bardziej złożone obliczenia, jest żmudna, a
powstałe maszyny mają sporo stanów. Proponuję czytelnikowi skonstruowanie maszyny dodającej dwie liczby. Proszę nie dziwić się, że będzie potrzeba wielu komend, których jedyną
rolą będzie przenoszenie informacji z jednego miejsca taśmy w inne.
1.2.2.4
Znaczenie maszyn Turinga
Okazuje się, że maszyny Turinga, mimo swojej prostoty pojęciowej, potrafią policzyć
wszystko, co w ogóle może zostać policzone. Stanowi to treść t.zw. „tezy Churcha-Turinga”:
Teza 1.15
Dla każdej funkcji f , której wartości dają się efektywnie obliczać dowolnymi sposobami,
istnieje maszyna Turinga obliczająca f .
Ta teza oczywiście nie jest twierdzeniem i nie daje się dowieść, bo nie posiadamy żadnej
ogólnej definicji „liczenia” niezależnej od pojęcia maszyny Turinga oraz od innych mechanizmów jej równoważnych. Jest to raczej teza filozoficzna, poparta tym, że jak dotąd nikomu nie
udało się wymyślić żadnego przykładu czegoś, co sensownie można by nazwać obliczaniem,
co nie byłoby wykonalne na jakiejś maszynie Turinga.
W szczególności wszystko, co daje się zaprogramować w dowolnym języku na komputer
o dowolnej architekturze, daje się również policzyć przy pomocy jakiejś maszyny Turinga.
Zachodzi też zależność odwrotna: jeśli abstrahować od skończoności pamięci komputerów, to
każdy imperatywny język programowania7 ogólnego zastosowania, taki jak Pascal, C lub Java,
ma pełną moc maszyn Turinga. Natomiast niekoniecznie mają ją języki wąsko specjalizowane,
takie jak HTML (język opisu witryn internetowych) lub SQL (język zapytań baz danych).
Znaczenie poznawcze maszyn Turinga wynika z tego, że
• stanowią one uniwersalny model obliczeń, oraz
7
Większość języków programowania komputerów to języki imperatywne. O nieimperatywnych językach
programowania będzie jeszcze wzmianka w rozdz. 1.2.3.
11
• są koncepcyjnie proste, znacznie prostsze niż istniejące języki programowania, czy architektury istniejących komputerów.
Dlatego każdy nowy model obliczeń zwyczajowo porównujemy z maszynami Turinga. Jeśli
okaże się, że potrafi on zasymulować każdą maszynę Turinga, to uważamy, że lepiej już być
nie może.
Definicja 1.16
Każdy model obliczeń, równoważny maszynom Turinga, nazywamy zupełnym w sensie
Turinga.
Oczywiście automaty skończone nie są zupełne w sensie Turinga.
1.2.2.5
Rozstrzygalność problemów i obliczalność funkcji
Łatwo sprawdzić, że różnych maszyn Turinga jest ℵ0 , czyli tyle, co liczb naturalnych.
Z drugiej strony, dla niepustego i skończonego alfabetu Σ, zbiór Σ∗ wszystkich słów nad
tym alfabetem ma moc ℵ0 , a języków zawierających te słowa jest 2ℵ0 = c, czyli continuum, to
znaczy znacznie więcej niż maszyn Turinga. Tak więc nie każdy język posiada rozpoznającą
go maszynę; i nie do każdego można skonstruować rozpoznający go program komputerowy.
Każdy t.zw. problem decyzyjny, czyli pytanie, na które maszyna (lub komputer) miałyby
odpowiedzieć „tak” lub „nie”, daje się sprowadzić do sprawdzenia należenia słów do pewnego
języka8 . Różnica między mocą zbioru języków oraz mocą zbioru maszyn Turinga określa
granicę możliwości komputerów: zawsze będą one potrafiły odpowiedzieć tylko na ℵ0 pytań,
czyli na niewielką część tego, czego nie wiadomo.
Podobnie ma się sprawa z obliczaniem wartości funkcji. Wszystkich funkcji z N do N jest
ℵ0
ℵ0 = c, czyli znowu continuum. Wobec tego daleko nie wszystkie funkcje dadzą się zaprogramować za pomocą maszyny Turinga; w związku z tym również nie dadzą się zaprogramować
w żadnym języku na żadnym komputerze.
Tych nierozstrzygalnych problemów i nieobliczalnych funkcji jest znacznie więcej niż rozstrzygalnych i obliczalnych, jednak wskazanie konkretnego takiego problemu lub funkcji nie
jest proste. Poniżej podany jest szkic konstrukcji ważnego problemu nierozstrzygalnego.
Definicja 1.17
Problem L (równoważnie: podzbiór L ⊆ Σ∗ ) nazywamy rozstrzygalnym jeśli istnieje maszyna Turinga M , taka że L = L(M ). Analogicznie, funkcję nazywamy obliczalną lub rekurencyjną, jeśli istnieje obliczająca ją maszyna Turinga.
Twierdzenie 1.18
Problem stopu maszyny Turinga, to znaczy problem, czy dana maszyna Turinga M kończy
obliczenie dla danego słowa w ∈ Σ∗ na taśmie, jest nierozstrzygalny. T.zn. nie istnieje maszyna
Turinga S, która potrafiłaby zbadać dowolną maszynę Turinga M oraz dowolne dane w (obie
rzeczy jakoś zakodowane na taśmie wejściowej dla S), zatrzymać się i poprawnie odpowiedzieć
na pytanie, czy M zatrzymuje się na w.
8
Terminy język rekurencyjny oraz problem rozstrzygalny są synonimami. Oba oznaczają akceptację przez
jakąś maszynę Turinga zarówno każdego słowa z języka (instancji problemu dla konkretnych danych), jak jego
dopełnienia (zaprzeczenia).
12
Dowód tw. 1.18 (szkic):
Łatwiej mówić o programach komputerowych analizujących inne programy, niż o maszynach Turinga, więc tego będę się trzymał. Ale dla pełnego dowodu należałoby rozważać
maszyny Turinga.
w
P
Załóżmy więc, że problem stopu jest rozstrzygalny; mamy więc taki
program komputerowy S w jakimś języku programowania, który jako
N dane bierze dowolny tekst programu P w tym języku oraz dowolne
dane w, chwilę działa, następnie zatrzymuje się, i drukuje poprawną
S
odpowiedź na pytanie, czy program P na danych w kończy obliczenie,
czy też działa nieskończenie długo.
N
Skonstruujmy program R przez dobudowanie do programu S:
• „podwajacza danych”, który tworzy dwie kopie podanych mu
na wejściu danych, i doczepmy go do obu wejść programu; w
ten sposób program R będzie wczytywał tekst programu P i
sprawdzał, czy program P zatrzymuje się na swoim własnym
tekście;
• ślepą pętlę, i doczepmy ją do wyjścia „stop”.
stop
R:
pętla
P
.....
.................. ....................
.....
..
N
S
N
.
Wobec tego, jeśli program P zatrzymuje się na swoim tekście, to pro..... ..........
... ..
pętla
........
.......
gram R na tekście programu P się zapętla; a w przeciwnym razie
zatrzymuje się (i drukuje wynik „pętla”).
I w końcu sprawdźmy, jak zachowuje się program R na swoim własnym tekście; to znaczy
wykonajmy go z R zamiast P . Są dwie możliwości:
• albo R zatrzymuje się na R; wtedy S powinien dać wynik „stop”; tak więc R wchodzi
w ślepą pętlę — sprzeczność;
• albo R zapętla się na R; wtedy S powinien dać wynik „pętla”, ważne, że zatrzymać się
i dać wynik; tak więc R zatrzymuje się — znowu sprzeczność.
W każdym przypadku dostajemy sprzeczność. To dowodzi, że oryginalny program S nie
może istnieć.
Załóżmy, że istnieje funkcja s(p, w), która na każdych danych zatrzymuje się i stwierdza,
czy obliczenie p(w) jest skończone. Definiujemy funkcję
fun r(p) :
if s(p, p) then {while true do
else return true;
fi
od} /∗ ślepa pętla ∗/
Czy obliczenie r(r) się zatrzymuje?
Rys. 1.2: Programistyczna wersja dowodu tw. 1.18.
Wniosek 1.19
Żaden język, dla którego problem stopu jest rozstrzygalny, nie może mieć pełnej mocy
maszyn Turinga (czyli: nie może być zupełny w sensie Turinga).
13
Twierdzenie 1.18 oraz wniosek 1.19 wykluczają możliwość, że kiedyś, w wyniku rozwoju
informatyki, programiści będą mogli wykrywać niebezpieczeństwo ślepych pętli w programach
za pomocą innych programów; przynajmniej jeśli teza 1.15 jest prawdziwa. Jest to możliwe
tylko dla bardzo „słabych” języków, w których nie da się wiele zaprogramować.
Ale to twierdzenie nie oznacza, że nigdy nie możemy wiedzieć, czy dany program się
zatrzyma. Nie istnieje metoda ogólna, stosująca się do wszystkich programów, ale w konkretnym przypadku możemy umieć zbadać własności konkretnego programu9 . Twierdzenie 1.18
nie wyklucza też istnienia programu, który zawsze prawidłowo orzeka o zatrzymywaniu się
innych programów, ale z rzadka poddaje się i mówi „nie wiem”.
Każda nauka powinna znać swoje możliwości i ograniczenia. Twierdzenie 1.18 ma poważne konsekwencje w ustalaniu granic informatyki. Z nierozstrzygalności własności stopu dla
maszyn Turinga wyprowadza się bardzo wiele innych twierdzeń o nierozstrzygalności. Nierozstrzygalne są między innymi
• uniwersalny problem stopu — czy dany program zatrzymuje się dla wszystkich danych;
• problem równoważności programów — czy dwa dane programy dają te same wyniki na
każdych danych;
• problem Rice’a — czy funkcja obliczana przez dany program ma daną własność (dla
każdej „sensownej” nietrywialnej własności).
Twierdzenie 1.18 ma bardzo bliski związek ze słynnym twierdzeniem Gödla o niezupełności aksjomatyzacji arytmetyki. Oba twierdzenia są negatywne, czyli stwierdzają niemożność
zrobienia czegoś. I dla dowodu obu stosuje się t.zw. rozumowanie przekątniowe, które trochę
przypomina wyciąganie siebie samego za włosy z bagna. Nietrudno jest wykazać równoważność obu twierdzeń.
1.2.3
λ-rachunek
W rozdz. 1.2.2.5 rozważaliśmy przede wszystkim rozstrzyganie problemów, chociaż zostało
również wspomniane, że podobne rozważania stosują się do obliczania wartości. Modelem, specjalnie dostosowanym do funkcji obliczalnych (rekurencyjnych), jest t.zw. λ-rachunek. Został
on zaproponowany w latach 1930. przez Alonzo Churcha w ramach badań nad podstawami
matematyki.
1.2.3.1
Określenia i konwersje
λ-rachunek operuje na wyrażeniach postaci λx.e . Takie wyrażenie oznacza, nieformalnie mówiąc, funkcję, która dla argumentu x daje wynik e — oczywiście w e może (ale nie
musi) występować zmienna x. Np. λx. x · x oznacza funkcję podnoszącą swój argument do
kwadratu.
Występujący na początku wyrażenia symbol λ jest operatorem wiążącym zmienne, takim
jak np. kwantyfikatory. To oznacza, że zmienna, występująca bezpośrednio za symbolem λ,
nie jest widoczna z zewnątrz wyrażenia i może bez zmiany jego znaczenia zostać zastąpiona
inną zmienną10 ; np. wyrażenia λx. x · x i λy. y · y oznaczają tą samą funkcję. Taka zmienna
9
10
Więcej o dowodzeniu stopu konkretnych programów będzie w rozdz. 1.6.2.2.
Taką zmianę nazwy zmiennej, t.zw. α-konwersję, należy wykonywać ostrożnie.
14
nazywa sią związana. Pozostałe zmienne, niewystępujące pod lambdą, są wolne. Wartość
wyrażenia zależy tylko od wartości zmiennych wolnych.
W zapisie λ-wyrażeń przyjmuje się umowy, że
• z dwóch wyrażeń, stojących obok siebie, pierwsze oznacza funkcję a drugie argument,
na którym ta funkcja działa; np. f x oznacza zastosowanie funkcji f do argumentu x;
• zastosowanie funkcji wiąże do lewej; tak więc f g x oznacza (f g) x ;
• funkcja ma zawsze tylko jeden argument, ale za to wynikiem jej działania może znowu
być funkcja; więc właściwie nie powinniśmy pisać 3 · 4 tylko raczej · 3 4 — i to oznacza
zastosowanie funkcji · do liczby 3; a następnie zastosowanie wynikowej funkcji · 3 ,
mnożącej swój argument przez 3, do liczby 4.
Podstawowym działaniem na λ-wyrażeniach jest t.zw. β-konwersja — zamiana zastosowania do argumentu wyrażenia zaczynającego się od λ (czyli funkcji) na ciało funkcji z
argumentem wstawionym w miejsce wystąpień zmiennej spod λ:
β-konwersja:
(λx.e) e′ 7→ e[e′ /x]
Przez e[e′ /x] rozumie się wyrażenie e z e′ wstawionym za x 11 .
Przykład 1.20
Oto kilka przykładów zastosowania β-konwersji:
1. (λx. x · x) 3 7→ 3 · 3
Wynikiem zastosowania funkcji, podnoszącej do kwadratu, do liczby 3 jest wyrażenie
·33.
2. (λf. λy. f (f y)) (λx. x · x) 3
7→ (λy. (λx. x · x) ((λx. x · x) y)) 3
7→ (λy. (λx. x · x) (y · y)) 3
7→ (λy. (y · y) · (y · y)) 3
7→ (3 · 3) · (3 · 3)
Tutaj funkcja wyższego rzędu12 , polegająca na złożeniu swojego funkcyjnego argumentu
z sobą samym, zostaje zastosowana do funkcji „kwadrat”, a wynik zostaje zastosowany
do liczby 3.
3. (λx. f (x x)) (λx. f (x x))
7→ f ((λx. f (x x)) (λx. f (x x)))
7→ f (f ((λx. f (x x)) (λx. f (x x))))
7→ . . .
Jak widać, β-konwersja może doprowadzić do wyrażenia, które dalej daje się przekształcać przez β-konwersję, i ten ciąg przekształceń może być nieskończony. W tym przykładzie, jeśli wprowadzimy oznaczenie
def
Fix f =
(λx. f (x x)) (λx. f (x x))
to otrzymamy ciąg przekształceń
Fix f 7→ f (Fix f ) 7→ f (f (Fix f )) 7→ . . .
11
Dokładniej: e′ wstawia się tylko za wolne wystąpienia zmiennej x w e; i to w taki sposób, żeby nie
doprowadzić do związania żadnej zmiennej wolnej z e′ . W razie konfliktu nazw należy zastosować α-konwersję,
czyli przenazwować jakieś zmienne związane.
12
Czyli biorąca funkcję za argument.
15
W pkt. 3 zademonstrowany został operator rekursji Fix , o którym jeszcze będzie mowa w
rozdziale 1.7.
1.2.3.2
λ-rachunek jako model obliczeń
Dla przeprowadzanych obliczeń nie ma znaczenia, na jakim „materiale” je wykonujemy
— czy na palcach, czy na ciągach bitów, czy na funkcjach. Ważne jest tylko, żeby podstawy
spełniały te same aksjomaty początkowe. Otóż funkcje, wyrażane przez λ-rachunek, są materiałem na tyle bogatym, że można przy ich pomocy zdefiniować liczby naturalne i podstawowe
działania na nich13 .
Okazuje się, że możliwe jest symulowanie w ten sposób obliczenia dowolnej maszyny Turinga:
Twierdzenie 1.21
λ-rachunek jest zupełny w sensie Turinga.
Wszystko, co daje się efektywnie obliczyć dowolnymi sposobami, daje się policzyć przy
pomocy pewnych β-konwersji (por. Teza 1.15). Jak widać z przykładu 1.20 pkt. 3, przy pomocy
β-konwersji możliwe jest uzyskanie nieskończonego obliczenia.
β-konwersja jest bardzo prostym mechanizmem obliczania, sprowadza się do samego zastępowania zmiennych wyrażeniami, z ewentualnym przenazwowaniem zmiennych. Nie zawiera
wielu elementów, do których programiści są przyzwyczajeni — np. pętli. A jednak okazuje się,
że nadaje się do obliczeń równie dobrze jak kroki maszyny Turinga. W szczególności wszystkie rodzaje pętli można zastąpić rekursją, a rekursję wyrazić przy pomocy operatora Fix
zademonstrowanego w przykładzie 1.20 pkt. 3.
Z tego powodu na modelu λ-rachunku zbudowano całą klasę języków programowania,
t.zw. języków funkcyjnych. Najstarszym przykładem takiego języka jest LISP; do nowszych
należą Standard ML i Haskell. λ-wyrażenia występują też w wielu zwykłych imperatywnych
językach programowania, takich jak Ruby czy C#.
W niektórych językach funkcyjnych (np. w Standard ML) stosuje się bardziej skomplikowany t.zw. typowany λ-rachunek . Bez wchodzenia w szczegóły należy zauważyć, że przy
klasycznym rozumieniu pojęcia typu wyrażenia, taki rachunek nie jest już zupełny w sensie
Turinga. Wynika to z tego, że operator rekursji Fix z przykładu 1.20 pkt. 3 zawiera samozastosowania funkcji do siebie samej. Takiej funkcji nie da się przypisać typu. Albo więc należy
bardzo zmienić pojęcie typu, albo wykluczyć samozastosowania. Standard ML i inne typowane
języki programowania poszły tą drugą drogą, a żeby nie utracić zupełności w sensie Turinga,
wprowadziły jawną rekursję jako dodatkowy mechanizm językowy, niezwiązany bezpośrednio
z β-konwersją.
1.3
O językach i gramatykach
Jak już było wspomniane, każdy problem decyzyjny jest równoważny problemowi należenia słowa do pewnego języka. Trudność takiego problemu można więc mierzyć stopniem
komplikacji algorytmu akceptacji słów, związanego z językiem. Z kolei akceptacja słów ma
związek14 z ich generowaniem. Klasycznymi generatorami słów są gramatyki, a klasyfikację
13
Tak więc użyte wyżej mnożenie · nie musi być funkcją predefiniowaną. Odpowiada mu pewne λ-wyrażenie,
a wykonuje się je przez zastosowanie β-konwersji do tego λ-wyrażenia.
14
Niezbyt prosty związek.
16
gramatyk przypisuje się Noamowi Chomsky’emu.
Gramatyka jest skończonym zbiorem zasad rozwijania i zmieniania napisów. Występują w
niej litery alfabetu oraz symbole pomocnicze. Generowanie słowa zaczyna się od początkowego
symbolu pomocniczego, w jego trakcie stosuje się zasady gramatyki i otrzymuje jakieś słowo
napisane samymi literami, już bez symboli pomocniczych.
Języki formalne i gramatyki zostały omówione przystępnie w już wspomnianym podręczniku Hopcroft, Motwani i Ullman [10].
O zastosowaniach gramatyk w kompilacji programów komputerowych napisano wiele; dość
pełnym i niezbyt skomplikowanym wykładem jest np. Gries [9].
1.3.1
Określenie gramatyk
Definicja 1.22
Gramatyka formalna 15 to czwórka G = hΣ, N, S, P i, w której
• Σ jest skończonym niepustym zbiorem liter , zwanych też symbolami terminalnymi lub
krótko terminalami; sam zbiór Σ nazywamy alfabetem terminalnym. Słowa, które gramatyka wygeneruje, będą należeć do zbioru Σ∗ .
• N jest skończonym niepustym zbiorem symboli pomocniczych, zwanych też nieterminalami; sam zbiór N nazywamy alfabetem nieterminalnym. Nieterminale używane są
w trakcie generowania słowa, ale są z niego eliminowane, zanim generowanie dobiegnie
końca.
• S ∈ N jest specjalnym nieterminalem początkowym lub aksjomatem. Od niego rozpoczyna się każde generowanie słowa.
• P jest skończonym zbiorem produkcji gramatyki; każda produkcja jest napisem postaci
w1 → w2 , gdzie w1 , w2 ∈ (Σ ∪ N )∗ , przy czym w słowie w1 musi występować przynajmniej jeden nieterminal. Każda produkcja zezwala na zastępowanie podsłowa w1
przez w2 w trakcie generowania słowa.
Poniżej opisane jest, w jaki sposób taka gramatyka generuje słowa.
Definicja 1.23
Niech G = hΣ, N, S, P i będzie gramatyką formalną. Relacja bezpośredniego wywodu w
gramatyce G, ⇒G ⊆ (Σ ∪ N )∗ × (Σ ∪ N )∗ , określona jest następująco:
def
v1 ⇒G v2 ⇐⇒
dla pewnych słów u, w1 , w2 , u′ ∈ (Σ ∪ N )∗
v1 = uw1 u′ & v2 = uw2 u′ &
w1 → w2 ∈ P
Jeśli v1 ⇒G v2 , to mówimy, że v2 da się wywieść bezpośrednio lub wywieść w jednym kroku
z v1 .
Czyli v1 ⇒G v2 oznacza, że v2 powstaje z v1 przez zastąpienie występującej w v1 lewej
strony jakiejś produkcji z P przez prawą stronę tej samej produkcji. W sytuacjach, kiedy nie
ma niejasności co do stosowanej gramatyki, indeks G się opuszcza: v1 ⇒ v2 .
15
Zwana też gramatyką struktur frazowych.
17
Definicja 1.24
Niech G = hΣ, N, S, P i będzie gramatyką formalną. Relacja wywodu w gramatyce G (ale
już niebezpośredniego), ⇒∗G ⊆ (Σ ∪ N )∗ × (Σ ∪ N )∗ , określona jest następująco:
def
v1 ⇒∗G v2 ⇐⇒
dla pewnych słów u1 , u2 , . . . , un ∈ (Σ ∪ N )∗
v1 = u1 & u1 ⇒ u2 & u2 ⇒ u3 & . . . & un−1 ⇒ un & un = v2
Jeśli v1 ⇒∗G v2 , to mówimy, że v2 da się wywieść z v1 . Ciąg u1 , u2 , . . . , un słów pośrednich
nazywamy wywodem.
Innymi słowami: relacja wywodu jest zwrotnym i przechodnim domknięciem relacji wywodu bezpośredniego. W sytuacjach, kiedy nie ma niejasności co do stosowanej gramatyki,
indeks G się opuszcza: v1 ⇒∗ v2 .
Definicja 1.25
Przez język generowany przez gramatykę G = hΣ, N, S, P i rozumiemy zbiór
n
def
L(G) =
w ∈ Σ∗ S ⇒∗G w
o
t.zn. zbiór takich słów, dających się wywieść (w dowolnej liczbie kroków) z aksjomatu gramatyki, które składają się już tylko z liter z Σ.
1.3.2
Przykłady
Przykład 1.26
def
Niech G składa się z następujących elementów: zbiór terminali Σ =
ha, b, ci, zbiór nieterdef
minali N = hS, A, B, Di i zbiór produkcji

S → ABc





S
→ ABSc





BA → BD



BD → AD
def
P =
AD → AB



Aa → aa




Ab → ab





Bb → bb



Bc → bc

(1)




(3)




(4)


(2)

(5)



(7)




(8)




(6)

(9)
Oto przykład wywodu w tej gramatyce. Przy każdym kroku wyprowadzenia, nad strzałką napisany jest numer zastosowanej produkcji, zastępowany fragment słowa (lewa strona
produkcji) jest oznaczony tłustym drukiem, a wynik zastąpienia jest podkreślony:
(2)
(1)
(3)
(4)
(5)
S ⇒ ABSc ⇒ ABABcc ⇒ ABDBcc ⇒ AADBcc ⇒ AABBcc
(9)
(8)
(7)
(6)
⇒ AABbcc ⇒ AAbbcc ⇒ Aabbcc ⇒ aabbcc
Istnienie powyższego
wykazuje,
że aabbcc ∈ L(G). Nietrudno się przekonać, że w
o
n dowodu
k
k
k
ogólności L(G) = a b c k ­ 1 .
18
Przykład 1.27
Rozpatrzmy fragment gramatyki zdań języka polskiego16 :
Σ=
def
def
N=
(
Anka, Ankę, Basia, Basię, Czesiek, Cześka, piękna,
gruby, stary, bije, kocha, w domu, nocą
(
)
)
hzdaniei , hgr.podmiotui , hpodmioti , hprzydawkai ,
hgr.orzeczeniai , horzeczeniei , hdopełnieniei , hokoliczniki
def
S=
hzdaniei




hzdaniei → hgr.podmiotui hgr.orzeczeniai





hgr.podmiotui
→
hpodmioti










hgr.podmiotui
→
hprzydawkai
hgr.podmiotui








hgr.orzeczeniai → horzeczeniei










hgr.orzeczeniai
→
horzeczeniei
hdopełnieniei










hgr.orzeczeniai
→
hgr.orzeczeniai
hokoliczniki








hpodmioti
→
Anka










hpodmioti
→
Basia






hpodmioti → Czesiek
def
P =

hprzydawkai → gruby



hprzydawkai → stary





horzeczeniei → bije




horzeczeniei
→ kocha





hdopełnieniei → Ankę




hdopełnieniei → Basię






hdopełnieniei → Cześka



hokoliczniki → w domu

hokoliczniki → nocą



































Przykładowy wywód słowa (czyli zdania) w tej gramatyce przedstawia rys.1.3 w postaci
t.zw. drzewa wywodu.
Oryginalna motywacja gramatyk formalnych pochodzi właśnie z badań nad strukturą zdań
w językach naturalnych.
Przykład 1.28
Poniższa gramatyka definiuje język wyrażeń arytmetycznych, takich jak używane w programowaniu, oczywiście język bardzo uproszczony.
n
def
Σ=
+, -, *, /, (, ), a, b, . . . , z, 0, 1, . . . , 9
n
o
o
def
N=
hwyrażeniei , hskładniki , hczynniki , hliczbai , hzmiennai , hcyfrai
def
S=
hwyrażeniei
16
Słowa języka polskiego są teraz osobnymi literami alfabetu terminalnego.
19
hzdaniei
hgr.orzeczeniai
hgr.podmiotui
hgr.orzeczeniai
hgr.podmiotui
hgr.podmiotui
hprzydawkai hprzydawkai
stary
gruby
hpodmioti
Czesiek
hgr.orzeczeniai
hgr.orzeczeniai
horzeczeniei hdopełnieniei hokoliczniki
bije
Ankę
w domu
hokoliczniki
nocą
Rys. 1.3: Wywód słowa w gramatyce z przykł. 1.27.




hwyrażeniei → hskładniki








hwyrażeniei
→
hwyrażeniei
+
hskładniki










hwyrażeniei
→
hwyrażeniei
hskładniki








hskładniki
→
hczynniki










hskładniki
→
hskładniki
*
hczynniki








hskładniki → hskładniki / hczynniki










hczynniki
→
hliczbai








hczynniki
→
hzmiennai










hczynniki → ( hwyrażeniei )

def
hliczbai → hcyfrai
P =



hliczbai
→ hliczbai hcyfrai





hzmiennai → a



hzmiennai → b





···




hzmiennai → z





hcyfrai → 0




hcyfrai → 1





···



hcyfrai → 9






































Przykładowy wywód słowa w tej gramatyce przedstawia rys.1.4.
Przedstawiona wyżej gramatyka jest uproszczonym fragmentem definicji składni języka
programowania. Definiowanie języków programowania jest obecnie najważniejszym zastosowaniem praktycznym gramatyk formalnych.
20
hwyrażeniei
hskładniki
hskładniki
hwyrażeniei
hczynniki
................................................................................
...............
..........
.......
..........
......
........
.....
.....
.
.
.
....
....
.
....
.
...
...
.
.
...
...
.
...
..
...
.
...
....
...
...
..
...
...
...
...
..
..
..
..
...
..
..
..
..
.
..
...
.
hskładniki
hwyrażeniei
hczynniki
hwyrażeniei
hskładniki
hskładniki
hczynniki
hliczbai
hczynniki
hliczbai
hczynniki
hcyfrai hcyfrai
hzmiennai
hcyfrai
hzmiennai
hliczbai
2
0
+
(
x
-
1
)
*
y
Rys. 1.4: Wywód słowa 20+(x-1)*y w gramatyce z przykł. 1.28.
1.3.3
Hierarchia Chomsky’ego gramatyk formalnych
Chomsky poklasyfikował języki formalne ze względu na to, jakie gramatyki je generują. Coraz mniejsze klasy języków otrzymujemy, narzucając coraz ciaśniejsze ograniczenia na
dopuszczalne postaci produkcji w ich gramatykach.
Definicja 1.29
Załóżmy, że dana jest gramatyka G = hΣ, N, S, P i.
Mówimy, że gramatyka G jest kontekstowa, jeśli każda jej produkcja ma postać uAt → uwt,
gdzie A ∈ N (czyli jest nieterminalem), a u, w, t ∈ (Σ ∪ N )∗ i dodatkowo w 6= ε. To znaczy
każda produkcja pozwala na wymianę pojedynczego nieterminalu A na niepuste słowo w,
w którym mogą występować terminale i nieterminale, w kontekście słów u i t. Język L jest
kontekstowy, jeśli L = L(G) dla jakiejś gramatyki kontekstowej G.
Mówimy, że gramatyka kontekstowa G jest bezkontekstowa, jeśli każda jej produkcja ma
postać A → w, gdzie A ∈ N (czyli jest nieterminalem), a w ∈ (Σ ∪ N )∗ r {ε}. To oznacza,
że wymiany nieterminalu A na słowo w nie może ograniczyć żaden kontekst. Język L jest
bezkontekstowy, jeśli L = L(G) dla jakiejś gramatyki kontekstowej G.
Mówimy, że gramatyka bezkontekstowa G jest prawoliniowa, jeśli dla każdej jej produkcji
A → w ∈ P , albo w ∈ Σ∗ , albo w = w′ B dla pewnego w′ ∈ Σ∗ i B ∈ N . To znaczy nieterminal
może pojawić się po prawej stronie produkcji najwyżej jeden i tylko na jej prawym skraju.
Język L jest regularny, jeśli L = L(G) dla jakiejś gramatyki prawoliniowej G.
21
Z samego sposobu zdefiniowania wynikają następujące zawierania klas języków:
regularne ⊆ bezkontekstowe ⊆ kontekstowe ⊆ rekurencyjnie przeliczalne
...................................................................................................................
......................
................
................
............
............
...........
.
.
.
.
.
.
.
.
.
.........
...
.......
........
.....
.......
.
.
.
.
......
....
.
.
.
......
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.......................................
...............................
.
.....
.
.
.
.
.....
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
................
....
...........
.
.
...
.
.
.
.
.
.
.
.
.
.
....
.
.
.
.
.
.
.
.
.
.
...........
.........
....
.
...
.
.
.
.
.
.
.
.
.
.
.
.
.
........
...
..
......
.
.
.
.
.
.
.
...
.
.
.
......
....
..
.
.
...
.
.
.
.
.
.
.
.....
....
...
..
.
.
.
.
.
.
.
.
.
.
.
....
............................. .................................................
...
.
.
.
.
.
.
.
.
.
...
.
.
.
.
.
.
.
.
.
.
.
....
.
.
.
..............
....
...
.........
.
.
.
.
.
.
.
.
...
.
.
.
.
.
.
.
.
...
.
.
........
...
..
......
.
.
..
.
.
.
.
..
.
...
.
.
.
......
...
....
.
..
..
.
.
.
.
..
.
.
.....
...
..
.
..
.
..
.
.
.
.
.
.
.
.
..
...
..
..
......................................................
.
.
.
.
.
.
.
.
.
.
.
..
.
.
.
...
.
..
.
.
.
.
.
.
.
.
.
.
.
.
.
.
............
...
.
.
.
...
.........
.
..
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.......
..
..
...
.
..
.......
.....
..
..
..
...
..
...
.....
....
...
...
..
..
...
....
..
...
...
...
...
...
..
....
...
...
...
...
....
...
...
....
............................................
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
....
...
......
....
...
....
.
...
..
...
....
...
.
.....
....
....
.....
...
....
....
.....
...... .........
....
....
....
..
....
..... ...........
..
..
......
....
.....
.....
.
.....
...
...... ............ ............ ..............
.........
..... ................. ............. .............. ...........
.
.
.
.
.........
.
.......
.
.........
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.........
..............................
...
...
..
........
.........
.....
........... ....................... .................................................................................................................................... ....................... ..................
.
..
............
...
.
.
...............................................................................................................................................................................................
....
......................
..
...
..........................................................................................................................................................
wszystkie
rek. przeliczalne
kontekstowe
bezkontekstowe
regularne
Rys. 1.5: Hierarchia Chomsky’ego języków formalnych.
Wszystkie te zawierania są właściwe:
Przykładn1.30
o
Język ak b k ­ 1 jest regularny.
n
Język ak bk k ­ 1
o
jest bezkontekstowy, ale nie jest regularny.
Również języki z przykładów 1.27 i 1.28 są bezkontekstowe.
Trudniej jest stwierdzić, czy te języki są regularne. Nie wystarczy oczywiste stwierdzenie,
że gramatyki, którymi zostały one zadane, nie są prawoliniowe, bo to nie wyklucza możliwości,
że te same języki dają się wygenerować innymi, prawoliniowymi gramatykami. Ostatecznie
okazuje się, że język z przykładu 1.27 jest w istocie regularny (posiada równoważną gramatykę
prawoliniową)17 ; a język z przykładu 1.28 nie jest regularny.
n
o
Język ak bk ck k ­ 1 jest kontekstowy (patrz przykład 1.26), ale nie jest bezkontekstowy.
Język




m jest zakodowaną postacią jakiejś maszyny Turinga M , 

def
Halt =
m ∗ w w jest jakimś słowem i




maszyna M zatrzymuje się na w
jest rekursywnie przeliczalny, ale nie jest konstektowy.
Język




m jest zakodowaną postacią jakiejś maszyny Turinga M , 

def
NHalt =
m ∗ w w jest jakimś słowem i




maszyna M nie zatrzymuje się na w
nie jest rekursywnie przeliczalny.
17
Istotne dla regularności jest tu, że to nie jest pełna gramatyka języka polskiego.
22
1.3.4
Generatory a akceptory
W rozdz. 1.2.1 i 1.2.2 omawialiśmy teoretyczne urządzenia akceptujące słowa; w tym rozdziale zajmowaliśmy się generowaniem słów. Oba podejścia umożliwiają definiowanie języków.
Okazuje się, że istnieją pewne związki między tymi dwoma podejściami.
Twierdzenie 1.31
Klasa języków regularnych jest dokładnie równa klasie języków rozpoznawanych przez
automaty skończone.
Dla języków z wyższych klas hierarchii Chomsky’ego sytuacja nie jest już tak prosta
i elegancka. Np. dla zaakceptowania języka bezkontekstowego potrzeba większej mocy niż
automatu skończonego, ale nie potrzeba pełnej mocy maszyn Turinga.
Twierdzenie 1.32
Klasa języków bezkontekstowych jest dokładnie równa klasie języków akceptowanych
przez niedeterministyczne maszyny stosowe.
Niedeterministyczna maszyna stosowa przypomina maszynę Turinga, lecz zamiast nieskończonej taśmy, po której można się poruszać dowolnie, dysponuje nieskończonym stosem, na
którym można operować tylko „na szczycie”. Zasadniczą różnicą jest niedeterminizm, który
oznacza (nieformalnie), że maszyna może przy każdym kroku wybrać jedną z wielu możliwości.
Podobnie jest z językami kontekstowymi:
Twierdzenie 1.33
Klasa języków kontekstowych jest dokładnie równa klasie języków akceptowanych przez
maszyny liniowo ograniczone.
To z kolei są maszyny Turinga z ograniczeniem na sposób korzystania z nieskończonej taśmy.
Najszersza klasa, języków rekurencyjnie przeliczalnych, jest równa klasie języków akceptowanych przez maszyny Turinga bez żadnych ograniczeń (por. definicja 1.10).
1.3.5
Znaczenie praktyczne klas języków formalnych
Na końcu rozdz. 1.2.1 zostało wspomniane, że automaty skończone (a więc również języki
regularne) mają znaczenie
• w budowie kompilatorów języków programowania, przy rozpoznawaniu słów kluczowych,
identyfikatorów, liczb itp.; czyli w analizie leksykalnej ; oraz
• przy rozpoznawaniu wzorców .
Największe zastosowanie mają bez wątpienia języki bezkontekstowe. W dużej części wynika ono z faktu, że potrzebne do ich akceptacji maszyny stosowe są stosunkowo łatwe do
realizacji komputerowej i szybko działają. Analiza syntaktyczna, stanowiąca jądro każdego
kompilatora, jest dokonywana przez taką maszynę stosową. Również w badaniach nad językami naturalnymi (por. przykład 1.27) eksploatowano aparat pojęciowy i maszynerię gramatyk
bezkontekstowych. Jednak ani języki programowania, ani (najprawdopodobniej) języki naturalne nie są bezkontekstowe.
23
W przypadku języków programowania na przeszkodzie bezkontekstowości stoją kwestie
semantyczne, takie jak obowiązek deklarowania zmiennych. Dlatego analizator syntaktyczny
kompilatora dokonuje tylko wstępnej analizy przedstawionego mu programu, akceptując słowo z języka bezkontekstowego stanowiącego nadzbiór języka programowania. Dopiero potem
sprawdza dodatkowe wymagania semantyczne.
W przypadku języków naturalnych problem jest znacznie trudniejszy, bo w przeciwieństwie do języków programowania nie mają one precyzyjnej definicji. Powszechne już dzisiaj
przekonanie, że nie istnieje gramatyka bezkontekstowa, opisująca w pełni np. język polski,
bierze się raczej z wielu lat nieudanych prób skonstruowania takiej gramatyki niż ze ściśle
udowodnionego faktu.
Zarówno języki programowania jak języki naturalne są kontekstowe. Ten fakt nie daje nam
jednak znaczących zastosowań, bo nie znane są żadne ogólne metody generowania akceptorów
(maszyn liniowo ograniczonych) do gramatyk kontekstowych.
Klasa języków rekurencyjnie przeliczalnych ma duże znaczenie teoretyczne; jednak jego
omówienie nie mieści się w zakresie tej pracy.
1.4
Złożoność obliczeniowa
Rozważania o językach i maszynach prowadzą do rozpoznania, co w ogóle daje się policzyć
i jakimi mechanizmami. Ale kiedy już wiemy, że coś daje się policzyć, powstaje kwestia kosztu
dokonania takiego obliczenia.
Najważniejszym kosztem interesującym informatyka jest czas działania programu. Czasem
interesuje go również wielkość niezbędnej do obliczeń pamięci. Termin złożoność użyty bez
żadnych przymiotników będzie zwykle oznaczał złożoność czasową.
Oczywiście miarą złożoności programu lub problemu programistycznego nie może być prosta liczba sekund upływających od wczytania danych do wydrukowania wyniku. Taka miara
więcej mówiłaby o samym komputerze i o danych niż o sprawności algorytmu czy o trudności problemu. Bezpośrednie liczenie czasu może mieć znaczenie poglądowe przemawiające do
laika, gdy np. podajemy mu liczbę tysiącleci potrzebnych dzisiejszym najszybszym komputerom do złamania szyfru RSA, żeby przekonać go, że jego pieniądze w banku internetowym
są dobrze zabezpieczone. Jednak do celów technicznych potrzebne są miary całkiem innej
natury.
Pierwsze badania złożoności algorytmów pochodzą od Borisa Trachtenbrota i Michaela
Rabina (lata 1950.).
Klasycznym podręcznikiem jest Aho, Hopcroft i Ullman [1]. Za kopalnię wiedzy o algorytmach i ich złożoności służy Cormen, Leiserson i Rivest [6].
1.4.1
Czas działania programu jako funkcja wielkości danych
Dla konkretnego programu i konkretnych danych, na których ten program się zatrzymuje,
możemy zliczyć liczbę kroków obliczenia wykonywanych w trakcie działania programu. Gdy
mówimy o maszynach Turinga, pojęcie takiego kroku jest całkiem jasne — chodzi o wykonanie
pojedynczej komendy. W przypadku realnych komputerów ta sprawa jest znacznie mniej jasna.
24
Ile kroków kosztuje wykonanie prostego obliczenia arytmetycznego z przypisaniem wartości zmiennej? Np. realizacja jednej komendy
c←a+b
(1.1)
(dodać wartość zmiennej a do wartości zmiennej b i wynik zapisać w zmiennej c) wymaga
standardowo trzech instrukcji języka wewnętrznego18 . Czy więc za jej koszt powinniśmy przyjąć 1 czy 3? Ale język wewnętrzny komputera to nie jest jeszcze najniższy poziom granulacji.
Realizacja każdej jego instrukcji wymaga wykonania wielu kroków mikroprogramu zaszytego
w procesorze. Dodawanie dwóch liczb nie jest przecież czynnością „atomową”, a liczba kroków
niezbędnych dla jego wykonania zależy od ich wielkości.
Na najniższym poziomie granulacji stoją kroki maszyny Turinga; twierdzenia o złożoności
odnoszą się do nich. Jednak w wielu praktycznych zastosowaniach, o których wiadomo, że
używane liczby będą „rozsądnej wielkości”, takie przypisania jak (1.1) można przyjąć za
atomowe i tylko zliczać, ile ich wykonano.
1.4.1.1
Definicja złożoności czasowej
Załóżmy więc, że program P wczytuje dane x ∈ D i drukuje wynik po wykonaniu T̃P (x)
kroków liczonych w jakiś przyjęty przez nas sposób. Liczba tych kroków zależy zarówno od
programu jak i od konkretnych danych, stąd obie te rzeczy występują w oznaczeniu. Pierwotną
miarą złożoności programu P będzie więc funkcja T̃P : D → N.
Ponieważ dane do programu mogą być wieloczęściowe i skomplikowane, lepiej byłoby odnieść czas działania nie do samych danych, lecz do jakiejś wielkości charakteryzującej to skomplikowanie. Załóżmy więc dalej, że mamy jakąś miarę skomplikowania danych µ : D → N, taką
że µ(x) oznacza liczbę klatek taśmy maszyny Turinga, którą trzeba poświęcić na zapisanie
danych x, albo wielkość liczb zawartych w danych x, albo jeszcze coś innego, podobnego.
Definicja 1.34
Pesymistyczną złożonością czasową programu P względem miary µ nazywamy funkcję
TP : N → N określoną jako
n
def
TP (n) =
max T̃P (x) x ∈ D & µ(x) ¬ n
o
Tak więc do definicji pesymistycznej złożoności bierzemy te dane x, o mierze nieprzekraczającej n, dla których program wykonuje maksymalną liczbę kroków19 .
Definiuje się również średnią złożoność czasową. Żeby to zrobić porządnie, należy uśredniać wartość
prawdopodobieństwa znalezienia konkretnych danych x
o
n T̄ (x), znając gęstość
w zbiorze x ∈ D µ(x) ¬ n . Dla konkretnych przykładów zwykle łatwo jest zgodzić się
na „rozsądny” rozkład prawdopodobieństwa. Średnia złożoność czasowa programu P będzie
oznaczana przez EP (n).
18
Trzech instrukcji wymaga ta akurat komenda; inne pojedyncze komendy mogą potrzebować innej liczby
instrukcji języka wewnętrznego.
19
W ogólnym przypadku takie maksimum może być nieskończone; np. jeśli danymi są liczby całkowite,
liczba kroków programu dla danych x wynosi TP (x) = x, a za miarę przyjmiemy funkcję stałą µ(x) def
= 1. Takie
przypadki świadczą o złym dobraniu miary danych do problemu.
25
1.4.1.2
Przykłady
Przykład 1.35
Niech P będzie programem wyszukującym maksymalny element w tablicy liczb rzeczywistych:
P :
m ← a[0];
for i in [1 . . n − 1] do
if a[i] > m then m ← a[i] fi
od
Załóżmy, że za miarę wielkości danych bierzemy liczbę elementów tablicy, czyli liczbę n;
a interesuje nas złożoność w sensie liczby wykonanych porównań liczb rzeczywistych. Dane
stanowi oczywiście tablica a. Tak więc
T̃P (a[0 . . n − 1]) = n − 1 więc
TP (n) = n − 1
Ponieważ czas działania tego programu zależy tylko od n, wobec tego czas średni jest taki
sam jak czas pesymistyczny.
Przykład 1.36
Różne programy sortowania (czyli ustawiania wg jakiegoś porządku) mają różne złożoności:
T (n)
bąbelkowe (ang. bubblesort)
a · n2
przez wybór (ang. selectionsort)
b · n2 + . . .
przez wstawianie (ang. insertionsort)
przez scalanie(ang. mergesort)
stertowe (ang. heapsort)
szybkie (ang. quicksort)
+ ...
c · n2 + . . .
E(n)
a · n2 + . . .
b · n2 + . . .
c · n2 + . . .
d · n · log2 n + . . . d · n · log2 n + . . .
e · n · log2 n + . . .
f
kubełkowe (ang. bucketsort)
· n2
+ ...
g · n + ...
e · n · log2 n + . . .
f · n · log2 n + . . .
g · n + ...
W tej tabelce w każdym przypadku napisany został tylko najbardziej istotny składnik funkcji
złożoności czasowej. Stałe mnożniki a, b, c, d, e, f, g są różne dla różnych algorytmów. Jak
widać z tabelki, algorytm szybki ma zasadniczo różną złożoność pesymistyczną od złożoności
średniej. Jego nazwa pochodzi stąd, że jego stały mnożnik f jest niższy niż w sortowaniu
przez scalanie i stertowym, dlatego jego czas działania jest zwykle lepszy, chociaż przy bardzo
„złośliwych” danych może być znacząco gorszy.
1.4.2
Asymptotyczna klasyfikacja algorytmów
Funkcja T̃P : D → N jest obiektem zbyt skomplikowanym, żeby móc służyć jako wygodna miara szybkości działania programu. Dlatego została zastąpiona przez prostszą funkcję
TP : N → N. Żeby teraz móc porównywać algorytmy, trzeba umieć porównywać takie funkcje.
26
1.4.2.1
Asymptotyczna klasyfikacja funkcji
Standardowym sposobem porównywania jest badanie asymptotycznego wzrostu funkcji, to
znaczy jej zachowania granicznego przy wzroście wielkości danych do nieskończoności.
Definicja 1.37
Załóżmy, że dana jest funkcja f : N → N. Określamy następujące klasy funkcji:
• funkcje asymptotycznie niewiększe niż f :
O(f ) =
def
(
)
istnieje stała c ∈ N r {0}, taka że
g : N → N
dla dużych n ∈ N zachodzi g(n) ¬ c · f (n)
• funkcje asymptotycznie niemniejsze niż f :
Ω(f ) =
def
(
)
istnieje stała c ∈ N r {0}, taka że
g : N → N
dla dużych n ∈ N zachodzi f (n) ¬ c · g(n)
• funkcje asymptotycznie równe f :
def
Θ(f ) =
O(f ) ∩ Ω(n)
Tak więc g ∈ O(f ) 20 oznacza, nieformalnie mówiąc, że przy n rosnącym do nieskończoności, g rośnie do nieskończoności co najwyżej tak szybko jak f ; g ∈ Ω(f ) oznacza, że g rośnie
do nieskończoności co najmniej tak szybko jak f ; g ∈ Θ(f ) oznacza, że g rośnie do nieskończoności tak samo szybko jak f .
Zauważmy, że w definicji 1.37 nie ma rozróżnienia między różnymi stałymi c; ważne jest
tylko, żeby c nie było zerem ani nieskończonością. Jesłi program komputerowy przeniesiemy
na inny komputer działający k razy szybciej, to czas wykonania tego programu zmniejszy się
k-krotnie. Nie wpłynie to jednak na asymptotyczną klasyfikację złożoności tego programu,
właśnie dlatego, że def. 1.37 abstrahuje od konkretnej stałej. W związku z tym klasyfikacja asymptotyczna dotyczy samych programów (algorytmów) niezależnie od komputerów, na
których te programy działają.
Twierdzenie 1.38
g(n)
. Wtedy:
Załóżmy, że istnieje granica właściwa lub niewłaściwa21 ilorazu
f (n)
g(n)
1. jeśli
lim
= 0 , to g ∈ O(f ) r Θ(f ) oraz f ∈ Ω(g) r Θ(g) ; t.zn. g rośnie
n→+∞ f (n)
asymptotycznie wolniej niż f ;
g(n)
= +∞ , to g ∈ Ω(f ) r Θ(f ) oraz f ∈ O(g) r Θ(g) ; t.zn. g rośnie
2. jeśli lim
n→+∞ f (n)
asymptotycznie szybciej niż f ;
g(n)
< +∞ , to g ∈ Θ(f ) oraz f ∈ Θ(g) ; t.zn. f i g rosną asymp3. jeśli 0 < lim
n→+∞ f (n)
totycznie tak samo szybko.
20
Tradycja nakazuje używać trochę mylącego zapisu: g(n) = O(f (n)). W tym artykule będę jednak unikał
pisania równości tam, gdzie w istocie chodzi o należenie.
21
Granica właściwa to liczba rzeczywista; granica niewłaściwa to +∞.
27
wielomiany
pierwiastki
Łatwo pokazać, że asymptotyczna równość jest równoważnością w zbiorze funkcji N → N;
a porównanie funkcji ze względu na asymptotyczny wzrost jest porządkiem częściowym. Jednak nie jest to porządek całkowity — dwie funkcje mogą nie dać się porównać, a granica z
twierdzenia 1.38 może nie istnieć. Jednak funkcje złożoności, najczęściej spotykane w analizie
algorytmów, należą do którejś z klas, ustawionych w tabelce na rys. 1.6 od wolniej do szybciej
rosnących. Ale oczywiście ta tabelka może być nieograniczenie przedłużana i w górę i w dół,
a także zagęszczana.
klasa
asymptotyczna
funkcje
Θ(1)
stałe
Θ(log n)
logarytmiczne
uwagi
podstawa logarytmu bez znaczenia
···
√
3
Θ( n)
√
Θ( 2 n)
Θ(n)
liniowe
Θ(n log n)
liniowo-logarytmiczne
Θ(n2 )
kwadratowe
Θ(n3 )
sześcienne
istotne dla sortowania
superwykładnicze
wykładnicze
···
Θ(2n )
poza możliwościami komputerów
Θ(3n )
···
Θ(n!)
silnia
2
Θ(2n )
n
Θ(22 )
Rys. 1.6: Klasy złożoności asymptotycznej najczęściej spotykane w analizie algorytmów.
Należy zwrócić uwagę, że wszystkie funkcje logarytmiczne, niezależnie od podstawy logarytmu, należą do tej samej klasy, bo
logp (n)
= logp q
logq (n)
i spełniony jest pkt. 3 twierdzenia 1.38.
28
1.4.2.2
Czas asymptotyczny a prędkość działania
Z tego, że złożoność czasowa algorytmu P1 należy do wyższej klasy
asymptotycznej niż złożoność czasowa algorytmu P2 , nie wynika jeszcze,
że algorytm P1 będzie zawsze działał wolniej niż algorytm P2 . Na szkicu obok zaznaczona jest taka możliwość: na całym początkowym zbiorze
danych, oznaczonym klamrą, kwadratowy algorytm P1 działa szybciej niż
liniowy algorytm P2 . Dla bardzo dużych danych liniowy algorytm w końcu
osiągnie przewagę — ale może tak być, że dla tak dużych, że już dla nas
nieinteresujących.
P.1
..
..
...
.
..
...
....
..
...
....
..
..
....
..
..
....
..
..
....
..
...
.
.
...
........
..
P2
Teza 1.39
Przyjmuje się, że algorytmy o złożoności wykładniczej nie dają się wykonać na komputerze, bo czas oczekiwania na odpowiedź jest już za długi
| {z }
dla każdych nietrywialnych danych.
Żeby łatwiej zdać sobie sprawę, dlaczego tak się uważa22 , spróbujmy rozważyć, jak wzrost
prędkości komputera wpływa na wielkość danych, dla których jeszcze ciągle możemy oczekiwać
wyniku w „rozsądnym” czasie.
Jeśli czas działania algorytmu jest asymptotycznego rzędu Θ(n) (czas liniowy), to stukrotne przyspieszenie komputera spowoduje, że da on sobie radę ze stukrotnie większymi danymi.
Jeśli T (n) ∈ Θ(n2 ) (czas kwadratowy), to to samo stukrotne przyspieszenie komputera umożliwi tylko dziesięciokrotne zwiększenie danych. A jeśli T (n) ∈ Θ(2n ) (czas wykładniczy), to
stukrotne przyspieszenie komputera da nam zwiększenie danych już nie „iluśkrotne”, to znaczy nawet najmniejszy mnożnik będzie za duży, tylko o log2 (100) ≃ 6.64. Stąd wniosek, że
wzrostu wykładniczego nie da się opanować przez zwiększanie mocy samego komputera, lepiej
postarać się o inny algorytm, o mniejszej złożoności asymptotycznej.
1.4.3
Złożoność problemów
Jednak nie zawsze ta dobra rada daje się zastosować — może nie istnieć algorytm równoważny danemu, mający lepszy asymptotyczny czas działania.
Na przykład wyszukiwanie maksimum tablicy (por. przykład 1.35) nie może trwać krócej
niż liniowo, bo do każdego z n elementów tablicy trzeba przynajmniej zajrzeć. Trudniej jest
znaleźć dolne ograniczenie na szybkość sortowania, ale i to można oszacować:
Twierdzenie 1.40
Sortowanie przy pomocy porównań zbioru złożonego z n obiektów nie może trwać krócej
niż Ω(n log n) 23 .
Dowód tw. 1.40 (szkic):
Każda permutacja n obiektów wymaga innej kolejności przestawień. Algorytm sortujący
bada jaka to jest permutacja, wykonując porównania. Pojedyncze porównanie dostarcza jednego bitu informacji o permutacji. Ponieważ wszystkich permutacji jest n!, potrzeba log2 n!
22
To oczywiście nie jest twierdzenie, tylko nieformalna teza metodologiczna.
W tabelce w przykładzie 1.36 stwierdzone jest (nie wprost), że sortowanie kubełkowe działa w czasie Θ(n).
Sortowanie kubełkowe nie działa przez porównania, więc nie przeczy tezie tego twierdzenia. Zarówno liczby,
jak i inne obiekty, można zawsze sortować przez porównania; a tylko bardzo specjalne obiekty da się sortować
kubełkowo.
23
29
bitów informacji, żeby rozróżnić je wszystkie. Wobec tego algorytm musi wykonać co najmniej
log2 n! porównań. Łatwo sprawdzić, że
log2 n! ∈ Θ(n log2 n)
skąd już wynika teza24 .
W tej chwili zajmujemy się już badaniem nie konkretnych programów, a raczej problemów programistycznych. Kiedy mówimy, że sortowanie przez porównania musi kosztować
Ω(n log n), jest to sąd o problemie sortowania w ogólności, a nie o konkretnym algorytmie
sortującym.
Definicja 1.41
Przez złożoność problemu rozumiemy minimalną klasę asymptotyczną algorytmu rozwiązującego ten problem.
Przykład 1.42
Z tw. 1.40 wynika, że złożoność problemu sortowania przez porównania należy do klasy Ω(n log n); to daje oszacowanie dolne na asymptotyczny czas sortowania. A w tabelce w
przykładzie 1.36 są algorytmy sortujące w czasie O(n log n) (scalanie i stertowe); to daje oszacowanie górne. Ponieważ te dwa oszacowania się pokrywają, złożoność problemu sortowania
przez porównania wynosi Θ(n log n).
Zbadano złożoność wielu problemów; ale są i takie, dla których znamy oszacowanie dolne
i górne, i te oszacowania się nie pokrywają. Wtedy nie wiemy, jaka naprawdę jest złożoność
problemu.
1.4.4
Hipoteza P =
6 N P i jej konsekwencje
Istnieje cała duża klasa ważnych problemów, dla których
1. znamy algorytm działający w czasie wykładniczym lub dłuższym, oraz
2. dla każdego wyniku potrafimy w czasie wielomianowym sprawdzić, czy ten wynik jest
dobrym rozwiązaniem problemu,
ale nie umiemy znaleźć takiego rozwiązania w czasie wielomianowym.
Przykład 1.43
Problem Sat 25 (spełnianie w rachunku zdań) brzmi następująco:
Dana jest formuła ϕ rachunku zdań (taka jak np. p ⇒ (q ⇔ ¬r)).
Znaleźć takie wartościowanie v zmiennych26 , żeby v ϕ;
lub ustalić, że takie wartościowanie nie istnieje (formuła niespełnialna).
24
Zwykle udowadnia się to twierdzenie badając wysokość drzewa decyzyjnego algorytmu.
Od ang. Satisfaction.
26
Wartościowanie zmiennych to funkcja v : Var → {false, true} przypisująca każdej zmiennej wartość prawdy
lub fałszu. Przy danym wartościowaniu cała formuła uzyskuje wartość prawdy lub fałszu. Oznaczenie v ϕ
czytamy: przy wartościowaniu v, formuła ϕ jest spełniona, czyli ma wartość prawdy.
25
30
Miarą wielkości danych będzie w tym przypadku długość formuły ϕ; albo, co niewiele zmienia,
liczba zmiennych w tej formule.
Problem Sat można rozwiązać „brutalną siłą”, przez sprawdzenie po kolei wszystkich
wartościowań. Dla każdego wartościowania v ustalenie, czy v ϕ, kosztuje Θ(n) — jest więc
spełniony warunek 2 powyżej. Ponieważ wszystkich wartościowań jest Θ(2n ), więc pesymistyczna złożoność czasowa tego algorytmu wynosi Θ(n · 2n ) — więc spełniony jest warunek 1.
Nie jest znany żaden algorytm rozwiązujący Sat w czasie wielomianowym. Nie ma też
dowodu, że taki algorytm nie istnieje.
Przykład 1.44
Załóżmy, że dany jest graf nieskierowany G = hV, Ei (V —
zbiór wierzchołków, E — zbiór krawędzi). Przez klikę rozmiaru k w grafie G rozumiemy taki zbiór K ⊆ V wierzchołków tego
grafu, że
• |K| = k, czyli K zawiera k wierzchołków, oraz
• każde dwa wierzchołki zbioru K są połączone krawędzią.
a
c
e
b
d
Np. graf na rysunku obok zawiera klikę {c, d, e} rozmiaru 3.
Problem Clique brzmi następująco:
Dany jest graf nieskierowany G oraz liczba naturalna k.
Znaleźć w grafie G jakąś klikę rozmiaru k;
lub ustalić, że żadnej takiej kliki nie ma.
Za miarę wielkości problemu przyjmijmy sumaryczną liczbę wierzchołków i krawędzi grafu
n = |V | + |E|.
Zastosowanie „brutalnej siły” polega w tym przypadku na przejrzeniu wszystkich kelementowych podzbiorów V . Sprawdzenie, czy dany zbiór k-elementowy
jest kliką rozmia
ru k, wymaga Θ(n) kroków. Wszystkich takich zbiorów jest |Vk | ; w najgorszym przypadku,
n
dla k = |V2 | , jest to wielkość rzędu Ω(2 2 ) ∩ O(nn ) — czyli czas działania algorytmu jest wykładniczy lub wyższy.
Również w tym przypadku nie znamy algorytmu wielomianowego, ani nie potrafimy udowodnić, że taki algorytm nie istnieje.
Definicja 1.45
Klasę problemów, dla których istnieje algorytm rozwiązujący, działający w czasie wielomianowym, oznaczamy przez P. Klasę problemów, dla których istnieje algorytm sprawdzający
rozwiązanie, działający w czasie wielomianowym, oznaczamy przez N P 27 .
Wniosek 1.46 P ⊆ N P .
To zawieranie jest oczywiste. Ale jest otwartym problemem, czy zachodzi również zawieranie w drugą stronę:
27
Klasę N P zwykle definiuje się przez t.zw. niedeterministyczną maszynę Turinga.
31
Hipoteza 1.47
P=
6 N P, czyli istnieje problem sprawdzalny wielomianowo, ale nierozwiązywalny wielomianowo: p ∈ N P r P.
Problemy przedstawione w przykładach 1.43 i 1.44 należą do klasy N P. Gdybyśmy potrafili udowodnić, że do któregoś z nich nie istnieje algorytm wielomianowy, to mielibyśmy
potwierdzenie hipotezy 1.47. Ale okazuje się, że działa również implikacja odwrotna: gdybyśmy mieli algorym wielomianowy rozwiązujący jeden z tych problemów, to hipoteza 1.47
byłaby obalona — a to dlatego, że te dwa problemy są N P-zupełne:
Definicja 1.48
Problem p nazywa się N P-zupełny, jeśli
• p ∈ N P, oraz
• dla każdego problemu p′ ∈ N P istnieje metoda przerobienia w czasie wielomianowym
dowolnego rozwiązania problemu p na jakieś rozwiązanie problemu p′ .
Lemat 1.49
Problemy Sat (patrz przykład 1.43) oraz Clique (patrz przykład 1.44) są N P-zupełne.
Lista znanych problemów N P-zupełnych jest bardzo długa: [11] przytacza ich ponad 90.
Znalezienie dla któregokolwiek z nich algorytmu o wielomianowym asymptotycznym czasie
działania, lub udowodnienie, że takiego algorytmu nie ma, obali lub potwierdzi hipotezę 1.47.
Ale na razie już od r. 1971 stanowi ona otwarty problem, mimo że za jego rozwiązanie Clay
Mathematical Institute ustanowił nagrodę w wysokości miliona dolarów.
Hipoteza 1.47 intryguje ludzi z powodów matematycznych: ponieważ pytania, tak prosto
sformułowane a tak trudne do odpowiedzi, są z samej swojej natury intrygujące. Ale jest
jeszcze aspekt bardzo praktyczny tego zainteresowania. Otóż istnieje dziedzina informatyki, w
której ludziom zależy na tym, żeby istniały problemy o wysokiej złożoności: szyfrowanie. Jeśli
szyfr jest oparty na jakimś problemie, o którym da się udowodnić, że jest trudny obliczeniowo,
to ten szyfr jest trudny do złamania.
Stosowane powszechnie szyfrowane połączenia komputerowe (np. z przegladarki klienta
do jego konta w banku), a także działanie podpisu elektronicznego, oparte są na problemie
znajdowania podzielników dużych liczb naturalnych28 . Wiadomo o nim, że jest w N P; i
gdyby okazało się, że P = N P, to oparte na nim szyfrowanie przestałoby być bezpieczne.
Ale dodatkową trudnością jest, że znajdowanie podzielników prawdopodobnie nie jest N Pzupełne; możliwe więc jest, że P =
6 N P (jak się powszechnie podejrzewa), a jednak mimo tego
istnieje algorytm znajdowania podzielników, działający w czasie wielomianowym. Przeważa
pogląd, że takiego algorytmu nie ma, zatem szyfrowanie i podpis elektroniczny są bezpieczne;
jednak fakt ten nie został udowodniony.
1.5
Logiczne podstawy informatyki
Informatyka jest w dużym stopniu dyscypliną inżynierską. Dlatego może dziwić, że badania jej podstaw teoretycznych wymagają zainteresowania się logiką matematyczną, dziedziną
leżącą u samych fundamentów matematyki, blisko jej granicy z filozofią. Tak głęboko do
28
Algorytm RSA — kryptografia asymetryczna (public key).
32
źródeł nie sięgają inne nauki stosujące matematykę. Najważniejsza z nich, fizyka, nie tylko
stosuje ale nawet rozwija takie dziedziny jak algebrę, równania różniczkowe, geometrię różniczkową, czy analizę funkcjonalną, ale nie ma powodu rewidować podstaw logicznych ani
teoriomnogościowych.
Okazuje się, że informatyka ma taką potrzebę. W dużej mierze wynika to z charakteru
samego procesu przetwarzania informacji. Jedyne, na czym działa procesor komputera, to
ciągi bitów. Jeśli chcemy w nie „zakląć” działania, obrazy, wnioskowania itp., to musimy
stawić czoła typowym logicznym problemom stosunków między różnymi poziomami teorii
(teoria — metateoria — metametateoria. . . ) a także związkom czystej składni ze znaczeniami,
które można przez nią przekazać. Innym powodem sięgania do podstaw logicznych są badania
z dziedziny sztucznej inteligencji, które z konieczności zahaczają o zasady rozumowania i
wyciągania wniosków.
Informatyka nie tylko czerpie ze skarbca idei napełnionego przez wysiłek pokoleń matematyków, również do niego dodaje (podobnie, jak czyni to fizyka). Pewne dziedziny logiki
powstały na potrzeby informatyki; pewne zostały utworzone przez informatyków a dopiero
później zajęli się nimi matematycy.
Korzenie logiki matematycznej tkwią oczywiście w starożytności; jednak bezpieczniej przyjąć za jej początki wiek XIX i XX. Oto niektórzy twórcy klasycznej logiki formalnej: George
Boole, Augustus De Morgan, Georg Cantor, David Hilbert, Ernst Zermelo, Bertrand Russell,
Kurt Gödel, Alfred Tarski. Co do logik nieklasycznych, to za inicjatorów uważa się
• logik wielowartościowych — Łukasiewicza i Posta; jednak były to trochę inne logiki niż
dzisiaj używane do badania programów mogących się nie zatrzymywać;
• logik modalnych — Clarence’a Lewisa;
• intuicjonizmu — Luitzena Brouwera.
Przystępny i zwięzły wykład podstaw klasycznej logiki zawiera Lyndon [12]. Logikę w
ujęciu bardziej programistycznym (w tym logikę temporalną) prezentuje Ben-Ari [5]. Polecić
należy także kompendium Marciszewski (red.) [15]; w którym znaleźć można również informacje o logikach nieklasycznych.
1.5.1
Logika klasyczna
.................................
...............
.......
.......
interpretacja
.....
.
....
.....
Klasyczny system logiczny łączy ze sobą kilka różnych światów. Jest
...
...
..
..
...
JĘZYK
..
...
..
.
....
rzeczywistość matematyczna, zwana modelem, którą system ma opisywać.
....
.
.....
.
.
.........
...
..................................................
I jest język formalny, który ma służyć do tego opisu. Między nimi istnieje funkcja interpretująca wyrażenia języka. Wnioskowanie przeprowadza
się po stronie języka, ale dopuszczalne sposoby wnioskowania muszą być
takie, żeby interpretacja wyprowadzonych wniosków była spełniona w świe?
cie. Temu służą aksjomaty i reguły wnioskowania występujące po stronie
....................................................
........
.....
.
.
.
.
....
..
języka.
...
...
..
..
...
.
... MODEL ....
Adekwatność języka do opisu modelu uzasadnia się udowadniając od....
...
.
.
.....
.
...
.........
.
.
.
.
.
.
..............................................
powiednie metatwierdzenia o funkcji interpretującej.
Jednak tylko w bardzo prostych przypadkach mamy dostęp bezpośrednio do modelu; zwykle dysponujemy jedynie opisem modelu w jakimś języku. Każdemu takiemu opisowi odpowiada wiele różnych modeli; sytuacja jest więc raczej taka:
33
...........
.................. ........................
.......
.....
.....
....
....
...
....
...
..
.
...
..
....
...
.
.
.....
.
..
.
.......
.
.
.
.
.
.................. ....................
...........
JĘZYK
ac
r et
er p
int
?
..........................................
......
..........
......
....
...
....
...
...
...
.
..
...
...
0
..
....
...
.
.....
.
.
...
........
.
.
.
.
.
.
...............................................
MODEL
ja 1
MODEL
interpretacja0
in
t
e
rp
re
1.5.1.1
−1
···
tac
ja
...................................
...............
.......
....
.......
....
....
...
...
....
...
..
.
...
−1
..
....
...
.
.....
.
.
..
.
........
.
.
.
.
.
...............................................
^
..................................
...............
.......
.......
.....
....
...
...
...
....
..
..
...
1
..
....
...
.
.
.
.....
.
...
.
........
.
.
.
.
.
...............................................
MODEL
···
Język logiki pierwszego rzędu
Najpierw zajmijmy się językiem. U podstaw każdej logiki pierwszego rzędu leżą
• skończony zbiór Fun symboli funkcyjnych; każdy symbol funkcyjny f ∈ Fun ma arność
oznaczającą (nieformalnie) liczbę argumentów, które f akceptuje:
ar
N; przez
: Fun → o
n
def
stałe rozumie się symbole funkcyjne o arności 0: Const =
f ∈ Fun ar(f ) = 0 ;
• zbiór Var zmiennych;
• skończony zbiór Rel symboli relacyjnych; również każdy symbol relacyjny posiada arność
ar : Rel → N.
Nad tymi zbiorami symboli budujemy termy i formuły wg następujących zasad:
Definicja 1.50
Zbiór Term jest najmniejszym zbiorem napisów, takim że
• Var ⊆ Term ; czyli każda zmienna jest termem;
• jeśli f ∈ Fun , ar(f ) = n i t1 , t2 , . . . , tn ∈ Term , to f (t1 , t2 , . . . , tn ) ∈ Term ;
czyli jeśli do symbolu f doczepimy ar(f ) termów, to powstanie nowy term.
Definicja 1.51
Zbiór Form jest najmniejszym zbiorem napisów, takim że
• jeśli r ∈ Rel , ar(r) = n i t1 , t2 , . . . , tn ∈ Term , to r(t1 , t2 , . . . , tn ) ∈ Form ;
czyli jeśli do symbolu r doczepimy ar(r) termów, to powstanie formuła nazywana atomową;
• jeśli
–
–
–
–
–
–
v ∈ Var i α, α1 , α2 ∈ Form , to
negacja (¬α),
koniunkcja (α1 & α2 ),
alternatywa (α1 ∨ α2 ),
implikacja (α1 ⇒ α2 ),
równoważność (α1 ⇔ α2 ),
kwantyfikacje (∀v α) i (∃v α)
należą do Form.
34
Przykład 1.52
W arytmetyce mamy:
Fun = {0, 1, 2, . . . , +, · }
przy czym ar(0) = 0
ar(1) = 0
ar(2) = 0
···
ar(+) = 2
ar(·) = 2
Rel = {=, 6=, <, ¬, >, ­}
przy czym ar(=) = 2
ar(6=) = 2
ar(<) = 2
ar(¬) = 2
ar(>) = 2
ar(­) = 2
Dlatego np. napis · (2(), +(x, 1())) jest termem. Oczywiście do zapisu takich termów będzie
stosowana bardziej zwyczajna notacja 2 · (x + 1) . Podobnie napis
(∀x > (· (2(), +(x, 1())), y) ∨ = (y, 0()))
lub w zwyczajnej notacji:
∀x (2 · (x + 1) > y ∨ y = 0)
jest formułą.
v ∈ Var f ∈ Fun r ∈ Rel
t
α
t ∈ Term
α ∈ Form
::= v | f (t1 , t2 , . . . , tar(f ) )
::= r(t1 , t2 , . . . , tar(r) ) | (¬α)
| (α1 & α2 ) | (α1 ∨ α2 ) | (α1 ⇒ α2 ) | (α1 ⇔ α2 )
| (∀v α) | (∃v α)
Rys. 1.7: Składnia języka logiki pierwszego rzędu.
1.5.1.2
Interpretacja języka w modelu
Dotąd omówiona została tylko składnia, czyli język, złożony z napisów, które na razie
niczego nie znaczą. Znaczenie otrzymują dopiero w modelu29 poprzez funkcję interpretującą.
Definicja 1.53
Przez model logiki pierwszego rzędu rozumiemy parę hM, Ii, w której
• M jest niepustym zbiorem, t.zw. nośnikiem modelu;
• I jest funkcją interpretującą symbole funkcyjne i relacyjne, w taki sposób że
– dla każdego f ∈ Fun,
I [[f ]] : M
× .{z
. . × M} → M
|
ar ( f ) razy
jest funkcją ar ( f )-argumentową na modelu M ;
29
Termin „model” jest używany w logice w innym sensie niż może być znany czytelnikowi np. z nauk
społecznych i innych (i innym niż w tytule rozdz.1.2). To nie jest matematyczna teoria opisująca rzeczywistość,
tylko odwrotnie, każda z rzeczywistości odpowiadających teorii.
35
– dla każdego r ∈ Rel,
I [[r]] : M
× .{z
. . × M} → {false, true}
|
ar ( r) razy
jest funkcją logiczną ar ( r)-argumentową na modelu M .
Termy i formuły mogą zawierać zmienne; wobec tego, żeby zinterpretować termy jako
elementy modelu a formuły jako wartości logiczne, trzeba znać chwilowe wartości zmiennych.
Dlatego interpretacje termów i formuł działają na wartościowaniach zmiennych:
Definicja 1.54
Wartościowaniem nazywamy dowolne przypisanie zmiennym języka wartości z modelu M ;
def
zbiór wartościowań oznaczamy Val =
Var → M .
Twierdzenie 1.55
Interpretacja I rozszerza się jednoznacznie do interpretacji termów i formuł jako funkcji
na wartościowaniach:
I [[t]] : Val → M
dla dowolnego t ∈ Term
I [[α]] : Val → {false, true} dla dowolnego α ∈ Form
w taki sposób, że spełnione są równości z rys. 1.8.
Przykład 1.56
„Naturalna” interpretacja arytmetycznego przykładu 1.52 jest następująca:
• za nośnik modelu służy zbiór N liczb naturalnych;
• funkcja interpretacji przypisuje symbolom funkcyjnym funkcje arytmetyczne a symbolom relacyjnym funkcje logiczne w sposób zgodny z intuicją; np.
I [[0]] to prawdziwa liczba 0 ∈ N,
I [[+]] to prawdziwa funkcja dodawania + : N × N → N,
I [[=]] to prawdziwa relacja równości = : N × N → {false, true},,
itd.
Załóżmy, że σ ∈ Val jest takim wartościowaniem, że
σ(x) = 2
oraz
σ(y) = 1
Łatwo się przekonać, że wtedy
I [[2 · (x + 1)]] σ = 6
oraz
I [[∀x (2 · (x + 1) > y ∨ y = 0)]] σ = true
W tym ostatnim przypadku wartość σ(x) jest nieistotna.
36
I [[v]] σ = σ(v)
I [[f (t1 , . . . , tn )]] σ = I [[f ]] (I [[t1 ]] σ , . . . , I [[tn ]] σ)
I [[r(t1 , . . . , tn )]] σ = I [[r]] (I [[t1 ]] σ , . . . , I [[tn ]] σ)
I [[(¬α)]] σ =
(
true
false
I [[(α1 & α2 )]] σ =
(
I [[(α1 ∨ α2 )]] σ =
(
false
I [[α2 ]] σ
true
I [[α2 ]] σ
I [[(α1 ⇒ α2 )]] σ =
(
I [[(α1 ⇔ α2 )]] σ =
(
I [[(∀v α)]] σ =
I [[(∃v α)]] σ =
jeśli I [[α]] σ = false
jeśli I [[α]] σ = true
true
I [[α2 ]] σ
true
false


true








false


true








false
jeśli I [[α1 ]] σ = false
jeśli I [[α1 ]] σ = true
jeśli I [[α1 ]] σ = true
jeśli I [[α1 ]] σ = false
jeśli I [[α1 ]] σ = false
jeśli I [[α1 ]] σ = true
jeśli I [[α1 ]] σ = I [[α2 ]] σ
jeśli I [[α1 ]] σ 6= I [[α2 ]] σ
jeśli I [[α]] σ ′ = true
dla wszystkich σ ′ ∈ Val różniących się od σ najwyżej dla zmiennej v:
σ ′ (v ′ ) = σ(v ′ ) dla v ′ ∈ Var r {v}
w przeciwnym razie
jeśli I [[α]] σ ′ = true
dla jakiegoś σ ′ ∈ Val różniącego się od σ najwyżej dla zmiennej v:
σ ′ (v ′ ) = σ(v ′ ) dla v ′ ∈ Var r {v}
w przeciwnym razie
dla dowolnych σ ∈ Val , v ∈ Var , f ∈ Fun , r ∈ Rel , t1 , . . . , tn ∈ Term , α, α1 , α2 ∈ Form .
Rys. 1.8: Równości spełnione przez interpretację logiki pierwszego rzędu (do tw. 1.55).
Ale w samym języku nie ma nic, co by nas zmuszało do nadania symbolom języka arytmetyki właśnie takiego znaczenia jak w powyższym przykładzie. Inny model moglibyśmy uzyskać
biorąc za nośnik pojedynczy punkt i sklejając wszystkie termy do tego jednego punktu. Taki
model byłby bezużyteczny, ale formalnie poprawny.
Definicja 1.57
Mówimy, że formuła α jest spełniona w modelu M = hM, Ii ozn. M α, jeśli dla każdego
wartościowania σ ∈ Val zachodzi
I [[α]] σ = true
37
1.5.1.3
Teoria aksjomatyczna
Mamy więc język formalny, którym potrafimy mówić o modelu. Interesują nas własności modelu, ale wyrażamy je w języku. Wobec tego dla badania własności modelu, musimy
dysponować reprezentacją tych własności w języku.
Taką reprezentację stanowi teoria aksjomatyczna. Składa się ona
• z aksjomatów , którymi są jakieś wybrane formuły z języka; zakłada się zwykle, że wszystkie zmienne formuły, będącej aksjomatem, powinny być związane jakimś kwantyfikatorem tej formuły; oraz
• z reguł wnioskowania, czyli syntaktycznych zasad przekształcania formuł w inne formuły.
Przykład 1.58
Dla opisu własności liczb naturalnych przyjmuje się zwykle t.zw. aksjomatykę Peano,
patrz rys. 1.9. Występujący w aksjomatach symbol ⊢ oznacza „daje się udowodnić” lub „ jest
twierdzeniem”. Aksjomaty są twierdzeniami z założenia; ale żeby móc postawić znak ⊢ przed
jakąkolwiek inną formułą, będzie potrzebny dowód (patrz niżej).
Definicja 1.59
Dowodem formuły α w teorii aksjomatycznej nazywamy drzewo, w którego węzłach rozmieszczone są formuły w taki sposób, że
• w liściach stoją aksjomaty,
• w korzeniu stoi formuła α,
• formuła w dowolnym węźle nie będącym liściem jest wyprowadzona z formuł w potomkach tego węzła przy pomocy jakiejś reguły wnioskowania.
Definicja 1.60
Każdą formułę α, posiadającą dowód w teorii aksjomatycznej, nazywamy twierdzeniem,
ozn, ⊢ α.
Przykład 1.61
Na rys. 1.10 przedstawiony jest dowód, że ⊢ 2 · 2 = 4 w aksjomatyce Peano. Ale dowód
ten jest jeszcze niepełny, bo korzysta z oczywistych ale niewymienionych jawnie własności
równości, rachunku zdań oraz rachunku kwantyfikatorów. Prezentacja dowodu jest liniowa a
nie drzewiasta, bo tak wielkie drzewo trudno byłoby narysować. . .
Jak widać dowodzenie wprost z oszczędnie sformułowanych aksjomatów przypomina mikroprogramowanie komputera — trzeba się bardzo napracować dla udowodnienia każdego
drobiazgu. W praktyce oczywiście nie robi się tego; jednak ważne jest, że da się to zrobić.
Aksjomatyka żyje cała po stronie języka. Ale żeby miała sens, musi być powiązana z
modelami. Załóżmy więc, że mamy dany model M = hM, Ii.
38
Specyficzne aksjomaty liczb naturalnych:
N1: ⊢ ¬∃n 0 = n + 1 — zero nie jest następnikiem żadnej liczby;
N2: ⊢ ∀n ∀m n + 1 = m + 1 ⇒ n = m — dwie liczby o równych następnikach są sobie równe;
N3: niech P będzie jakąś własnością liczb naturalnych; wtedy
⊢ (P (0) & (∀n P (n) ⇒ P (n + 1))) ⇒ ∀n P (n)
N4:
N5:
N6:
N7:
N8:
N9:
— jeśli własność P przysługuje liczbie 0 oraz z tego, że P przysługuje jakiejś liczbie n,
wynika, że P przysługuje liczbie n + 1, to własność P przysługuje wszystkim liczbom
naturalnym.
⊢ ∀n n + 0 = n — dodanie zera nie zmienia liczby;
⊢ ∀n ∀k n + (k + 1) = (n + k) + 1 — łączność dodawania (tylko dla liczby 1, dla wyższych daje się już wyprowadzić);
⊢ ∀n n · 0 = 0 — wynikiem mnożenia przez zero jest zero;
⊢ ∀n ∀k n · (k + 1) = n · k + k — rozdzielność mnożenia względem dodawania (też tylko dla liczby 1);
⊢ ∀n ∀m (n ¬ m ⇔ ∃k n + k = m) — liczba n jest mniejsza od liczby m, jeśli m jest
wynikiem dodania jakiejś liczby do n;
0+1 =1
1+1 =2
2+1 =3
3+1 =4
···
Punkt N3 przedstawia t.zw. aksjomat indukcji. Właściwie nie jest to pojedynczy aksjomat,
tylko nieskończony schemat aksjomatów , po jednym aksjomacie dla każdej formuły języka liczb
naturalnych przedstawionego w przykładzie 1.52. Punkt N4 i dalsze to właściwie definicje
działań, porzadku i stałych. Dla zwięzłości nie zostały zamieszczone aksjomaty definiujące
pozostałe relacje porównywania liczb, ale czytelnik bez trudu je dopisze.
Oprócz aksjomatów dotyczących liczb naturalnych potrzebne są jeszcze aksjomaty dotyczące
rachunku zdań i rachunku kwantyfikatorów, bo arytmetyka opiera się na nich. Te aksjomaty
nie zostaną tu omówione.
Arytmetyka Peano nie potrzebuje żadnych specyficznych reguł wnioskowania; te należące do
rachunku zdań wystarczą. Spośród nich najbardziej znana jest reguła odrywania, czyli modus
ponens:
⊢p
⊢p⇒q
⊢q
Inne reguły dotyczą możliwości podstawiania równego za równe.
Rys. 1.9: Aksjomatyka Peano dla liczb naturalnych.
39
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
⊢
⊢
⊢
⊢
⊢
⊢
⊢
⊢
⊢
⊢
⊢
⊢
⊢
⊢
⊢
⊢
⊢
⊢
⊢
⊢
⊢
⊢
⊢
⊢
⊢
⊢
⊢
0+1 =1
1+1 =2
2+1 =3
3+1 =4
2 · 2 = 2 · (1 + 1)
2 · (1 + 1) = 2 · 1 + 2
2 · 1 + 2 = 2 · 1 + (1 + 1)
2 · 1 + (1 + 1) = (2 · 1 + 1) + 1
2 · 1 = 2 · (0 + 1)
2 · (0 + 1) = 2 · 0 + 2
2·0 =0
2·0+2 = 0+2
0 + 2 = 0 + (1 + 1)
0 + (1 + 1) = (0 + 1) + 1
(0 + 1) + 1 = 1 + 1
(0 + 1) + 1 = 2
0 + (1 + 1) = 2
0+2 =2
2·0+2 = 2
2 · (0 + 1) = 2
2·1 =2
2 · 1 + (1 + 1) = (2 + 1) + 1
2 · 1 + (1 + 1) = 3 + 1
2 · 1 + (1 + 1) = 4
2·1+2 = 4
2 · (1 + 1) = 4
2·2 =4
—
—
—
—
—
—
—
—
—
—
—
—
—
—
—
—
—
—
—
—
—
—
—
—
—
—
—
z
z
z
z
z
z
z
z
z
z
z
z
z
z
z
z
z
z
z
z
z
z
z
z
z
z
z
aks. N9
aks. N9
aks. N9
aks. N9
2 i własności równości
aks. N7
2 i własności równości
aks. N5
1 i własności równości
aks. N5
z6
11 i własności równości
2 i własności równości
aks. N5
1 i własności równości
15, 2 i własności równości
14, 16 i własności równości
13, 17 i własności równości
12, 18 i własności równości
10, 19 i własności równości
9, 20 i własności równości
8, 21 i własności równości
22, 3 i własności równości
23, 4 i własności równości
7, 24 i własności równości
6, 25 i własności równości
5, 26 i własności równości
Rys. 1.10: Dowód, że ⊢ 2 · 2 = 4 .
Definicja 1.62
Mówimy, że teoria aksjomatyczna jest zgodna z modelem 30 M, jeśli dla dowolnej formuły α,
jeśli ⊢ α to M α
czyli twierdzenia teorii są spełnione w modelu.
Mówimy, że teoria jest pełna dla modelu M, jeśli dla dowolnej formuły α,
jeśli M α to ⊢ α
czyli formuły spełnione w modelu są twierdzeniami teorii31 .
Jeśli interesują nas własności jakiegoś modelu, to od opisującej go teorii musimy wymagać
zgodności; teoria niezgodna jest bezużyteczna, bo pozwala wyprowadzać twierdzenia, które
w tym modelu wcale nie są spełnione.
30
31
Ang. sound ; w terminologii polskiej nie ma powszechnie przyjętego jednego terminu.
Te dwa pojęcia mają związek z niesprzecznością i zupełnością teorii, ale nie są z nimi tożsame.
40
Chciałoby się jeszcze, żeby taka teoria była pełna. Jednak okazuje się, że na ogół nie da
się tego zrobić, ze względu na twierdzenie Gödla. Ale teoria niepełna, choć niedoskonała, nie
jest bezużyteczna.
Twierdzenie 1.63
Na to, żeby teoria była zgodna z modelem M, potrzeba i wystarcza, żeby
• jej aksjomaty były spełnione w M, oraz
• jej reguły wnioskowania przeprowadzały formuły spełnione w M na formuły spełnione
w M.
W rozdziale 1.6 zostaną omówione zastosowania logiki formalnej do weryfikacji poprawności programów.
1.5.2
Logiki nieklasyczne
Klasyczna logika, taka jak została wyżej przedstawiona, z różnych powodów nie w pełni
satysfakcjonuje informatyków. Dlatego żywe są badania różnych innych wersji. Zwykle chodzi
nie o to, żeby myśleć inaczej niż czynią to matematycy, a raczej o to, żeby pozwolić opisom
programów odbiegać od klasycznych ram.
Poniżej znajduje się bardzo szkicowy opis motywacji leżących u podstaw niektórych odstępstw od klasyki. Nie jest on ani wyczerpujący, ani głęboki; ma służyć tylko orientacji.
1.5.2.1
Logiki wielowartościowe
W klasycznej logice mamy do czynienia z dwiema wartościami logicznymi: false oraz true.
Elementarne wykłady logiki zawsze zaczynają się od tabelek działań spójników zdaniowych,
takich jak negacja czy koniunkcja, na tych dwóch wartościach.
Z punktu widzenia programisty te tabelki są niepełne, to znaczy nie dają pełnej wiedzy o
możliwych wynikach działań.
Przykład 1.64
Załóżmy, że mamy funkcję zdefiniowaną następująco:
nat : Z →
˜ {false, true}
fun nat(n) :
if n = 0 then return true
else return nat(n − 1)
fi
Czyli jest to funkcja z liczb całkowitych do wartości logicznych dająca wartość true na liczbach
naturalnych, a licząca w nieskończoność na pozostałych, czyli ujemnych.
Rozważmy teraz dwa wyrażenia, które programista mógłby napisać w swoim programie:
1. n ­ 0 & f (n)
oraz
2. f (n) & n ­ 0
41
Jaka będzie wartość tych wyrażeń dla n = −1 ?
Okazuje się, że odpowiedź zależy od konkretnego języka programowania.
W dawnych językach (np. w dawnych wersjach Pascala) koniunkcja była operacją przemienną, więc oba wyrażenia dawały ten sam wynik: nieskończoną pętlę. A to dlatego, że
komputer najpierw obliczał wartość obu argumentów, a dopiero potem stosował koniunkcję
do wyników. Ale jeśli obliczenie któregoś argumentu było nieskończone, to wyniku już nie
było.
W nowszych językach (np. w C) zrezygnowano z przemienności koniunkcji, żeby uniknąć
ślepej pętli tam, gdzie się da. Komputer najpierw oblicza pierwszy argument i jeśli wyjdzie
mu false, to drugiego argumentu już wcale nie liczy, tylko uznaje koniunkcję za fałszywą.
Wobec tego wartością wyrażenia 1 będzie false, a wartością wyrażenia 2 będzie nieskończona
pętla.
Oto tabelki dla koniunkcji w trójwartościowym rachunku zdań; wartość „ślepa pętla” jest
oznaczona przez ⊥:
koniunkcja przemienna:
&
true
false
⊥
true false
true false
false false
⊥
⊥
⊥
⊥
⊥
⊥
koniunkcja nieprzemienna:
&
true
false
⊥
true false
true false
false false
⊥
⊥
⊥
⊥
false
⊥
Wprowadzenie trzeciej wartości logicznej ma dość głębokie konsekwencje w całej logice
programów.
Istnieją jeszcze inne poważne powody badania niestandardowych wartości logicznych,
szczególnie w dziedzinie sztucznej inteligencji, ale leżą one poza zakresem tej pracy.
1.5.2.2
Krótka informacja o logikach modalnych
Logiki modalne stanowią próbę „zmiękczenia” kategoryczności sądów logiki klasycznej.
Operatory modalne wprowadzają rozróżnienie między stwierdzeniami „tak jest”, „tak musi
być” i „tak może być”. Zwykle oznacza się te wypowiedzi operatorami modalnymi:
• ϕ (czytamy: ϕ zachodzi);
np. „pada deszcz”— ta wypowiedź może być prawdziwa lub fałszywa, zależnie od dnia;
• ϕ (czytamy: ϕ koniecznie zachodzi);
np. „musi padać deszcz” lub „zawsze pada deszcz” — ta wypowiedź jest fałszywa, bo
jednak czasem deszcz nie pada, nawet w Anglii;
• ♦ϕ (czytamy: jest możliwe, że ϕ zachodzi);
np. „być może pada deszcz” lub „czasem pada deszcz” — ta wypowiedź jest prawdziwa.
Logika, posługująca się takimi modalnościami, jest dyscypliną starą i szacowną. Jednak od
stosunkowo niedawna różne jej wersje stosuje się do opisu efektów współbieżnego wykonywania
obliczeń.
Ze współbieżnością i związanym z nią niedeterminizmem mamy do czynienia przy badaniu współdziałania wielu komputerów, np. za pośrednictwem internetu; albo w przypadku
pojedynczych komputerów o wielu procesorach.
42
Jedną z wersji logiki modalnej, stosowaną w kontekście współbieżności, jest logika temporalna. Stosowane w niej operatory modalne mają znaczenie kwantyfikowania po czasie;
jednak ich rachunek jest prostszy niż jawne operowanie na takich kwantyfikatorach. Logika
temporalna rozszerza klasyczną logikę o dwa operatory modalne:
• Gϕ (czytamy: odtąd zawsze będzie zachodziło ϕ);
• F ϕ (czytamy: kiedyś zajdzie ϕ).
Przy pomocy takich operatorów można charakteryzować sposób działania programu. Oto
przykłady sądów, które mogą być użyteczne przy dowodzeniu ich poprawności:
• F (x = 0) — kiedyś x stanie się równe zeru;
• GF (x = 0) — co jakiś czas zdarza się (będzie się zdarzać, nigdy na dobre nie przestanie
się zdarzać), że x = 0;
• G(x · ai = ab ) & F (i = 0) — zawsze będzie zachowany związek x · ai = ab a ponadto
kiedyś i stanie się równe 0; jasne jest, że jeśli to jest prawda, to F (x = ab ), czyli kiedyś x
stanie się potęgą a;
• G(sygnał ⇒ F reakcja) — system nigdy nie pozostawi żadnego sygnału bez reakcji, chociaż między sygnałem a reakcją może minąć nieokreślony czas.
Inną wersją logiki modalnej jest logika dynamiczna. Każdy program służy w niej jako
osobny operator modalny działający na klasycznych formułach logicznych.
1.5.2.3
Krótka informacja o intuicjonizmie i konstruktywizmie
Standardową metodą dowodzenia, że coś istnieje, jest
konstrukcja. Np. żeby udowodnić, że do każdego kąta istnieje
kąt o połowę od niego mniejszy, możemy przeprowadzić standardową konstrukcję dwusiecznej kąta przy pomocy cyrkla i
linijki.
Jednak istnieją również niekonstrukcyjne dowody istnienia. Na przykład wiedząc, że funkcja ciągła f : [−1 . . 1] → R
ma taką własność, że f (−1) < 0 i f (1) > 0, możemy być pewni, że gdzieś w odcinku [−1 . . 1] jest punkt x0 , dla którego
f (x0 ) = 0, bo wykres funkcji musi jakoś przechodzić z jednej
strony osi x na drugą32 .
........
......
......
.....
....
....
....
....
...
...
...
...
..
...
...
...
..
..
..
.
..
..
.................
..........
... ...
.
Takie niekonstrukcyjne dowody sprawiają pewne trudności filozoficzne i dlatego od dawna
były kwestionowane przez niektórych matematyków. Okazuje się, że możliwe jest stworzenie
całej konkurencyjnej matematyki, w której nie ma żadnych niekonstrukcyjnych dowodów istnienia. Matematyka intuicjonistyczna jest uboższa niż klasyczna, to znaczy każde twierdzenie
intuicjonistyczne jest również twierdzeniem klasycznym, ale nie każde twierdzenie klasyczne jest uznawane przez intuicjonistów. Wypada na przykład zasada wyłączonego środka:
⊢ ϕ ∨ ¬ϕ ; to wyklucza dowody „nie wprost”. Bardzo spłaszczona jest teoria mocy zbiorów:
ponieważ wszelkich rzeczy jest tylko tyle, ile potrafimy ich skonstruować, a wszelkich konstrukcji jest ℵ0 (czyli przeliczalnie wiele, czyli tyle, ile liczb naturalnych), więc nie da się
32
To jest t.zw. własność Darboux .
43
już zbudować hierarchii coraz wyższych nieskończoności, jak w przypadku klasycznym. Raczej niechcianą konsekwencją założeń intuicjonistycznych jest to, że iloczyn kartezjański wielu
zbiorów niepustych może być pusty33 .
Jednak cokolwiek robi komputer, jest konstruowalne, mieści się więc wewnątrz tej mniejszej matematyki intuicjonistycznej. Nie mamy w nim nigdy do czynienia z pełnym zbiorem
liczb rzeczywistych matematyki klasycznej, a tylko z ich przeliczalnym przybliżeniem. Również
funkcji mamy tylko przeliczalnie wiele. Stąd zainteresowanie informatyków logiką intuicjonistyczną.
Jako wsparcie tego kierunku myślenia okazało się, że istnieje daleko sięgająca paralela
między logiką intuicjonistyczną a systemem typów używanym przez informatyków: intuicjonistyczne reguły wnioskowania zbudowane są tak samo jak reguły typowania wyrażeń.
Krytycy czysto intuicjonistycznego podejścia do informatyki skłaniają się do poglądu,
że procesy obliczeniowe komputerów mieszczą się w pełni w świecie intuicjonistycznym, ale
nasze rozumowanie o tych procesach już się nie zawsze w tym świecie mieści. Dlatego logika
intuicjonistyczna może być użytecznym narzędziem, ale nie może zastąpić klasycznej.
1.6
Dowodzenie poprawności programów
Dzięki konstrukcji pętli, działającej dopóki spełniony jest pewien warunek (pętla while),
skończony program komputerowy może wygenerować nieskończenie wiele różnych obliczeń
zależnie od dostarczonych mu danych. Stąd bierze się moc komputerów, ale również stąd
bierze się trudność w weryfikacji ich poprawności. Ponieważ różnych obliczeń nawet prostego
programu jest nieskończenie wiele, nie ma praktycznej możliwości przetestowania wszystkich.
Zgodnie z powszechną wiedzą programistów:
testowanie może wykazać błędność programu,
ale nie może wykazać jego poprawności.
Czy więc w ogóle jakoś można się przekonać o poprawności programu komputerowego?
Pierwsze matematyczne podejście do dowodzenia poprawności algorytmów (w odróżnieniu
od testowania), oparte o pojęcie niezmiennika, zostało zaproponowane przez Roberta Floyda, a następnie przekształcone w dojrzały system wnioskowania o poprawności przez Tony
Hoare’a.
Logika Hoare’a jest wyłożona w wielu podręcznikach, ale zwykle jako dodatek do innych
rozważań o programowaniu. Dosyć pełny przegląd reguł wnioskowania dla częściowej poprawności, również dla szerszych klas programów, niż omówione tutaj, znajduje się w Dahl [7]. Z
podręczników dostępnych po polsku, można polecić Alagić i Arbib [2] oraz Dembiński i Małuszyński [8].
Aksjomaty dla poprawności całkowitej są omówione w Nielson i Nielson [16], a także
w Sokołowski [17]. Nieco inne podejście do dowodzenia całkowitej poprawności przedstawia
Manna i Pnueli [14] (opisane również w Manna [13]).
Problemy ze zgodnością i zupełnością zostały opisane np. w Apt [3].
33
Bo jego niepustość jest równoważna aksjomatowi wyboru, który intuicjoniści też odrzucają jako niekonstruktywny.
44
1.6.1
Programowanie z niezmiennikami
Idea niezmiennika bierze się spoza informatyki; źródłem może być na przykład następujące
proste zadanie z fizyki:
Przykład 1.65
Kulka toczy się z prędkością 2 m/sek. po
poziomej drodze, potem stacza się do dołka o
2 m/sek
dość skomplikowanym kształcie (patrz: rysu...............
.......
......
6
nek obok), a po jego przebyciu toczy się dalej
......
.....
..........................
..........
.....
6
12 m
po poziomej drodze znajdującej się o 2 m ni.......
.....
.
.
.
.
......
10 m
....
.
.
.
.
.
.
.
.
.
.
............. .............
żej niż droga oryginalna. Z jaką prędkością v1
?
?
.........
będzie się toczyć po tej nowej drodze? Opory
ruchu oraz moment bezwładności kulki zaniedbujemy.
Oczywiście, wpadając do dołka, kulka zwiększa prędkość, a „wygrzebując” się znowu z
niego, zmniejsza prędkość.
Trudno jest prześledzić wszystkie zmiany pozycji i prędkości kulki wzdłuż skomplikowanej
krzywej. Na szczęście wiemy, że jej energia jest niezmiennikiem takiego ruchu, wobec tego jest
taka sama na górnej drodze jak na dolnej drodze:
m · v12
m · v02
+ mgh0 =
+ mgh1
2
2
gdzie m jest masą kulki, h0 i h1 to wysokości obu dróg ponad jakiś poziom odniesienia, g to
ziemskie przyspieszenie grawitacyjne. Stąd:
(2 m/sek.)2
v2
+ gh0 = 1 + gh1
2
2
i
(2 m/sek.)2
4 m2
v12
m
=
+ g(h0 − h1 ) =
+ 10 · (h0 − h1 )
2
2
2
2 sek.
sek.2
m2
m2
m2
+
20
=
22
= 2
sek.2
sek.2
sek.2
więc
v1 =
√
44
m
m
≈ 6.63
sek.
sek.
Okazuje się, że dla wyliczenia rezultatu końcowego nie trzeba znać wszystkich wartości
pośrednich. Wystarczy wiedzieć, że pewien związek między interesującymi nas wielkościami
(energia) jest stały wzdłuż całego toru ruchu; oraz znać jego wartość na początku toru.
Podobne metody stosujemy przy badaniu własności pętli:
1. proponujemy pewien związek między zmiennymi programu;
2. dowodzimy, że jest on niezmienniczy, czyli nie narusza go pojedyncze przejście przez
ciało pętli;
45
3. pokazujemy, że jest spełniony przy pierwszym wejściu do pętli;
4. stąd wnioskujemy, że jest nadal spełniony przy wyjściu z pętli; i z warunku wyjścia
wyliczamy wartości interesujących nas zmiennych.
Główna różnica w stosunku do fizyki jest taka, że informatyk musi wymyślić inny niezmiennik
do każdej konstruowanej przez siebie pętli; a w fizyce każdy nowy niezmiennik jest odkryciem
na miarę Nobla.
Przykład 1.66
Jak udowodnić, że pętla, przedstawiona na rysunku obok, liczy potęgę; czyli że po jej wykonan­0
?
niu zachodzi p = xn ?
k ← 0; p ← 1
Najpierw wykazujemy, że p = xk & k ¬ n jest
niezmiennikiem pętli. Istotnie, każde wykonanie
p = xk & k ¬ n
ciała pętli zwiększa k o jeden, wobec tego zwięk?
sza xk x-krotnie; jednocześnie domnaża p przez x,
k<n | k=n
zachowując równość. W dodatku to zwiększenie
?
dokonuje się tylko wtedy, gdy k < n, więc k nie
k ← k + 1; p ← p · x
może przekroczyć n. Wobec tego, jeśli spełnione
p = xn
jest p = xk & k ¬ n oraz warunek k < n wejścia
?
do ciała pętli, to po jednorazowym przejściu przez
ciało pętli formuła jest nadal spełniona.
Na początku pętli, w wyniku wykonania pierwszych dwóch podstawień, formuła ta ma
postać 1 = x0 & 0 ¬ n, więc jest spełniona. Tak więc ze wględu na niezmienniczość jest nadal spełniona po zakończeniu działania pętli. Ale wtedy k = n, więc z niezmiennika wynika
formuła p = xn .
Nasuwa się pytanie, jak do istniejącego programu znajdować właściwe niezmienniki. Okazuje się, że jest to problem nierozstrzygalny; to znaczy, że nie istnieje algorytm znajdujący
takie niezmienniki automatycznie do każdego programu. Nie przekreśla to jednak możliwości inteligentnego znalezienia niezmienników do danego konkretnego programu. Ale szukanie
niezmienników do już istniejącego, być może błędnego, programu nie jest właściwą drogą
postępowania.
Konstruując program do przykładu 1.66 najpierw napisałem formułę p = xk , a dopiero potem tak dostroiłem fragmenty pętli, żeby ta formuła istotnie była niezmiennikiem. To wydaje
się lepsze — konstruować program zaczynając od niezmienników, nowe niezmienniki zawsze
o pół kroku przed odpowiednimi komendami programu.
1.6.2
Aksjomatyka Hoare’a poprawności częściowej
Metoda niezmienników z rozdz. 1.6.1 daje się wyrazić w postaci systemu wnioskowania
wyprowadzającego stwierdzenia o poprawności programów. Wyprowadzane sądy mają postać
{P } s {Q}
gdzie s jest komendą (być może złożoną), a P i Q są formułami zbudowanymi na zmiennych
występujących w programie s i być może jeszcze jakichś. Nieformalnie taki sąd rozumiemy
następująco:
46
jeśli zaraz przed wejściem do komendy s spełnione jest P ,
to zaraz po wyjściu z komendy s spełnione jest Q.
System Hoare’a służący do wyprowadzania takich sądów przedstawiony jest na rys. 1.11.
Aksjomat komendy pustej (A∅):
Aksjomat przypisania (A←):
⊢ {P [e/x]} x ← e {P }
⊢ {P } {P }
Reguła komendy warunkowej (R if):
Reguła złożenia (R;):
⊢ {Q & p} s {R}
⊢ {Q & ¬p} t {R}
⊢ {Q} if p then s else t fi {R}
⊢ {P } s {Q}
⊢ {Q} t {R}
⊢ {P } s; t {R}
Reguła pętli (R while):
Reguła konsekwencji (R⇒):
⊢ {Q & p} s {Q}
⊢ {Q} while p do s od {Q & ¬p}
⊢ P ⇒ P′
⊢ {P ′ } s {Q′ }
⊢ Q′ ⇒ Q
⊢ {P } s {Q}
Rys. 1.11: System Hoare’a wyprowadzania sądów o częściowej poprawności programów.
1.6.2.1
Aksjomaty i reguły
Jak widać, w przeciwieństwie do systemów przedstawionych w rozdziale 1.5 system Hoare’a
ma liczne reguły wnioskowania, ale za to niewiele aksjomatów. Aksjomaty opisują poprawność
pojedynczych komend prostych: komendy pustej oraz przypisania wartości zmiennej. Reguły
wnioskowania służą dowodzeniu poprawności komend złożonych w oparciu o już udowodnioną poprawność innych komend. Trzy z nich wyprowadzają poprawność złożenia, komendy
warunkowej i komendy pętli z poprawności ich części składowych. Czwarta jest specjalna i
wymaga bardziej szczegółowego omówienia.
Reguła konsekwencji, jako jedyna w całym systemie, łączy logikę programów z teorią
opisującą dane, na których działają programy; np. z arytmetyką Peano, jeśli program działa
na liczbach naturalnych. Aksjomatyka Hoare’a jest rozszerzeniem teorii danych na teorię
programów nad tymi danymi.
Na rys. 1.12 zademonstrowane jest wyprowadzenie sądu
⊢ {n ­ 0} k ← 0; p ← 1;
while k < n do k ← k + 1; p ← p · x od
n
{p = x }
Oznacza on, że jeśli podana komenda złożona zostanie uruchomiona w dowolnym wartościowaniu, w którym n ­ 0, to po zakończeniu jej działania będzie spełniona formuła p = xn .
Ponieważ sama ta komenda nie zmienia wartości zmiennych n ani x, z tego sądu wynika, że komenda oblicza wartość xn i przypisuje ją zmiennej p 34 . Ściślej można to sformułować tak: jeśli
34
Jednak z tego sądu nie wynika, że ta komenda musi kiedyś zakończyć działanie; mogłoby się zdarzyć, że
będzie działać bez końca. Ten problem zostanie omówiony szczegółowiej w rozdz. 1.6.2.2
47
1. ⊢ {1 = x0 & 0 ¬ n} k ← 0 {1 = xk & k ¬ n}
— z (A←)
2. ⊢ {1 = xk & k ¬ n} p ← 1 {p = xk & k ¬ n}
— z (A←)
3. ⊢ {1 = x0 & 0 ¬ n} k ← 0; p ← 1 {p = xk & k ¬ n}
— z 1, 2 i (R;)
4. ⊢ {p · x = xk+1 & k + 1 ¬ n} k ← k + 1 {p · x = xk & k ¬ n}
5. ⊢ {p · x = xk & k ¬ n} p ← p · x {p = xk & k ¬ n}
— z (A←)
— z (A←)
6. ⊢ {p · x = xk+1 & k + 1 ¬ n} k ← k + 1; p ← p · x {p = xk & k ¬ n}
7. ⊢ p = xk & k ¬ n & k < n ⇒ p · x = xk+1 & k + 1 ¬ n
8. ⊢ p = xk & k ¬ n ⇒ p = xk & k ¬ n
— z 4, 5 i (R;)
— z teorii liczb całkowitych
— z teorii liczb całkowitych
9. ⊢ {p = xk & k ¬ n & k < n} k ← k + 1; p ← p · x {p = xk & k ¬ n}
— z 7, 6, 8 i (R⇒)
10. ⊢ {p = xk & k ¬ n} while k < n do k ← k + 1; p ← p · x od {p = xk & k ¬ n & ¬k < n}
— z 9 i (R while)
11. ⊢ {1 = x0 & 0 ¬ n} k ← 0; p ← 1;
while k < n do k ← k + 1; p ← p · x od {p = xk & k ¬ n & ¬k < n}
— z 3, 10 i (R;)
12. ⊢ p = xk & k ¬ n & ¬k < n ⇒ p = xn
13. ⊢ n ­ 0 ⇒ 1 = x & 0 ¬ n
0
— z teorii liczb całkowitych
— z teorii liczb całkowitych
14. ⊢ {n ­ 0} k ← 0; p ← 1;
while k < n do k ← k + 1; p ← p · x od {p = xn }
— z 13, 11, 12 i (R⇒)
Rys. 1.12: Wywód poprawności częściowej programu z przykładu 1.66.
komenda zostanie uruchomiona na dowolnym wartościowaniu σ takim, że I [[n ­ 0]] σ = true
i zakończy działanie w jakimś wartościowaniu σ ′ , to I [[p = xn ]] σ ′ = true .
Jak widać z rys. 1.12, dowód nawet stosunkowo prostej własności prostego programu może być całkiem skomplikowany. Dlatego od dawna myślano o możliwościach automatycznego
generowania dowodów. Istnieją programy, które potrafią wczytać tekst programu komputerowego wzbogacony o niezmienniki (po jednym dla każdej pętli) i wygenerować formuły już
pozbawione elementów programistycznych35 , których udowodnienie wystarczy do stwierdzenia, że program jest poprawny. Jednak niezmienniki trzeba dostarczyć, w ogólnym przypadku
nie da się ich zgadnąć automatycznie. Również dowodów wygenerowanych formuł nie da się
przeprowadzić w pełni automatycznie.
1.6.2.2
Poprawność częściowa i całkowita
Metoda niezmienników Floyda-Hoare’a służy do udowadniania t.zw. częściowej poprawności.
Definicja 1.67
Komenda s jest częściowo poprawna względem formuł P i Q wtedy i tylko wtedy, gdy
∀σ,σ′ (I [[P ]] σ = true & I [[s]] σ = σ ′ ⇒ I [[Q]] σ ′ = true)
35
Takie jak przyjęte za twierdzenia teorii liczb całkowitych na rys. 1.12.
48
czyli gdy zachodzi następująca zależność:
jeśli P jest spełnione dla jakiegoś stanu σ i s przeprowadza stan σ w stan σ ′ ,
to Q jest spełnione dla stanu σ ′ .
Ale programistę interesuje zwykle silniejsza własność:
Definicja 1.68
Komenda s jest całkowicie poprawna względem formuł P i Q wtedy i tylko wtedy, gdy
∀σ (I [[P ]] σ = true ⇒ ∃σ′ (I [[s]] σ = σ ′ & I [[Q]] σ ′ = true))
czyli gdy zachodzi następująca zależność:
jeśli P jest spełnione dla jakiegoś stanu σ, to istnieje taki stan σ ′ ,
że s przeprowadza stan σ w stan σ ′ i Q jest spełnione dla stanu σ ′ .
Różnicę między poprawnością częściową a całkowitą stanowią obliczenia nieskończone: program chodzący w ślepej pętli jest częściowo poprawny względem każdej pary formuł, ale na
ogół nie jest całkowicie poprawny względem tych formuł.
Twierdzenie 1.69
Komenda s jest całkowicie poprawna względem P i Q wtedy i tylko wtedy, gdy s jest częściowo poprawna względem P i Q oraz s zatrzymuje się dla każdych danych spełniających P .
Zwykle więc, chcąc udowodnić całkowitą poprawność jakiejś komendy, dowodzimy osobno
• częściowej poprawności tej komendy, metodą niezmienników, oraz
• zatrzymywania, jakąś inną metodą.
Istnieją zasadniczo dwie metody dowodzenia zatrzymywania: metoda liczników pętli oraz
metoda zbiorów dobrze ufundowanych.
Metoda liczników pętli wymaga, żeby z każdą pętlą wewnątrz komendy związać wyrażenie
całkowite c (licznik pętli) oraz niezmiennik ϕ (na ogół różny od niezmiennika potrzebnego do
dowodu poprawności częściowej); a następnie wykazać, że
• pojedyncze przejście przez ciało pętli zmniejsza wartość c; oraz
• z ϕ wynika, że c ­ 0.
Stąd już wyniknie, że liczba przejść przez ciało pętli musi być ograniczona.
Przykład 1.70
Dla dowodu, że program w przykładzie 1.66 zatrzymuje się, weźmy
• wyrażenie n − k za licznik pętli;
• formułę k ¬ n za jej dodatkowy niezmiennik.
Niezmienniczość formuły k ¬ n jest oczywista; ograniczoność licznika przy zachowaniu tego
niezmiennika również. Należy jedynie pokazać, że każde przejście przez ciało pętli zmniejsza
wartość n − k.
49
Metoda liczników pętli została zaksjomatyzowana w system podobny do systemu Hoare’a.
Sformułowanie metody zbiorów dobrze ufundowanych jest bardziej skomplikowane, ale w
pewnych przypadkach ta metoda pozwala na prostsze dowody zatrzymywania się programu.
Definicja 1.71
Przez zbiór dobrze ufundowany rozumiemy parę hS, ≺i, gdzie
• S jest zbiorem,
• ≺ jest ostrym porządkiem częściowym na zbiorze S, czyli relacją antysymetryczną 36 i
przechodnią 37 ,
• porządek ≺ jest dobrze ufundowany, to znaczy nie istnieją nieskończone ciągi malejące
s0 ≻ s1 ≻ s2 ≻ . . . elementów zbioru S.
Dla dowodu, że dany program się zatrzymuje, należy dla każdej pętli znaleźć taką funkcję
ze zbioru wartościowań zmiennych do zbioru dobrze ufundowanego, której wartość maleje w
wyniku jednego przejścia przez ciało pętli.
Przykład 1.72
hN, <i, czyli liczby naturalne ze zwykłym ostrym porządkiem, to zbiór dobrze ufundowany. Ale hZ, <i, czyli liczby całkowite ze zwykłym porządkiem, nie tworzą zbioru dobrze
ufundowanego.
Inny przykład zbioru dobrze ufundowanego: hN × N, ≪i, gdzie ≪ jest porządkiem leksykograficznym:
def
(n1 , n2 ) ≪ (k1 , k2 ) ⇐⇒
n1 < k1 ∨ (n1 = k1 & n2 < k2 )
W tym zbiorze nie istnieją nieskończone ciągi malejące, ale mogą istnieć nieskończone ciągi
rosnące, nawet całe mieszczące się poniżej ustalonego elementu; np.
(0, 0) ≪ (0, 1) ≪ (0, 2) ≪ . . . ≪ (0, n) ≪ . . . ≪ (1, 0)
Możemy użyć tego zbioru np. do udowodnienia, że poniższa pętla zatrzymuje się dla dowolnych
początkowych wartości zmiennych n i k:
while n > 0 & k > 0 do
n ← losowa liczba całkowita z przedziału [0 . . n − 1];
k ← losowa liczba całkowita z przedziału [0 . . k − 1];
od
Istotnie, wartość funkcji
f : Val → N × N
def
f (σ) =
hσ(n), σ(k)i
maleje z każdym obrotem pętli.
Również metoda zbiorów dobrze ufundowanych została zaksjomatyzowana w system podobny do systemu Hoare’a.
36
37
Antysymetria oznacza ∀a,b∈S (a ≺ b ⇒ ¬ b ≺ a).
Przechodniość oznacza ∀a,b,c∈S (a ≺ b & b ≺ c ⇒ a ≺ c).
50
1.6.3
Problem zgodności i pełności reguł
Kiedy wprowadza się do języka programowania jakąś nową konstrukcję, w zasadzie należałoby wyrazić jej własności w postaci nowej reguły wnioskowania o poprawności. Okazuje
się jednak, że jest to zadanie niebezpieczne, bo łatwo można zaproponować złą regułę. Samemu Hoare’owi przytrafiło się ogłoszenie reguły dla funkcji definiowanych w programie, która
okazała się niezgodna z naturalnym modelem. Wobec tego potrzebne są kryteria badania
zgodności i pełności nowych reguł.
Ale logiki programów nie są teoriami samodzielnymi, a tylko rozszerzeniami teorii struktur danych, na których działają programy. Jeśli leżąca u podstaw teoria tych danych jest
niezgodna, to jej rozszerzenie automatycznie również staje się niezgodne bez żadnej własnej
„winy”. Jak więc w ogóle sformułować własność zgodności i pełności dla samego roszerzenia,
w abstrakcji od teorii bazowej?
1.6.3.1
Zgodność
W rozdz. 1.6.2.2 była już mowa o interpretacji komend, ale ta interpretacja nie została zdefiniowana38 . Definicja dla języka programowania, dla którego dyskutowaliśmy pojęcia
częściowej i całkowitej poprawności, znajduje się na rys. 1.13.
x ∈ Var
σ ∈ Val
e — wyrażenia
p — formuły
s, t — komendy
I [[s]] : Val →
˜ Val
def
I [[ ]] σ =
σ
def
I [[x ← e]] σ =
σ[I [[e]] σ / x]
def
I [[s; t]] σ =
I [[t]] (I [[s]] σ)
I [[if p then s else t fi]] σ =
def
I [[while p do s od]] σ =
def
(
(
I [[s]] σ
I [[t]] σ
jeśli I [[p]] σ = true
jeśli I [[p]] σ = false
σ
I [[while p do s od]] (I [[s]] σ)
jeśli I [[p]] σ = false
jeśli I [[p]] σ = true
Rys. 1.13: Semantyka języka programowania.
Interpretacja komendy jest funkcją częściową prowadzącą od wartościowań zmiennych do
wartościowań zmiennych. Częściowość bierze się z tego, że pętla może nie zakończyć działania
i nie dać żadnego wyniku.
Niech T będzie teorią aksjomatyczną zgodną z modelem M = hM, Ii. Taką teorię można
rozszerzyć o programy i system wnioskowania o częściowej poprawności w sposób opisany
wyżej (rys. 1.11 oraz rys. 1.13) — nazwijmy otrzymany system H(T , M).
38
Interpretacja samych wyrażeń i formuł została zdefiniowana, patrz rys. 1.8. Niezdefiniowana została tylko
interpretacja komend.
51
Definicja 1.73
Rozszerzenie H teorii o programy nazwiemy zgodnym, jeśli dla każdego modelu M i
zgodnej z nim teorii T , z tego, że ⊢ {P } s {Q} w H(T , M) wynika
∀σ,σ′ (I [[P ]] σ = true & I [[s]] σ = σ ′ ⇒ I [[Q]] σ ′ = true)
(por. z definicją 1.67).
Tak więc dla zgodności rozszerzenia wymaga się, żeby działało ono dobrze w teoriach
zgodnych z modelem. Od badania teorii niezgodnych z modelem jesteśmy zwolnieni.
Twierdzenie 1.74 System Hoare’a z rys. 1.11 jest zgodnym rozszerzeniem.
Tak więc w systemie Hoare’a nie da się wyprowadzić niepoprawnego sądu o częściowej poprawności programów.
Tego twierdzenia dowodzi się sprawdzając kolejno wszystkie aksjomaty i reguły wnioskowania rozszerzenia. Gdybyśmy chcieli wprowadzić nową komendę do języka, należałoby
udowodnić podobne twierdzenie dla nowego rodzaju rozszerzeń.
1.6.3.2
Pełność
Problem pełności rozszerzenia H(T , M) jest bardziej skomplikowany. Po wstępnym zapoznaniu się z systemem Hoare’a można łatwo dojść do przekonania, że jeśli dobrze się rozumie
program, to odpowiednie niezmienniki jakoś się wymyśli i dowód poprawności się przeprowadzi.
Tymczasem sytuacja jest bardziej skomplikowana. Otóż może się zdarzyć, że niezmienników nie da się sformułować nie „z winy” rozszerzenia, tylko z powodu ubóstwa języka teorii T .
Przykład 1.75
Spróbujmy udowodnić sąd o częściowej poprawności
⊢ {true} n ← −1;
przedstawiony obok.
while n 6= 0 do
Ten sąd oznacza, że program nie zatrzymuje się dla
n ← n−1
żadnych danych. Istotnie, gdyby się zatrzymał i dał wyod
nik, to ten wynik musiałby spełniać formułę false, co jest
{false}
niemożliwe.
Naturalnym sposobem dowodzenia byłoby przyjęcie formuły n < 0 za niezmiennik pętli.
Założenie
⊢ {n < 0 & n 6= 0} n ← n − 1 {n < 0}
reguły (R while) systemu z rys. 1.11 jest oczywiste, wobec reguła ta prowadzi do
⊢ {n < 0} while n 6= 0 do n ← n − 1 od {n < 0 & n = 0}
i potrzebna poprawność wynika z reguły (R⇒), ponieważ
⊢ n < 0 & n = 0 ⇒ false
Jednak jak mielibyśmy dowodzić tego samego sądu, gdyby język teorii T zawierał symbole
relacyjne false, true oraz = (ze zwykłymi interpretacjami), ale nie zawierał relacji porównywania? Wtedy sam problem nadal dawałby się postawić; ale dowód już by nie przeszedł, bo
nie byłoby jak wyrazić niezmiennika.
52
Fakt, że nie możemy udowodnić poprawności, bo teoria danych jest zbyt uboga, nie świadczy wcale o sile reguł wnioskowania o programach, w dużym stopniu niezależnych od danych.
Dlatego pełność rozszerzenia bada się tylko nad odpowiednio bogatymi teoriami danych:
Definicja 1.76
Załóżmy, że dana jest teoria aksjomatyczna T i jej model M = hM, Ii. Mówimy, że teoria T jest wyrażalna, jeśli dla każdej formuły ϕ tej teorii oraz dla każdej komendy s języka
programowania istnieje w teorii taka formuła ψ, że dla każdego wartościowania σ ∈ Val,
I [[ϕ]] (I [[s]] σ) = true ⇐⇒ I [[ψ]] σ = true
Twierdzenie 1.77 Arytmetyka Peano jest wyrażalna.
Definicja 1.78
Rozszerzenie H teorii o programy nazwiemy pełnym w sensie Cooka, jeśli dla każdego
modelu M i każdej zgodnej z nim wyrażalnej teorii T , z tego, że
∀σ,σ′ (I [[P ]] σ = true & I [[s]] σ = σ ′ ⇒ I [[Q]] σ ′ = true)
wynika ⊢ {P } s {Q} w H(T , M).
Twierdzenie 1.79 System Hoare’a z rys. 1.11 jest rozszerzeniem pełnym w sensie Cooka.
1.6.4
Znaczenie metody niezmienników
Logika Hoare’a swoim pojawieniem się rozbudziła od razu wielkie nadzieje, czasem, jak
się obecnie wydaje, całkiem naiwne.
Początkowo liczono na to, że pozwoli ona na opracowanie automatycznie działających superprogramów, które będą w stanie ocenić, czy dany program poprawnie wypełnia postawione
przed nim zadanie. Byłby to wielki krok w stronę automatycznego programowania, to znaczy
zwolnienia człowieka z obowiązku żmudnego konstruowania programu.
Jednak na przeszkodzie stanęły problemy z rozstrzygalnością i obliczalnością. Otóż automatyczne generowanie niezmienników do wszystkich pętli jest niewykonalne.
Wobec tego proponowano, żeby programista dostarczał programu razem z niezmiennikami
wszystkich pętli, a superprogram sprawdzałby tylko, czy to istotnie są niezmienniki. Niestety
i tego nie da się zrobić automatycznie. Można automatycznie wyprodukować formuły orzekające coś na temat struktur danych, którymi manipuluje program; ale udowodnienie, że
takie formuły są twierdzeniami, jest niewykonalne automatycznie dla każdej teorii choćby tak
złożonej jak arytmetyka liczb naturalnych.
Tak więc nadzieje na to, że logika Hoare’a mogłaby choć częściowo zautomatyzować proces
tworzenia programu, nie sprawdziły się i ten kierunek badań należy uznać za wyczerpany.
Jednak wpłynęła ona znacznie na kształt używanych przez nas języków programowania.
Otóż w początkowym okresie zachwytu nad tym formalizmem próbowano rozszerzyć go
na wiele różnych konstrukcji występujących w ówczesnych językach programowania a nieopisanych regułami wnioskowania systemu Hoare’a. I wtedy okazało się, że rozszerzenie systemu
na niektóre z nich jest trudne; a przy staranniejszym przyjrzeniu się one same wydają się
niejasne.
53
Najważniejszą ofiarą tego przeglądu stała się komenda skoku: goto . . . . Sam Hoare zaproponował sposób dowodzenia poprawności częściowej programów ze skokami, ale ich stosowanie
było dość skomplikowane a poprawność samych reguł niejasna. Powoli zwyciężył pogląd, że
wynika to nie tyle z wadliwości reguł, co z trudności wiążących się z rozumieniem, co właściwie robi program ze skokami. Obecnie większość języków programowania wyklucza, albo
przynajmniej silnie ogranicza, stosowanie skoków.
Niezależnie od możliwości automatyzacji programowania niezmienniki mają znaczenie dydaktyczne, programiści powinni uczyć się je pisać. Konstruując pętlę, programista i tak ma
w głowie jakiś niezmiennik, tylko nie zawsze jest świadom, że „myśli prozą”. . . Jawne wypisanie takiej formuły i próba wykazania, że jest ona niezmiennikiem (niekoniecznie bardzo
rygorystycznie przeprowadzona) może pomóc w dostrojeniu elementów pętli, wyeliminować
codzienne wahanie „czy włącznie czy wyłącznie” oraz wiele innych rodzajów pomyłek w programowaniu. Dlatego warto patrzeć na programy poprzez teorię ich poprawności.
1.7
Tajniki rekursji
Większość języków programowania dopuszcza rekursywne definicje funkcji; czyli takie, w
których definiens, czyli ciało definicji, jawnie zawiera definiendum, czyli definiowane pojęcie.
Na przykład typowa definicja silni w języku C wygląda następująco:
int silnia(int n) {
if (n == 0) return 1;
else return n * silnia(n-1);
}
(1.2)
Czy takie definicje mają sens?
Powinniśmy postarać się odpowiedzieć na to pytanie zarówno z punktu widzenia techniki obliczeniowej, jak i z punktu widzenia logiki. W dodatku pożądane jest, żeby te dwa
punkty widzenia zgadzały się ze sobą, ponieważ rozbieżności między „myśleniem ludzkim” a
„myśleniem maszynowym” są potencjalnym źródłem błędów.
Rekursja jako konstrukcja programistyczna pojawiła się po raz pierwszy w języku Algol 60. Ale o jej logicznej dopuszczalności dyskutowano już znacznie wcześniej. Można o niej
przeczytać w większości podręczników logiki formalnej, np. w Marciszewski (red.) [15].
Teoria rekursji jako najmniejszego punktu stałego pochodzi od Dana Scotta i Christophera
Stracheya. Można o niej przeczytać w wielu miejscach; m.in. w Nielson i Nielson [16].
1.7.1
Dopuszczalność definicji kołowych
Najprostsza odpowiedź na pytanie o dopuszczalność definicji kołowych, czyli takich, w
których definiens zawiera definiendum, jest zdecydowanie negatywna. Definicja powinna być
tylko wprowadzeniem skrótu dla pojęcia, które wcześniej potrafimy wyrazić bez tej definicji.
Tak więc definicja powinna mieć postać
def
definiendum =
definiens
(1.3)
gdzie definiendum jest pojedynczą dotąd nieużywaną nazwą bez żadnych parametrów, czy
innych „komplikacji”; a definiens jest wyrażeniem zawierającym wyłącznie wcześniej już
54
wprowadzone pojęcia. Te ograniczenia wprowadzone są po to, żeby było całkiem jasne, co
definiujemy; oraz żeby uniknąć błędnych kół w definicjach.
Tak więc należałoby odrzucić jako bezsensowną definicję taką jak:
def
kwadrat(x) =
x·x
(1.4)
ponieważ po jej lewej stronie występuje wyrażenie bardziej skomplikowane niż tylko definiendum. Jednak można potraktować ją jako skrót notacyjny od
def
kwadrat =
λx. x · x
(1.5)
(por. rozdz.1.2.3), która jest już całkiem poprawna. Matematycy często piszą definicje w lekko
nieformalnym stylu (1.4), w istocie mając na myśli (1.5).
Również definicję
def
x =
2x − 3
należałoby uznać za bezsensowną, ponieważ jej lewa strona (czyli x) występuje po prawej
stronie. Można ją jednak napisać w postaci
def
x =
jedyny taki x′ , że x′ = 2x′ − 3
co jest już całkiem poprawne pod warunkiem, że wcześniej udowodnimy, że istnieje dokładnie
jeden x′ , spełniający podane równanie.
Wobec tego bardziej wyrafinowana odpowiedź na pytanie o dopuszczalność definicji kołowych mogłaby je dopuszczać, gdybyśmy potrafili je za każdym razem przetłumaczyć na
„oficjalną” postać (1.3).
1.7.2
Stałopunktowa teoria rekursji
Definicja rekursywna daje się łatwo przetłumaczyć na równanie, po lewej stronie którego
występuje samo definiendum. Rozpatrzmy dla przykładu taką definicję rekursywną:
fun id(x) {
if 0 ¬ x & x < 1 then return x
else return id(x − 1) + 1
}
(1.6)
Można ją zapisać w postaci równania
id = λx. (if 0 ¬ x & x < 1 then x else id(x − 1) + 1 fi)
(1.7)
Jednak nie jest prawdą, jakoby takie równania miały dokładnie jedno rozwiązanie.
Przykład 1.80
Poniżej podane są dwa różne rozwiązania równania (1.7) w zbiorze funkcji z liczb rzeczywistych do liczb rzeczywistych:
id1 : R → R
def
id1 (x) =
x
id2 : R → R
(
def
id2 (x) =
x
x+1
55
jeśli x ­ 0
jeśli x < 0
Natomiast z operacyjnego punktu widzenia naturalna jest tylko ta część definicji, która
dotyczy nieujemnych wartości argumentu x, a dla nich funkcje id1 i id2 pokrywają się. Istotnie,
funkcja, zdefiniowana przez (1.6), dla ujemnych argumentów x wchodzi w nieskończoną pętlę.
Sytuacja naszkicowana w przykładzie 1.80 jest typowa. Definiując jakąś funkcję f przez
rekursję, deklarujemy, że będzie ona brała argumenty z pewnego zbioru A i dawała wyniki
w pewnym zbiorze B. Ale potem okazuje się, że daje ona w ogóle jakieś wyniki tylko dla
mniejszego podzbioru dom f ⊆ A. Oznacza to, że mamy do czynienia nie ze zbiorem funkcji
całkowitych A → B, a raczej ze zbiorem funkcji częściowych A →
˜ B 39 . I okazuje się, że na
zbiorze dom f wszystkie rozwiązania równania pokrywają się. Można więc umówić się, że
definicja rekursywna taka jak (1.6) określa najmniejsze rozwiązanie równania (1.7) — w
sensie, który teraz zostanie naszkicowany.
1.7.2.1
Zbiory łańcuchowo zupełne
Definicja 1.81
Przez zbiór częściowo uporządkowany rozumiemy parę hX, ⊑i, w której
• X jest zbiorem,
• ⊑ ⊆ X × X jest relacją na zbiorze X, dla dowolnych x, y, z, ∈ X spełniającą warunki:
– zwrotność: x ⊑ x,
– słaba antysymetria: x ⊑ y & y ⊑ x ⇒ x = y,
– przechodniość: x ⊑ y & y ⊑ z ⇒ x ⊑ z.
Definicja 1.82
Niech hX, ⊑i będzie zbiorem częściowo uporządkowanym i niech A ⊆ X będzie jego podF
zbiorem. Przez kres górny zbioru A, ozn. A, rozumiemy taki element xA ∈ X, że
• ∀x x ⊑ xA , oraz
• jeśli dla pewnego y ∈ X zachodzi ∀x x ⊑ y, to xA ⊑ y.
Zbiór A może nie mieć kresu górnego; ale jeśli go ma, to dokładnie jeden. Ten kres górny
może, ale nie musi, należeć do A.
Definicja 1.83
Zbiór częściowo uporządkowany hX, ⊑i jest łańcuchowo zupełny, jeśli
• istnieje element najmniejszy w X, czyli taki ⊥ ∈ X, że ∀x∈X ⊥ ⊑ x, oraz
• każdy przeliczalny niemalejący ciąg elementów x0 ⊑ x1 ⊑ x2 ⊑ . . . (czyli łańcuch) poF
siada kres górny ∞
i=0 xi .
39
Oczywiście A → B ⊆ A →
˜ B — każda funkcja całkowita jest specjalnym przypadkiem funkcji częściowej.
Okazuje się, że to jest podzbiór nierozstrzygalny, to znaczy dla danej definicji funkcji f ∈ A →
˜ B nie da się
stwierdzić automatycznie, czy ta funkcja jest całkowita, czyli czy f ∈
A
→
B.
Przez dom oznacza się dziedzinę funkcji częściowej, czyli dom f def
= x ∈ A f (x) jest określone . Oczywiście
f ∈ A → B wtedy i tylko wtedy, gdy dom f = A .
56
Przykład 1.84
Zbiór uporządkowany hN, ¬i (liczby naturalne ze zwykłym porządkiem) nie jest łańcuchowo zupełny, bo np. łańcuch 0 ¬ 1 ¬ 2 ¬ . . . nie ma kresu górnego.
Niech X będzie jakimś zbiorem niepustym i niech hP(X), ⊆i będzie zbiorem jego wszystkich podzbiorów, uporządkowanym przez zwykłe zawieranie zbiorów. Ten porządek częściowy
jest łańcuchowo zupełny: elementem najmniejszym jest zbiór pusty, a kresem górnym łańcuS
cha A0 ⊆ A1 ⊆ A2 ⊆ . . . jest suma zbiorów ∞
i=0 Ai .
Niech hPf (N), ⊆i będzie zbiorem wszystkich podzbiorów skończonych zbioru liczb naturalnych, uporządkowanym przez zawieranie zbiorów. Ten porządek nie jest łańcuchowo zupełny,
bo np. łańcuch ∅ ⊆ {0} ⊆ {0, 1} ⊆ {0, 1, 2} ⊆ . . . nie ma skończonego kresu górnego.
Przykład 1.85
W zbiorze funkcji częściowych X →
˜ Y wprowadźmy t.zw. poziomy porządek częściowy
zdefiniowany następująco:
def
f ¬ g ⇐⇒
∀x∈X jeśli istnieje f (x), to również istnieje g(x) i f (x) = g(x)
Przejście od funkcji częściowej f do funkcji poziomo większej g
polega na dorysowania fragmentu wykresu tam, gdzie dotąd była
ona nieokreślona. Funkcje całkowite są poziomo maksymalne — nie
da się ich powiększyć.
Obok przedstawione są schematycznie dwie funkcje: f ¬ g. Na
rysunku wykresy są lekko przesunięte względem siebie, żeby się nie
zasłaniały, ale naprawdę powinny się częściowo pokrywać. Dziedziną
funkcji f jest odcinek Oa, a dziedziną funkcji g jest odcinek Ob.
Okazuje się, że porządek poziomy na zbiorze funkcji częściowych
jest łańcuchowo zupełny; elementem najmniejszym jest funkcja nieokreślona dla żadnego punktu.
1.7.2.2
g..........
......
.
.
.
.
...
.....
...
.
.
.
. ...
f .... ....
.
.
..
... ...
.... .....
.
.
.. .
.... ...
..... .........
.
.
.
.
.
..
.............................
.........
OR
a
b
Przekształcenia ciągłe i ich punkty stałe
Definicja 1.86
Niech hX, ⊑i będzie zbiorem uporządkowanym łańcuchowo zupełnym. Przekształcenie
F : X → X nazywa się ciągłym jeśli
• F jest monotoniczne, t.zn. z x ⊑ y wynika F (x) ⊑ F (y) dla dowolnych x, y ∈ X 40 , oraz
• F zachowuje kresy, t.zn. dla dowolnego ciągu niemalejącego x0 ⊑ x1 ⊑ x2 ⊑ . . ., zachodzi
F(
∞
G
i=0
xi ) =
∞
G
F (xi )
i=0
Jest tu intencjonalna analogia do pojęć ze zwykłej analizy matematycznej: zamiast ciągów
stosuje się łańcuchy; zamiast granic ciągów — kresy łańcuchów; zamiast zwykłej ciągłości
(w sensie zachowywania granic ciągów) — powyżej zdefiniowane zachowywanie kresów łańcuchów.
40
Monotoniczność gwarantuje, że F przeprowadza łańcuchy na łańcuchy.
57
Twierdzenie 1.87
Niech hX, ⊑i będzie zbiorem uporządkowanym łańcuchowo zupełnym a F : X → X przekształceniem ciągłym.
Wtedy
o
n
def
• zbiór Fix F =
x ∈ X F (x) = x punktów stałych przekształcenia F jest niepusty;
• w zbiorze Fix F istnieje element najmniejszy.
Dowód tego twierdzenia jest ważny, bo ilustruje sposób działania obliczenia rekursywnego.
Dowód twierdzenia 1.87:
Ponieważ ⊥ jest elementem najmniejszym, mamy ⊥ ⊑ F (⊥). Ale F jest monotoniczne,
wobec tego zachodzi również F (⊥) ⊑ F 2 (⊥). Stosując założenie o monotoniczności wielokrotnie, otrzymujemy nieskończony łańcuch
⊥ ⊑ F (⊥) ⊑ F 2 (⊥) ⊑ F 3 (⊥) ⊑ . . .
F
def
∞
i
Oznaczmy jego kres górny przez x0 =
i=0 F (⊥).
Okazuje się, że x0 jest punktem stałym przekształcenia F . Istotnie:
F (x0 )
(z definicji x0 )
F∞
i=0 F
i (⊥))
i=0 F (F
i (⊥))
= F(
=
F∞
=
F∞
i+1 (⊥)
=
F∞
i (⊥)
i=0 F
i=0 F
(z ciągłości F )
(ponieważ kres górny łańcucha
nie zależy od pierwszego elementu)
= x0
Wobec tego x0 ∈ Fix F .
Pozostaje wykazać, że x0 jest najmniejszym punktem stałym F . Niech więc x′0 będzie
innym punktem stałym: F (x′0 ) = x′0 . Wtedy ⊥ ⊑ x′0 , więc z monotoniczności F oraz z faktu,
że x′0 jest jego punktem stałym mamy:
F i (⊥) ⊑ F i (x′o ) = x′0
Ponieważ x′0 jest górnym ograniczeniem wszystkich F i (⊥), więc jest również większe od ich
kresu górnego:
x0 =
∞
G
i=0
F i (⊥) ⊑ x′0
58
Przykład 1.88
Rozpatrzmy znowu równość (1.7). Można ją potraktować jako
równanie stałopunktowe id = F (id), gdzie F jest określone następująco:
F 3 (⊥)
F : (R →
˜ R) → (R →
˜ R)
F 2 (⊥)
F (f ) = λx. (if 0 ¬ x & x < 1 then x
else f (x − 1) + 1 fi)
def
Tak więc funkcja id określona jest jako punkt stały pewnego
przekształcenia ciągłego41 . Przyjmujemy, że chodzi o najmniejszy, czyli najmniej określony, punkt stały.
Rysunek pokazuje kolejne przybliżenia funkcji id:
F (⊥)
OR 1
2
3
F (⊥) = λx. (if 0 ¬ x & x < 1 then x else ⊥(x − 1) + 1 fi)
= λx. (if 0 ¬ x & x < 1 then x else nieokreślone fi)
— czyli identyczność na odcinku [0 . . 1) oraz nieokreślone poza tym przedziałem. Dalej:
F 2 (⊥) = λx. (if 0 ¬ x & x < 1 then x else F (⊥)(x − 1) + 1 fi)
= λx. (if 0 ¬ x & x < 1 then x else
if 0 ¬ x − 1 & x − 1 < 1 then x − 1 else nieokreślone fi + 1 fi)
= λx. (if 0 ¬ x & x < 1 then x else
if 1 ¬ x & x < 2 then x else nieokreślone fi fi)
= λx. (if 0 ¬ x & x < 2 then x else nieokreślone fi)
Podobnie
F 3 (⊥) = λx. (if 0 ¬ x & x < 3 then x else nieokreślone fi)
i ogólnie:
F i (⊥) = λx. (if 0 ¬ x & x < i then x else nieokreślone fi)
Kresem górnym tego ciągu jest funkcja
id =
∞
G
i=0
F i (⊥) = λx. (if x ­ 0 then x else nieokreślone fi)
Żeby ten mechanizm działał, potrzebna jest jeszcze wiedza o tym, że potrzebne nam
przekształcenia na przestrzeni fukcji częściowych są ciągłe.
41
W sprawie ciągłości takich przeskształceń — patrz dyskusja niżej.
59
Teza 1.89
Wszystkie definicje rekursywne postaci
f : (X →
˜ Y ) → (X →
˜ Y)
def
f = F (f )
dające się zaprogramować, prowadzą do przekształceń ciągłych F .
Powyższa teza nie może zostać nazwana twierdzeniem, bo pojęcie „dające się zaprogramować” nie zostało ściśle zdefiniowane. Jednak staje się twierdzeniem i daje się udowodnić po
określeniu języka programowania. Dowody są zwykle żmudne, ale koncepcyjnie proste.
A jakie niedające się zaprogramować definicje nie prowadzą do przekształceń ciąglych?
Przykład 1.90
Rozpatrzmy taką definicję rekursywną:
f1 : N →
˜ N
def
f1 (n) =
if f1 (n) = ⊥ then 0 else ⊥ fi
Czyli: jeśli rekursywne wywołanie funkcji f1 zapętla się, to powinno przyjmować wartość 0,
w przeciwnym razie zapętlać się. W takim przypadku odpowiednie przekształcenie
˜ N) → (N →
˜ N)
F1 : (N →
def
F1 (f ) = λn. if f (n) = ⊥ then 0 else ⊥ fi
nie jest monotoniczne: jeśli zwiększymy dziedzinę argumentu f , to częściej będzie liczona
część else, więc F1 (f ) będzie częściej nieokreślone. Sprawdzenie, czy wywołanie rekursywne
się pętli, jest nieprogamowalne.
Przykład 1.91
Inny przykład:
f2 : N →
˜ N
def
f2 (n) =
if (∀i∈N f2 (i) = n) then 0 else 1 fi
Czyli: jeśli wartości funkcji f2 są równe n dla wszystkich argumentów, to f2 (n) ma być 0, w
przeciwnym razie 1.
F2 : (N →
˜ N) → (N →
˜ N)
def
F2 (f ) =
λn. if (∀i∈N f (i) = n) then 0 else 1 fi
Ta funkcja też jest nieprogramowalna: nie da się sprawdzić wartości funkcji f dla wszystkich
argumentów.
1.8
Co jeszcze?
Jak już zostało powiedziane we wstępie, matematyczne podstawy informatyki to bardzo
bogata i różnorodna dziedzina wiedzy; nie da się omówić jej całej w krótkim rozdziale. Więc
teraz, zamiast podsumowania, spojrzyjmy jeszcze tylko bardzo przelotnie na niektóre pola
badań, które w tym opracowaniu się nie zmieściły. Ale należy pamiętać, że nawet po tym
uzupełnieniu obraz dziedziny nadal nie jest pełny.
60
1.8.1
Klasyfikacja języków i problemów nierozstrzygalnych
W rozdziałach 1.2 i 1.3 była mowa o problemach nierozstrzygalnych, oraz o klasyfikacji języków. Ta dziedzina jest znacznie bogatsza, niż zostało to przedstawione. Okazuje się,
że problemy nierozstrzygalne dadzą się pogrupować ze względu na stopień nierozstrzygalności: problemy bardziej nierozstrzygalne nie są sprowadzalne do mniej nierozstrzygalnych. Ta
hierarchia jest nieskończona.
1.8.2
Klasyfikacja języków i problemów rozstrzygalnych
Problemy rozstrzygalne są sklasyfikowane w sposób naturalny przez ich asymptotyczną
złożoność czasową (por. rozdz. 1.4). Jednak istnieją jeszcze inne hierarchie złożoności; np. złożoność przestrzenna mierząca ilość potrzebnej algorytmowi dodatkowej pamięci.
Teoria złożoności bada m.in. związki między różnymi hierarchiami.
1.8.3
Formalna semantyka
Budowa modeli matematycznych programów komputerowych jest zadaniem bardziej złożonym, niż wynikałoby z uproszczonego opisu z rozdz. 1.2. Po drodze można natknąć się na
wiele rozmaitych problemów i zasadzek. Ale te modele są potrzebne, żeby dobrze rozumieć,
co robi program; żeby móc napisać poprawny translator języka; i żeby opracować metody
weryfikacji poprawności.
Dwie podstawowe szkoły opisu, co robi program, to t.zw.
• semantyka operacyjna, czyli opis, co się w programie dzieje; oraz
• semantyka denotacyjna, czyli opis, jakie funkcje czy relacje są realizowane przez program.
Przedstawiona w rozdz. 1.6 interpretacja I [[. . .]] komend należy do tradycji semantyki denotacyjnej.
Opisy denotacyjne są na ogół prostsze o szczegóły, od których abstrahują. Ale w sposób
denotacyjny trudniej jest opisać zachowanie programów reaktywnych (czyli takich, które z
założenia nie mają się zatrzymywać, jak systemy operacyjne) oraz współbieżnych. Oczywiście
istnieją style budowy modeli, mieszczące się między tymi dwoma biegunami; a w nowszych
prezentacjach oba style w dużym stopniu się zlewają.
1.8.4
Teoria typów
W pierwszym przybliżeniu typ jest zbiorem, do którego należą wartości, z którymi mamy
do czynienia w programie; np. typ liczb całkowitych, typ tablic liczb rzeczywistych, itp. Model
programu buduje się w t.zw. algebrze wielorodzajowej składającej się z typów i operacji między
tymi typami. Do tak podstawowego zestawu typów należy jeszcze dołączyć typy funkcyjne.
Jednak typy są w językach programowania często definiowane rekursywnie; tak najczęściej
definiowane są listy i drzewa. Prowadzi to do równań stałopunktowych podobnych do tych,
omawianych w rozdz. 1.7, ale tym razem nie na funkcjach tylko na zbiorach. Do takich równań
stosuje się nadal twierdzenie 1.87 dające rozwiązanie; nadal z wymaganiem, żeby odpowiednie
przekształcenie było ciągłe.
Problem polega na tym, że pewne rozważania informatyczne prowadzą do nieciągłych
przekształceń na typach — a intuicja podpowiada, że rozwiązania nadal istnieją. Okazuje się,
61
że można staranniej określić, o jakie zbiory nam w informatyce chodzi (np. nie o pełen zbiór
funkcji z liczb rzeczywistych do liczb rzeczywistych, tylko o zbiór funkcji programowalnych) i
wtedy odpowiednie przekształcenia stają się ciągłe w sensie, o który naprawdę chodzi informatykom. Ale w tym podejściu typ nie jest już zwykłym zbiorem w rozumieniu klasycznej
teorii mnogości.
1.8.5
Specyfikacje
Przy próbie wyspecyfikowania, co ma robić zamówiony program, powstają problemy teoretyczne. Na potrzeby specyfikowania programów powstało wiele teorii i technik.
Okazuje się, że niektóre rodzaje specyfikacji dają się automatycznie przekształcić w spełniające je programy; czyli same stanowią programy wymagające trochę nietypowego translatora. Doprowadziło to do kontrowersji, co właściwie powinno być rozumiane pod pojęciem
specyfikacji.
Jeden z poglądów głosi, że specyfikacja jest pewnego rodzaju wzorcem, który przy przerabianiu w gotowy program należy optymalizować i zaopatrzyć w wygodny interfejs; czyli
specyfikacja ma być szkieletem, na którym opiera się gotowy program. Pogląd przeciwny głosi, że specyfikacja ma tylko określać wymagania i testy, którym zostanie poddany gotowy
program; i nie może wyręczać programisty w dokonywaniu decyzji projektowych. Wg tego
poglądu specyfikacja wykonywalna nie jest żadną specyfikacją, tylko prototypem programu.
1.8.6
Zastosowania teorii kategorii
Zastanawiające jest, że tak abstrakcyjna dziedzina jak teoria kategorii znalazła zastosowania pozamatematyczne. To jest dział matematyki równie podstawowy, co teoria mnogości
i stanowiący fundament, na którym teoria mnogości i reszta matematyki mogą zostać zbudowane.
Podstawowym pojęciem teorii kategorii jest morfizm — w zastosowaniach morfizmami
mogą być przekształcenia zbiorów, ale też np. strzałki w grafie skierowanym. Istnieją pewne
sposoby utożsamiania (sklejania) ze sobą pewnych morfizmów; teoria kategorii bada ich jedyność z dokładnością do sklejeń. Język teorii kategorii pozwala uprościć wiele sformułowań
w matematyce; i wprawdzie nie skraca dowodów twierdzeń, ale zwraca uwagę na ich analogiczność w różnych dziedzinach (np. wykazuje podobieństwa między pewnymi dowodami w
algebrze i w topologii).
Okazuje się, że to, co robimy w informatyce, w wielu przypadkach prowadzi do obiektów
początkowych 42 pewnych naturalnych kategorii powstających przy matematycznym modelowaniu programów.
Częścią informatycznej teorii kategorii jest teoria instytucji służąca do badania związków
między specyfikacjami a realizacjami tych specyfikacji.
1.8.7
Sieci Petri’ego
Automaty skończone, omawiane w rozdz. 1.2, nie są dobrym modelem dla obliczeń współbieżnych. Proponowano rozmaite modyfikacje, z których najbardziej znane są sieci Petri’ego.
Są to grafy, w których działania (t.zw. odpalanie tranzycji) mogą się niezależnie toczyć w
42
Termin „obiekt” pochodzi z matematycznej teorii kategorii i nie ma związku z programowaniem obiektowym.
62
różnych miejscach, również jednocześnie; a ich efekty mogą się po sieci propagować i na siebie
nawzajem wpływać.
Opracowano wiele wariantów sieci do różnych rozważań na temat obliczeń współbieżnych.
1.8.8
Topologia algebraiczna
Do badania rekursji od dawna stosuje się aparat pojęciowy topologii ogólnej. Ale w ostatnich czasach zastosowanie znalazła również topologia algebraiczna. Jest to matematyczna nauka o tłumaczeniu problemów topologicznych na (łatwiejsze do rozwiązania) problemy algebraiczne.
Okazało się, że do pewnych problemów współbieżności można stosować bardzo podobne
techniki zamiany na problemy algebraiczne. Ten kierunek badań jest bardzo nowy i trudno na
razie przewidzieć, czy zamrze (jak już wiele różnych teoretycznych podejść do informatyki),
czy też odciśnie trwałe piętno.
Literatura
[1] Aho A. V., Hopcroft J. E., Ullman J. D. [2003], Projektowanie i analiza algorytmów,
Gliwice.
[2] Alagić S., Arbib M. A. [1982], Projektowanie programów poprawnych i dobrze zbudowanych, Warszawa.
[3] Apt K., [1981], Ten years of Hoare’s Logic: A survey — Part I, [w]ACM Transactions
on Programming Languages and Systems (TOPLAS) 3 (1981), s. 431–483.
[4] Barendregt H. P. [1984], The lambda calculus. Its syntax and semantics.
[5] Ben-Ari M. [2005], Logika matematyczna w informatyce, Warszawa.
[6] Cormen T. H., Leiserson C. E., Rivest R. L. [2001], Wprowadzenie do algorytmów, Warszawa.
[7] Dahl O-J. [2001], Verifiable programming, New York.
[8] Dembiński P., Małuszyński J. [1981], Matematyczne metody definiowania języków programowania, Warszawa.
[9] Gries, D. [1984], Konstrukcja translatorów dla maszyn cyfrowych, Warszawa.
[10] Hopcroft J. E., Motwani R., Ullman J. D. [2005], Wprowadzenie do teorii automatów,
języków i obliczeń, Warszawa.
[11] List of NP-complete problems [2009],
http://en.wikipedia.org/wiki/List of NP-complete problems
[12] Lyndon R. C. [1978], O logice matematycznej, Warszawa.
[13] Manna Z. [1974], Mathematical theory of computation.
63
[14] Manna Z., Pnueli A., [1974], Axiomatic approach to total correctness, [w]Acta Informatica 3 (1974), s. 243–264.
[15] Marciszewski W. (red.) [1987], Logika formalna: zarys encyklopedyczny z zastosowaniem
do informatyki i lingwistyki, Warszawa.
[16] Nielson H. R., Nielson F. [1992], Semantics with applications: A formal introduction.
[17] Sokołowski S., [1977], Axioms for total correctness, [w]Acta Informatica 9 (1977), s. 61–
71.
64
Podsumowanie
Program komputerowy może wygenerować nieskończenie wiele rożnych obliczeń zależnie
od dostarczonych mu danych. Stąd bierze się moc komputerów, ale również trudności w rozumieniu działania algorytmów, w badaniu ich własności i w ogóle w „panowaniu nad bogactwem”. Niezbędne okazuje się sięgnięcie do metod i konceptualizacji z najróżniejszych działów
matematyki. W tym rozdziale zostały jedynie zasygnalizowane niektóre z nich: modele obliczeń, języki i gramatyki, badania złożoności algorytmów, logika matematyczna, dowodzenie
poprawności programów czy teoria rekursji.
Matematyczne modele obliczeń są wykorzystywane do teoretycznego badania i testowania prototypów rozwiązań wykorzystywanych następnie w komputerach. Ich ważną rolą jest
wyznaczanie „granic informatyki”, czyli odróżnianie rzeczy wykonalnych od niewykonalnych.
Języki i gramatyki są wykorzystywane przy budowie kompilatorów języków programowania, w analizie semantycznej i syntaktycznej kodu oraz przy rozpoznawaniu wzorców.
Badanie złożoności algorytmów jest wykorzystywane do oceny praktycznej wykonalności
obliczenia danym algorytmem oraz do wyboru lepszych algorytmów.
Dla dobrego rozumienia działania programów oraz badania ich własności konieczna jest
znajomość podstaw logik matematycznych; nie tylko klasycznych, ale również wariantów powstałych w mniejszym lub większym stopniu na potrzeby informatyki.
Weryfikacja poprawności algorytmów jest osobnym działem na pograniczu informatyki
i logiki. Zaczęło się od prostej niezmiennikowej logiki Hoare’a, ale prędko obrosła ona całą
dziedziną wiedzy.
Definicje rekursywne (szczególnie rekursywne definicje zbiorów) od dawna budziły wątpliwości filozoficzne i logiczne. Jednak okazało się, że w programach komputerowych działają i
ułatwiają pracę programiście. Opracowano więc spójną i matematycznie elegancką teorię rekursji, z której wynikają sposoby dowodzenia własności obiektów zdefiniowanych rekursywnie
oraz wnioski odnośnie pożądanych cech języków programowania.
Główne koncepcje, nowe terminy
• Matematyczne modele obliczeń.
• Języki.
• Gramatyki.
• Logika.
• Algorytmy.
• Badanie złożoności algorytmów.
• Badanie niezmienników.
• Rekursja.
65
Aktualne problemy badawcze
Wiele nowych zastosowań matematyki w rozwiązaniach informatycznych poznać można
na podstawie lektury tejże książki. Oto wybrane z nich:
• Wykorzystanie i rozwój modeli matematycznych w systemach wspomagania decyzji.
• Wykorzystanie i rozwój modeli matematycznych w systemach eksploracji danych.
• Wykorzystanie i rozwój modeli matematycznych w inteligentnych systemach zarządzania.
Przyszłość problematyki
Matematyka rozwija się między innymi poprzez rozwój takich dziedzin jak: fizyka, chemia,
informatyka, nauki inżynierskie czy nauki ekonomiczne. To na ich użytek i w odpowiedzi
na ich potrzeby tworzone są nowe twierdzenia i definicje, a następnie dopracowane przez
matematyków są włączane do kanonu nauk matematycznych.
Ze swojej strony matematyka wdzięcznie odpowiada na „powierzane” jej motywacje badawcze. Udana matematyzacja dowolnej dziedziny wiedzy zawsze skutkuje skokiem jej możliwości poznawczych.
Zadania i pytania kontrolne
1. Co to jest automat skończony? Do czego jest wykorzystywany w programach komputerowych?
2. Co to jest gramatyka? Do czego jest wykorzystywana w informatyce?
3. Jak mierzy się prędkość działania algorytmu w odróżnieniu od prędkości wykonującego
go komputera?
4. Opisz elementy tworzące klasyczny system logiczny.
5. Co stanowi różnicę pomiędzy poprawnością częściową a całkowitą programu komputerowego?
6. Jakie jest znaczenie niezmienników dla programu komputerowego?
7. Czym się różni semantyka operacyjna od semantyki denotacyjnej?
8. Co to są sieci Petri’ego?
Odpowiedzi
1. W najprostszej swojej postaci automat skończony służy do definiowania języków, czyli do odróżniania słów spełniających pewne kryteria od słów ich niespełniających. W
programach komputerowych wykorzystywany jest do sprawdzenia czy podstawowe jego
jednostki, takie jak słowa kluczowe, identyfikatory, liczby itp., są napisane poprawnie;
oraz do rozpoznawania wzorców. Lista bardziej zaawansowanych zastosowań automatów
skończonych jest oczywiście znacznie dłuższa.
66
2. Gramatyka jest skończonym zbiorem zasad rozwijania i zmieniania napisów. Występują w niej litery alfabetu oraz symbole pomocnicze. Generowanie słowa zaczyna się
od początkowego symbolu pomocniczego, w jego trakcie stosuje się zasady gramatyki i otrzymuje jakieś słowo napisane samymi literami, już bez symboli pomocniczych.
Obecnie najważniejszym zastosowaniem praktycznym gramatyk jest definiowanie języków programowania. Gramatyki formalne pochodzą z badań nad językami naturalnymi
i tam również znajdują zastosowania.
3. Bada się asymptotyczną złożoność czasową algorytmu, czyli sposób wzrastania czasu
jego działania przy zwiększających się danych. Ten sam algorytm będzie zawsze maił tą
samą złożoność asymptotyczną niezależnie od czasu wykonania operacji jednostkowych
(czyli od prędkości działania komputera).
4. Klasyczny system logiczny łączy ze sobą kilka różnych światów. Jest rzeczywistość matematyczna, zwana modelem, którą system ma opisywać. I jest język formalny, który
ma służyć do tego opisu. Między nimi istnieje funkcja interpretująca wyrażenia języka.
Wnioskowanie przeprowadza się po stronie języka, ale dopuszczalne sposoby wnioskowania muszą być takie, żeby interpretacja wyprowadzonych wniosków była spełniona w
świecie. Temu służą aksjomaty i reguły wnioskowania występujące po stronie języka.
5. Różnicę między poprawnością częściową a całkowitą stanowią obliczenia nieskończone.
Przykładowo, program chodzący w ślepej pętli jest częściowo poprawny względem każdej
pary formuł, ale na ogół nie jest całkowicie poprawny względem tych formuł.
6. Początkowo liczono na to, że logika Hoare’a pozwoli na opracowanie automatycznie
działających superprogramów, które będą w stanie ocenić, czy dany program poprawnie wypełnia postawione przed nim zadanie. Niestety okazało się to niemożliwe. Obecnie
niezmienniki są używane do lepszego panownaia nad działaniem pętli w programach.
Przedstawienie w kodzie programu formuły i wykazanie, że jest ona niezmiennikiem,
może pomóc w dostrajaniu inicjalizacji i warunków końca pętli (stale dręczący programistów problem „włącznie/wyłącznie”) i w ograniczaniu błędów programowania.
7. Semantyka operacyjna zawiera opis tego, co dzieje się w programie. Semantyka denotacyjna prezentuje charakterystykę funkcji i relacji realizowanych przez program.
8. Sieci Petri’ego są to grafy, w których działania (tzw. odpalanie tranzycji) mogą się
niezależnie toczyć w różnych miejscach, również jednocześnie, a ich efekty mogą się
po sieci propagować i na siebie nawzajem wpływać. Sieci Petri’ego mają zastosowanie
przede wszystkim w rozważaniach na temat obliczeń współbieżnych.
67