7 Exkurs: Berechenbarkeit 7 · Exkurs: Berechenbarkeit 7.1 Mächtigkeit von Programmiersprachen · 7.1 Mächtigkeit von Programmiersprachen Aus mathematischer Sicht ist ein Programm eine Funktion, die eine Eingabe zusammen mit dem Zustand der Maschine vor Start des Programmes abbildet auf eine Ausgabe und einen neuen Zustand nach Programmende. program :: (State, Input) → (State, Output) Diese Funktion ist nicht unbedingt auf jeder Eingabe definiert (es ist dann insbesondere eine partielle Funktion): I I Ein Programm kann fehlschlagen, z.B. bei Division durch Null. (Nagut, das könnte man noch als Zustand auffassen). Wichtiger: Ein Programm muss nicht terminieren (es kann sich “aufhängen”). Hier wird also kein definierter Zustand erreicht. Offensichtliche Frage Was kann man überhaupt berechnen? Antwort Tatsächlich ist nicht jede Funktion berechenbar! Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 196 7 · Exkurs: Berechenbarkeit Mächtigkeit von Programmiersprachen · 7.1 Turing-Maschine Die Turing-Maschine ist eine abstrakte Rechenmaschine26 . I von Neumann Computer (z.B. unsere PCs) entsprechen Turing-Maschinen mit nur endlichem Hauptspeicher. Definition Berechenbare Funktionen Genau die Funktionen die von einer Turing-Maschine berechnet werden können, heißen berechenbar. I Eine Programmiersprache heißt turingmächtig, wenn man mit ihr jede berechenbare Funktion ausdrücken (implementieren) kann. • Die meisten “Allzweck”-Programmiersprachen sind turingmächtig27 . Beispiele: java, Haskell, C, Assembler, der λ-Kalkül, Brainfuck, ... Beweis: Implementation einer Turing-Maschine in der jeweiligen Sprache. • Praktische Bedeutung: Wir können prinzipiell jede berechenbare Funktion implementieren. 26 Vorlesung Konzepte der Informatik oder 27 unter der Annahme es stünde unendlich Michael Grossniklaus · DBIS Theoretische Informatik viel Hauptspeicher zur Verfügung! Informatik 2 · Sommer 2017 197 7 · Exkurs: Berechenbarkeit Mächtigkeit von Programmiersprachen · 7.1 Anmerkungen I Wir machen bei diesen Betrachtungen keine Aussage über Laufzeit und Speicherbedarf des Programmes. I Auch die Einfachheit der Implementierung ist hier nicht relevant. I Die Definition von “berechenbar” (cf. Seite 197) ist zwar ausreichend, aber etwas unbefriedigend, falls wir kein offensichtlich “unberechenbares” Problem beschreiben können. Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 198 7 · Exkurs: Berechenbarkeit 7.2 Das Halteproblem · 7.2 Das Halteproblem Das Halteproblem ist sowohl praktisch relevant, als auch ein klassisches Beispiel für eine nicht berechenbare Funktion. Angenommen wir haben ein Programm p (z.B. in Form von Quellcode), das eine Eingabe x verarbeitet: I 1 I void p( Input x ) ... —in einer fiktiven, z.B. imperativen Sprache Es wäre schön zu wissen, ob p bei einer bestimmten Eingabe x terminiert, oder “sich aufhängt”. • Diese Frage ist das Halteproblem für p(x). • Ausprobieren ist keine Option. (Warum nicht?) Frage Kann man ein Programm schreiben, welches entscheiden kann ob ein übergebenes Programm (durch dessen Analyse) terminiert, welches also das Halteproblem beantwortet? Anwort Nein, es ist im Allgemeinen nicht berechenbar, ob ein Programm hält. (Im Speziellen, für manche Programme also, ist dies möglich.) Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 199 7 · Exkurs: Berechenbarkeit Das Halteproblem · 7.2 Beweisskizze Beweis durch Widerspruch Angenommen wir hätten ein Programm halts, 1 Bool halts( Sourcecode p, Input x ) { ... } —in einer fiktiven imperativen Sprache welches für jedes beliebige Programm p und jede beliebige Eingabe x berechnen kann, ob das Programm p mit Eingabe x terminiert: halts(p, x) _ True ⇐⇒ p(x) terminiert halts(p, x) _ False ⇐⇒ p(x) terminiert nicht Beispiel Anwendung auf ein terminierendes Programm: 1 void test1( Input x ) { 1 2 3 4 ~ print(x); > test1("hello world") hello world > halts(test1, "hello world") True Anmerkung Michael Grossniklaus · DBIS } 1 2 3 4 > test1(test1) void test1( Input x ) { print(x); } > halts(test1, test1) True Wir identifizieren hier Programme mit ihrem Quellcode. Informatik 2 · Sommer 2017 200 7 · Exkurs: Berechenbarkeit Das Halteproblem · 7.2 Beispiel Anwendung auf ein nicht immer terminierendes Programm: 1 Int test2( Input x ) { 2 2 while( x > 0 ) { x := 4; } 3 4 5 6 3 4 5 6 return 42; 7 8 1 } Erinnerung Michael Grossniklaus · DBIS 7 > halts(test2, -23) True > test2(-23) 42 > halts(test2, 23) False > test2(23) 8 ... terminiert nicht Das Programm halts terminiert auf jeden Fall. Informatik 2 · Sommer 2017 201 7 · Exkurs: Berechenbarkeit Das Halteproblem · 7.2 Wir konstruieren einen Widerspruch Erinnerung I 1 halts(p, x) berechnet, ob p(x) terminiert. Konstruieren jetzt ein Programm evil wie folgt: Int evil( Sourcecode p ) { • halts wendet das übergebene 2 if (halts(p, p)) { while (True) {} —Endlosschleife } 3 4 5 6 • evil kann in einer Endlosschleife hängen bleiben. return 1; 7 8 Programm p auf p selbst an. Das hatten wir schon, cf. Seite 200. } Frage ⇒ ⇒ Terminiert evil für ein gegebenes Programm p? p(p) terminiert halts(p, p) _ True evil(p) terminiert nicht Michael Grossniklaus · DBIS ⇒ ⇒ p(p) terminiert nicht halts(p, p) _ False evil(p) terminiert Informatik 2 · Sommer 2017 202 7 · Exkurs: Berechenbarkeit Erinnerung ⇔ Das Halteproblem · 7.2 Bis jetzt haben wir ein Programm evil konstruiert: evil(p) terminiert 1 Int evil( Sourcecode p ) { ... } p(p) terminiert nicht I Jetzt wenden wir evil auf sich selbst an. I Terminiert evil(evil)? Aus der Konstruktion folgt: evil(evil) terminiert ⇔ evil(evil) terminiert nicht Ein Widerspruch. Also muss unsere Annahme (die Existenz von halts) falsch sein. Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 203 7 · Exkurs: Berechenbarkeit Das Halteproblem · 7.2 Anmerkungen I Es sieht hier so aus als wäre dieser Beweis unabhängig vom Rechenmodell. Ein formaler Beweis bezieht sich jedoch z.B. auf die Turing-Maschine, und verwendet keine “fiktive imperative Programmiersprache”. I Es ist tatsächlich kein mächtigeres Konzept für eine Rechenmaschine bekannt. I Turing-Maschine und der einfache untypisierte λ-Kalkül (den wir bisher besprochen haben) sind gleich mächtig. • Auch im λ-Kalkül können wir nicht alles berechnen, ... • ...aber kein formales System kann mehr berechnen. • Beweisidee: Im λ-Kalkül eine Turing-Maschine beschreiben, und umgekehrt. Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 204 7 · Exkurs: Berechenbarkeit Konsequenzen · 7.3 Konsequenzen 7.3 Für die Theorie... I Es gibt tatsächlich Funktionen die prinzipiell nicht berechenbar sind (Man sagt auch: unentscheidbare Probleme). ...aber auch für die Praxis: I Wir können kein Programm bauen, das im Allgemeinen entscheiden kann, ob sich ein gegebenes Programm “aufhängt”. I Dieses Problem erstreckt sich bereits auf Teile von Programmen: 1 2 3 4 5 Anweisung 1; Anweisung 2; ... Anweisung n; print("hello"); Michael Grossniklaus · DBIS • Die Anweisungen 1–n formen bereits ein Teilprogramm. • Im Allgemeinen ist also nicht entscheidbar, ob die Anweisung in Zeile 5 jemals ausgeführt wird. Informatik 2 · Sommer 2017 205 7 · Exkurs: Berechenbarkeit Konsequenzen · 7.3 Bedeutung für Compiler I Ein Compiler hat auch die Aufgabe in unseren Programmen Fehler zu finden, damit sie nicht erst im Betrieb auffallen. • Dazu gehören “offensichtliche” Fehler wie z.B. falsche Syntax, • weniger offensichtliche, wie nicht deklarierte Variablen oder Typfehler, • und Fehler wie das “Abstürzen” (unerwarteter Zustand) oder “Aufhängen” (nicht-Erreichen eines definierten Zustandes). I Dazu bieten sich zwei Strategien an: 1. Alles zurückweisen von dem der Compiler nicht beweisen kann, dass es korrekt ist. I I Das ist eine sehr starke Einschränkung. Solche Sprachen lassen viele legitime Programme nicht mehr zu. Der Simply Typed λ-Calculus28 ist ein Beispiel dafür. Dieser ist nicht turingmächtig, dafür terminieren alle damit formulierten Programme. 2. Nur verbieten was der Compiler als definitiv falsch erkennt. I 28 Eine Hier wurde die Grenze der erkennbaren Fehler immer weiter verschoben. Typsysteme sind eine Methode dazu. Variante des λ-Kalküls, auf die wir nicht weiter eingehen werden. Michael Grossniklaus · DBIS Informatik 2 · Sommer 2017 206