4.1 Primitive Rekursion. Motivation und Definition

Primitive Rekursion ist ein Versuch, Berechbarkeit von Funktionen f:NkN anhand eines "Baukastenprinzips" zu modellieren. Man stellt gewisse Basisfunktionen als "offensichtlich berechenbar" zur Verfügung und beschreibt Kombinatoren, die aus bereits konstruierten Funktionen neue bauen können. Die primitiv-rekursiven Funktionen sind dann all jene, die mittels der Kombinatoren von den Basisfunktionen ausgehend konstruiert werden können. Die Basisfunktionen sind:

zero:NNx0 succ:NNxx+1

Sind diese Funktionen "offensichtlich berechenbar"? Ich würde sagen, die fundamentale Eigenschaft der natürlichen Zahlen ist, dass jede Zahl einen Nachfolger (successor) hat; daher ist irgendwie klar, dass succ berechen bar ist. Aber seien wir vorsichtig: wenn wir für natürliche Zahlen die Dezimaldarstellung (oder Binärdarstellung, spielt keine Rolle) verwenden, dann ist die Operation xx+1 bereits eine nicht ganz triviale Operation, sie erfordert beispielsweise Schleifen (von rechts nach links durchgehen), if-then-else-Ausdrücke (gibt es ein Carry?) etc. Daher sollten Sie so tun, also würden wir natürliche Zahlen in unärer Schreibweise (auhc Steinzeitnotation genannt) darstellen, also vier =1111, sieben = =1111111. Jezt brauchen wir für succ kein if-then-else und keine Schleifen, denn succ(x)=1x.

Eine weitere Klasse von "offensichtlich" berechenbaren Funktionen sind die sogenannten Projektionen πkn, definiert als πkn:NnNxxk . Irgendwie sollte auch hier klar sein, dass die Vorschrift "gib von den 3 Argumenten, die Du erhältst, das erste zurück" ohne Zweifel "berechenbar" ist. Weil wir bald alles in einem Python-Framework implementieren werden, sei angemerkt, dass ich die Zählung der Indizes bei 0 beginnen lasse, also zum Beispiel π03:(x,y,z)xπ13:(x,y,z)y . Auch die Stelligkeit n lasse ich oft weg und schreibe einfach πk statt πkn.

Kombinatoren. Die primitive Rekursion stellt zwei Kombinatoren zur Verfügung: Komposition (Verknüpfung) und primitive Rekursion.
Definition 4.1.1 (Komposition) Sei f:NkN und g1,,gk:NlN. Dann ist Comp(f,g1,,gk) die Funktion NlNxf(g1(x),,gk(x)) Graphisch können Sie sich Komposition so vorstellen:

Um aber komplexere Operationen implementieren zu können, brauchen wir eine Art von Schleife. Was ist die einfachste Art von Schleife oder Rekursion. Wir dürfen nur eine sehr beschränkte Form der Rekursion verwenden:

Definition 4.1.2 Primitive Rekursion Seien g:NkN und h:Nk+2N. Wir definieren eine neue Funktion f:Nk+1N wie folgt: f:Nk+1N(t,x){g(x) if t=0h(f(t1,x),t1,x) if t1. Für diese Konstruktion schreiben wir kompakt f:=PrimRec(g,h).

Wenn Sie Rekursionshasser sind, dann können Sie sich es als Funktion mit einer for-Schleife vorstellen, in der nur eine lokale Variable erlaubt ist:

def PrimRec(g, h):
	def f(t,*x):
	    temp = g(*x)
	    for i in range(t):
          temp = h(temp, i, *x)
	    return temp
	return f       
Die Forderung, dass man nur eine lokale Variable durch die Schleife führen darf, scheint sehr restriktiv; es ist aber wohl die einfachste Form einer Schleife, die wirklich etwas "schleifenhaftes" tut.

Demo. 4.1.3 Speichern Sie die Datei primrec.py auf Ihrem Rechner. Diese Datei stellt ein Framework für die Implementierung primitiv rekursiver Funktionen zur Verfügung. Insbesondere implementiert sie die folgenden Funktion NkN: zero:x0succ:xx+1 als "übliche" Python-Funktionen. Darüberhinaus implementiert sie die folgenden Kombinatoren, welche Ihnen nach den Regeln der primitiven Rekursion neue Funktionen erstellt:
  • Proj(k): erzeugt die Funktion πk:NNxxk .
  • Comp(f, g0, g1, ...): erzeugt die Funktion xf(g0(x),g1(x),...) Sie als User müssen sicherstellen, dass die Stelligkeit von f mit der Anzahl der als gi übergebenen Funktionen übereinstimmt.
  • PrimRec(g,h): erzeugt die Funktion (t,x){g(x) if t=0,h(f(t1,x),t1,x) if t1.

Wenn wir die primitive Rekursion als "Programmiersprache" betrachten, dann heißt das, dass wir neue Funktionen bauen dürfen, indem wir zero,succ,Proj,Comp,PrimRec verwenden, aber nicht selbst Python-Funktionen schreiben. Wir dürfen also nie selbst Integers in die Hand nehmen.

Lassen Sie mich das am Beispiel der Addition erklären. Ich will eine Funktion add:N2N schreiben, die ihre beiden Argumente addiert. Ich darf also nicht einfach python programmieren und

def add(x,y):                           
  return x + y

schreiben, denn unsere "Programmiersprache" ist Primitive Rekursion, nicht Python! Wir müssen uns add aus den Kombinatoren zusammenbasteln. Ich schreibe nun add(t,x) statt add(x,y), um den Rekursionsparameter t deutlich zu machen.

add(t,x)={x if t=0succ(add(t1,x)) if t1.={π0(x) if t=0succ(π0(add(t1,x),t1,x)) if t1.

Wir sehen also, dass dies eine Anwendung der primitiven Rekursion ist mit g=π0 und h=Comp(succ,π0), also

p0 = Proj(0)                            
add = PrimRec (p0, Comp(succ,p0))
Übungsaufgabe 4.1.1 Zeigen Sie, dass die folgenden Funktionen primitiv-rekursiv sind, und implementieren Sie sie in meinem Python-Framework, so wie ich Addition mit add = PrimRec (p0, Comp(succ,p0)) implementiert habe:
  1. mult:(x,y)xy
  2. exp:(a,b)ab
  3. pred:xmax(0,x1)
  4. minus:(x,y)xy

Tip: Für exp und minus ist es einfacher, die Argumente "umgedreht" zu betrachten, also (a,b)ba und (x,y)yx.

Übungsaufgabe 4.1.2 Wenn Sie die vorherige Übungsaufgabe gelöst (oder darüber aufgegeben) haben, sehen Sie sich die Datei stockpile.py an, in der ich diese Funktionen zum Großteil implementiert habe (basierend auf den Übungen, die wir direkt in der Vorlesung gemacht haben).

Experimentieren Sie weiter und implementieren Sie "Boolesche" Prädikate und Funktionen wie

  • isPositive
  • greaterThan, lessThan, greaterEqual, lessEqual
  • max, min
  • ifThenElse(x,y,z): dies soll z zurückliefern, falls x=0 (also false) ist, und y sonst.