4.2 Primitive Rekursion: Konstruktionen und Tricks
4.2 Primitive Rekursion: Konstruktionen und Tricks
Ich beginne dieses Teilkapitel, manche der primitiv-rekursive Implementierungen in stockpile.py zu erklären und werde dann weitere, weniger offensichtliche Konstruktionen diskutieren.
Wir haben bereits gesehen, dass wir die Addition per
${\rm add} = \primrec (\pi_0, \comp(\succ, \pi_0))$
schreiben können. Beachten Sie, dass ich hier die
Indizierung immer bei 0 beginnen lasse, im Python-Code
sowie im Mathematik-Modus (in der Vorlesung hatte ich an
der Tafel das oft bei 1 beginnen lassen, entschuldigen Sie
die Inkonsistenz). Multiplikation geht nach dem gleichen
Schema. Wir schreiben
mult
in der Schleifenform, wie sie
die primitive Rekursion zulässt, und schreiben in Orange
auch gleich die Funktionen $g$ und $h$ dazu:
def mult(t,x):
temp = 0 = zero(x)
for i in range(t):
temp = add(temp,x) = Comp(add, p_0, p2) (temp,i,x)
return temp
erinnern Sie sich daran: $h$ muss immer eine Funktion in der lokalen Variable temp, der Laufvariable $i$ und $\vec{x}$ sein. Daher:
Genauso erhalten wir $x^t$, indem wir statt bei 0 bei 1 beginnen und statt $x$ draufzuzählen, es draufmultiplizieren:
Allerdings gibt uns das $(t,x) \mapsto x^t$. Für ${\rm exp}(a,b) = a^b$ müssen wir die Argumente umdrehen:
Die Fakultätsfunktion $t!$ ist konzeptuell ähnlich, allerdings etwas interessanter, weil wir hier auf das Zwischenergebnis und die Laufvariable zugreifen müssen:
def factorial(t):
temp = 1 = one()
for i in range(t):
temp = mult(temp,i+1) = Comp(mult, p_0, Comp(succ, p_1)) (temp,i)
return temp
Wir können nicht einfach $i+1$ schreiben! Wir müssen dies via $\comp(\succ,\pi_1)$ konstruieren. Denn: die Funktion $h$ bekommt ${\rm temp}, i, \vec{x}$ als Input; $\pi_1$ gibt uns davon das $i$ zurück und $\comp(\succ, \pi_1)$ gibt uns $i+1$. Operationen wie $\pred: t \mapsto t-1$ und ${\rm minus}$ sind konzeptuell nur ein bisschen anders. Das liegt auch daran, dass wir keine negativen Zahlen haben. Was ist $\pred(0)$? Wir definieren das als 0. So können wir pred implementieren:
def pred(t):
temp = 0 = zero()
for i in range(t):
temp = i = p1 (temp,i)
return temp
In Worten: wenn $t=0$, dann geben wir einfach $\zero()$ zurück, also $0$. Ansonsten durchlaufen wir die Schleife $t$ mal; der letzte Schleifenindex ist $t-1$, und das ist dann auch der Wert von temp, der zurückgegeben wird. Daher:
Für minus schreiben wir uns erst mal ${\rm subtractFrom}: (t,x) \mapsto x-t$. Hierfür wenden wir einfach pred $t$ mal auf $x$ an:
Die primitive Rekursion stellt uns als "Datentyp" nur die
natürlichen Zahlen zur Verfügung. Alles andere müssen wir
als natürliche Zahlen nach einem von uns selbst gewählten
Schema codieren. Für Boolesche Werte ist das recht
naheliegend. Wir codieren
True
als 1 und
False
als 0.
Unser erstes Prädikat, also Funktion mit Booleschem
Ausgabewert, ist
isPositive:
def pred(t):
temp = 0 = zero()
for i in range(t):
temp = 1 = one
return temp
und somit
Auf ähnliche Weise können wir uns nun lessThan, lessEqual, and, or, not, ifThenElse konstruieren.
Eine größere Herausforderung stellt die ganzzahlige Wurzel $x \mapsto \floor{\sqrt{x}}$ dar. Hierfür haben wir keine Formel mit $+, *, -$, auch keine schöne rekursive Formel zur Hand. Es handelt sich um ein Suchproblem: $\floor{\sqrt{x}}$ ist die größte natürliche Zahl $i$ mit $i^2 \leq x$. Wir können auch den Suchraum begrenzen: da $\sqrt{x} \leq x$ für alle natürlichen Zahlen, so gilt: $\floor{\sqrt{x}}$ ist die größte natürliche Zahl $i \in \{0,1,\dots,x\}$ mit $i^2 \leq x$. Das Prädikat
ist primitiv rekursiv:
und damit können wir die ganzzahlige Wurzel schreiben als
def largestIbelowTwithISquareLessEqualX(t,x):
temp = 0 = zero(x)
for i in range(t):
temp = ifThenElse(iSquareLessEqualX(i,x), i, temp) = Comp(ifThenElse, Comp(iSquareLessEqual, p_1, p_2), p_1, p_0) (temp,i,x)
return temp
Was geht hier vor? Wenn ${\rm iSquareLessEqualX}(i,x))$ True ist, dann ersetzen wir temp durch $i$. Ansonsten belassen wir es beim alten Wert; der endgültige Wert von temp ist also das letzte $i$, für das das Prädikat True wahr.
Und schließlich ist
da wir zum Suchen der Wurzel von $t$ gleich $t$ als Obergrenze des Suchraumes angeben können. Ganz allgemein können wir für ein Prädikat ${\rm predicate}(i,x)$ das größte $i \in \{0,\dots,t\}$ finden, für das ${\rm predicate}(i,x)$ True ergibt. Da wir wissen, wie wir das primitiv rekursiv machen können, erlauben wir uns, einen neuen Kombinator zu definieren, anstatt diese Konstruktion jedes Mal "von Hand" durchzuführen:
def LargestLessThan(upperBound, predicate):
def new_function(*x):
temp = 0
for i in range(upperBound(*x)):
if (predicate(i, *x)):
temp = i
return temp
return new_function
und somit können wir sqrt schreiben als
Eine recht stark anmutende Beschränkung primitiv
rekursiver Funktionen ist die Tatsache, dass wir in der
Schleife nur
eine
lokale Variable führen dürfen, hier
meistens
temp
genannt. Manche Funktionen scheinen
inhärent mindestens zwei zu benötigen. Betrachten wir den
Fall der Fibonacci-Zahlen:
def fibIterative(t):
a = 0
b = 1
for i in range(t):
c = a+b
a = b
b = c
return a
Wir führen hier zwei lokale Variable, $a$ und $b$. Das $c$ könnten wir mit diesem schönen Trick eliminieren:
b = a+b a = b-a
Doch selbst dann hätten wir immer noch zwei lokale
Variable. Die Fibonacci-Zahlen rekursiv per
return
F(n-1)+F(n-2)
scheint noch weiter weg zu sein vom
Paradigma der primitiven Rekursion; primitive Rekursion
verzweigt sich nie. Dennoch ist es möglich,
fib
primitiv
rekursiv zu implementieren. Hauptzutat hierbei ist es, dass
wir
Paare
als neue Datenstruktur verwenden. Erinnern Sie
sich: die primitive Rekursion stellt uns als Datentyp von
Haus aus nur die natürlichen Zahlen zur Verfügung. Alles
andere müssen wir nach einem selbst gewählten Schema
codieren. Bei Booleschen Werten war es einfach. Wie steht
es mit
Paaren
von natürlichen Zahlen? In
Kapitel 2:
Unendliche Mengen
haben wir die bijektive
Funktion
kennengelernt. Diese Funktion ist primitiv rekursiv: es gilt ${n \choose 2} = 1 + 2 + \cdots + (n-1)$ und somit
def choose2(t):
temp = 0 = zero()
for i in range(t):
temp = add(temp,i) = Comp(add, p0, p1)
und somit
Um die Umkehrfunktionen $\first, \second$ zu implementieren, die uns aus $\pair(x,y)$ wieder $x$ und $y$ berechnen, bestimmen wir erst den Wert von $x+y$. Wenn $\pair(x,y) = n$ gilt und $x+y=i)$ ist, dann ist
und somit ist $x+y$ der größte Wert von $i$, so dass $n \geq \pair(0,i)$ ist. Aus $x+y$ und ${x+y+1 \choose 2} + x$ können wir dann leicht $x$ und $y$ berechnen:
Listen können wir implementieren, indem wir Paare verschachtelt zusammenhängen, z.B. $[5,7,9]$ wird zu $\pair(5, \pair(7, 9))$. Das ist natürlich inkorrekt:
>>> pair(5,pair(7,9)) 11031 >>> pair(7,9) 143 >>> pair(5,143) 11031
Das Problem ist, dass wir das Ende der Liste nicht kennen. Wir lösen das Problem, indem wir die leere Liste mit $0$ codieren und dann aber beim Davorhängen eines Elements 1 draufaddieren müssen, also
Jetzt klappt alles wunderbar:
>>> push(11, push(7, push(5, 0))) 90537 >>> head(90537) 11 >>> head(tail(90537)) 7 >>> head(tail(tail(90537))) 5
Native Implementierung.
Die primitive Rekursion ist ein
theoretischer Berechenbarkeitsbegriff für Funktionen auf
natürlichen Zahlen. Es ist definitiv keine ernstzunehmende
Programmiersprache. Ich finde allerdings, dass es hilfreich
ist, sie als Programmiersprache zu begreifen und zu
verwenden, rein experimentell. Leider ist sie extrem
ineffizient: selbst Subtraktion hat quadratische
Komplexität. Längere Listen wie die gerade werden Sie in
vertretbarer Zeit nicht behandeln können. Bei meinen
eigenen Experimenten mit meinem Python-Framework bin ich
daher dazu übergegangen, dass ich, sobald ich gezeigt habe,
dass eine Funktion $f$ primitiv rekursiv ist, sie
nativ
in Python zu implementieren, also beispielsweise in meiner
Datei
stockpile.py:
def pair(x,y):
return int(((x+y+1) * (x+y)) / 2 + x )