4.2 Primitive Rekursion: Konstruktionen, Tricks

Ich beginne dieses Teilkapitel, manche der primitiv-rekursive Implementierungen in stockpile.py zu erklären und werde dann weitere, weniger offensichtliche Konstruktionen diskutieren.

Arithmetische Operationen

Wir haben bereits gesehen, dass wir die Addition per add=PrimRec(π0,Comp(succ,π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 x sein. Daher: mult=PrimRec(zero,Comp(add,π0,π2)) Genauso erhalten wir xt, indem wir statt bei 0 bei 1 beginnen und statt x draufzuzählen, es draufmultiplizieren: expReverse=PrimRec(one,Comp(mult,π0,π2)) Allerdings gibt uns das (t,x)xt. Für exp(a,b)=ab müssen wir die Argumente umdrehen: exp=Comp(expReverse,π1,π0) 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,π1) konstruieren. Denn: die Funktion h bekommt temp,i,x als Input; π1 gibt uns davon das i zurück und Comp(succ,π1) gibt uns i+1.

Operationen wie pred:tt1 und 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 t1, und das ist dann auch der Wert von temp, der zurückgegeben wird. Daher: pred=PrimRec(zero,π1)

Für minus schreiben wir uns erst mal subtractFrom:(t,x)xt. Hierfür wenden wir einfach pred t mal auf x an: subtractFrom=PrimRec(π0,Comp(pred,π0))minus=Comp(subtractFrom,π1,π0)

Boolesche Werte

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 isPositive=PrimRec(zero,one)

Auf ähnliche Weise können wir uns nun lessThan, lessEqual, and, or, not, ifThenElse konstruieren.

Wurzel und Suchen im Allgemeinen

Eine größere Herausforderung stellt die ganzzahlige Wurzel xx dar. Hierfür haben wir keine Formel mit +,,, auch keine schöne rekursive Formel zur Hand. Es handelt sich um ein Suchproblem:
x ist die größte natürliche Zahl i mit i2x.

Wir können auch den Suchraum begrenzen: da xx für alle natürlichen Zahlen, so gilt:

x ist die größte natürliche Zahl i{0,1,,x} mit i2x.

Das Prädikat iSquareLessEqualX(i,x)=[i2x] ist primitiv rekursiv: iSquareLessEqualX=Comp(lessEqual,Comp(mult,π0,π0),π1) 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 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.

largestIbelowTwithISquareLessEqualX=PrimRec(zero,Comp(ifThenElse,Comp(iSquareLessEqual,π1,π2),π1,π0)) Und schließlich ist sqrt=Comp(largestIbelowTwithISquareLessEqualX,π0,π0) 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 predicate(i,x) das größte i{0,,t} finden, für das 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

sqrt=LargestLessThan(π0,iSquareLessEqual)

Paare und Listen

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

pair:N2N(x,y)(x+y+12)+x kennengelernt. Diese Funktion ist primitiv rekursiv: es gilt (n2)=1+2++(n1) und somit
def choose2(t):
    temp = 0 = zero()
    for i in range(t):
        temp = add(temp,i) = Comp(add, p0, p1)
und somit choose2=PrimRec(zero,Comp(add,π0,π1))pair=Comp(add,Comp(choose2,Comp(add,π0,Comp(succ,π1)),π0)

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 n=pair(x,y)=(i+12)+x(i+12)+0=pair(0,i) , und somit ist x+y der größte Wert von i, so dass npair(0,i) ist. Aus x+y und (x+y+12)+x können wir dann leicht x und y berechnen:

getXplusY=LargestLessThan(π0,Comp(greaterEqual,π1,Comp(pair,zero,π0)))first=Comp(minus,π0,Comp(pair,zero,getXplusY))second=Comp(minus,getXplusY,first)

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

push(x,restlist)=1+pair(x,restlist)head(list)=first(list1)second(list)=second(list1)

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):code>
  return int(((x+y+1) * (x+y)) / 2 + x )