Scheinbar unmögliche Programme
{-# LANGUAGE FlexibleInstances #-}
module Main where
import Data.Maybe
Mit [Bool]
meinen wir den Typ der unendlichen 0/1-Folgen.
p3 :: [Bool] -> Bool
p1, p2,= xs!!3
p1 xs = xs!!4
p2 xs = xs!!(2+1) && (xs!!8 || not (xs!!8)) p3 xs
= p1 == p2
test1 = p1 == p3 test2
Das liefert einen Fehler über eine fehlende Eq
-Instanz, oder?
Weit gefehlt! Beide Programme terminieren. test1
hat den Wert False
,
test2
hat den Wert True
.
Das ist umso erstaunlicher, wenn man bedenkt, dass [Bool]
überabzahlbar
unendlich groß ist.
Hintergrund: Abzählbarkeit und Überabzählbarkeit
Eine Menge heißt genau dann abzählbar, wenn es eine unendliche Liste gibt, in der alle Elemente der Menge vorkommen.
Prototypbeispiel: Die Menge N der natürlichen Zahlen ist abzählbar.
0, 1, 2, 3, 4, ...
Die Menge Z der ganzen Zahlen ist ebenfalls abzählbar, insbesondere also genau so groß wie die Menge der natürlichen Zahlen:
0, 1, -1, 2, -2, 3, -3, 4, -4, 5, -5, ...
Übungsaufgabe: Zeige, dass auch die Menge Q der rationalen Zahlen abzählbar ist.
Eine Menge heißt genau dann überabzählbar, wenn sie nicht abzählbar ist.
Beispiel (Cantor): Die Menge R der reellen Zahlen ist überabzählbar.
Oder: Die Menge [Bool] aller unendlichen 0/1-Folgen ist überabzählbar.
Beweis durch das Cantorsche Diagonalargument:
Keine Liste von 0/1-Folgen kann alle 0/1-Folgen enthalten. Denn wenn zum Beispiel folgende Liste gegeben ist:
01000101011011011... 11101011111101111... 10101010101010101... 11100011100010010... 10010101100100000... 11010101100100000... . . .
Wenn man nun die Diagonale (von oben links nach unten rechts) durchgeht, und jeweils die Gegenziffer wählt, erhält man eine 0/1-Folge, die sicher nicht in der Liste vorkommt. Im Beispiel wäre das
100110...
Diese 0/1-Folge kann nicht das erste Element der Liste sein, denn sie unterscheidet sich vom ersten Element ja an der vordersten Stelle. Sie kann auch nicht das zweite Element der Liste sein, denn sie unterscheidet sich vom zweiten Element ja an der zweitvordersten Stelle. Und so weiter!
Epsilon
Sei p :: [Bool] -> Bool
ein beliebiges Prädikat.
Falls p
erfüllbar ist, d.h. falls es eine 0/1-Folge xs
gibt, sodass p xs
,
dann soll epsilon p
irgendeine 0/1-Folge sein, sodass p (epsilon p)
.
Falls p
nicht erfüllbar ist, dann kann epsilon p
sein, was es möchte.
epsilon :: ([Bool] -> Bool) -> [Bool]
= if p (False : xs)
epsilon p then False : xs
else True : epsilon (p . (True:))
where xs = epsilon (p . (False:))
Etwas schneller geht es dank Laziness so:
epsilon :: ([Bool] -> Bool) -> [Bool]
epsilon p = h : epsilon (p . (h:))
where
h = not $ p (False : epsilon (p . (False:)))
Wer es noch schneller möchte – sodass auch Code wie epsilon $ \xs -> xs !! (10^10)
funktioniert, kann sich den Code von Martín Escardó ansehen.
exists
Sei p :: [Bool] -> Bool
ein beliebiges Prädikat.
Falls p
erfüllbar ist, dann soll exists p
ein Zeuge dieser Erfüllbarkeit
sein (in einem Maybe verpackt), also eine 0/1-Folge xs
, sodass p xs
.
Falls p
nicht erfüllbar ist, soll exists p
Nothing
sein.
exists :: ([Bool] -> Bool) -> Maybe [Bool]
= if p xs then Just xs else Nothing
exists p where xs = epsilon p
forall
Sei p :: [Bool] -> Bool
ein beliebiges Prädikat.
forall p
soll dann und nur dann True sein, falls `p auf jeder 0/1-Folge konstant
True` ist.
forall :: ([Bool] -> Bool) -> Bool
forall p = isNothing $ exists (not . p)
Punktfrei geht es auch:
forall = not . isJust . exists . (not .)
Eq-Instanz für Funktionen [Bool] -> Bool
instance Eq ([Bool] -> Bool) where
== g = forall $ \xs -> f xs == g xs
f -- "zwei Prädikate sind genau dann gleich, wenn sie auf jedem Argument
-- gleich sind"
Exkurs: Beispiel für eine unstetige Funktion
Eine Funktion von R nach R ist genau dann stetig, wenn man ihren Graph zeichnen kann, ohne den Stift abzusetzen. Eine stetige Funktion darf also keine Sprungstellen besitzen.
f : R --> R x |-> if x < 0 then -1 else if x == 0 then 0 else 1
Die Signumfunktion!
Sie ist aber nur definiert auf der Teilmenge
{ x in R | x < 0 oder x = 0 oder x > 0 }.
Konstruktiv kann man nicht zeigen, dass diese Teilmenge ganz R ist. Die Funktion ist also nicht als total nachweisbar.
Wieso funktionieren die “scheinbar unmöglichen Programme”?
In Haskell kann man nur stetige Funktionen implementieren. (Wie in manchen Schulen konstruktiver Mathematik auch.) Jede Funktion vom Typ
[Bool] -> Bool
ist stetig.In der Topologie gibt es folgenden Satz: Jede stetige Funktion, deren Definitionsmenge kompakt ist, ist schon gleichmäßig stetig.
Die Menge
[Bool]
ist kompakt. (Eigentlich sollte man “Raum” statt “Menge” sagen. Für Topologie-Fans: Das folgt sofort aus dem Satz von Tychonoff.)Im Spezialfall von Funktionen
[Bool] -> Bool
bedeutet gleichmäßig stetig: Eine solche Funktion ist genau dann gleichmäßig stetig, wenn es eine Schrankem
gibt, sodass die Funktion zur Berechnung ihres Ergebnisses nur die erstenm
Bits der Folge benötigt (unabhängig von der speziellen Eingabefolge).epsilon
ruft sich selbst rekursiv auf. Wennp
als Schrankem
hat, dann hat das im rekursiven Aufruf verwendete Prädikat(p . (False:))
als Schrankem-1
. Also terminiert nachm
rekursiven Aufrufen das Verfahren.
Und die Moral von der Geschicht
In der Frage, ob Gleichheit von Funktionen vom Typ A -> Bool
entscheidbar
ist, ist es nicht relevant, ob A
endlich ist oder nicht. Tatsächlich
entscheidend ist, ob A
kompakt ist oder nicht.
- Endliche Typen sind kompakt.
[Bool]
ist kompakt.- Sind
A
undB
kompakt, so auch der Produkttyp(A,B)
. Nat
undInteger
sind nicht kompakt.- Der Datentyp der “lazy naturals”, welche auch
+infty
enthalten, ist kompakt.
Übungsaufgaben:
Übertrage die Funktion
epsilon
– und damit auchexists
undforall
, auf den Fall von FunktionenNAT -> Bool
(statt[Bool] -> Bool
). Dabei sollNAT
der Typ der “lazy naturals” sein, zum Beispiel definiert durch:data NAT = Zero | Succ NAT
infty = Succ infty
Definiere eine Typklasse
Compact
und implementiere Instanzen für[Bool]
undNAT
sowie eine generische Instanz(Compact a, Compact b) => Compact (a,b)
.Schreibe eine Funktion, die den größten Funktionswert einer gegebenen Funktion
[Bool] -> Nat
bestimmt.
Diesen Text gibt es auch als kompilierbares Literate Haskell!