Rekursion och den universella funktionen

Rekursion och den universella funktionen
Algoritmik för Systemvetenskap
HT 2007
Vi har hittills koncentrerat oss på primitiv rekursion. Bland de saker vi
noterat är att från varje definition av en primitivt rekursiv funktion, så kan vi
skapa ett program (i stenhögsspråket, eller liknande) som beräknar funktionen.
Däremot såg vi att vi inte kan göra det omvända, att gå från ett program till
en motsvarande primitivt rekursiv funktion. Anledningen var helt enkelt att till
exempel ett program som
Sålänge x är icke-tom {
Öka(x)
}
faktiskt inte beräknar någon funktion (eftersom det ibland inte stannar och ger
ett svar). För att kunna beskriva vad alla program gör, behöver vi alltså fler
begrepp.
Partiella funktioner
En partiell funktion är nästan samma sak som en funktion, förutom att det inte
alltid finns något utdata som hör till ett givet indata, det vill säga:
• För varje uppsättning indata finns högst ett tillhörande utdata1 . Om det,
givet indata x, finns ett tillhörande utdata, så skriver vi som vanligt f (x)
för detta. Om det inte finns något motsvarande utdata, så säger vi att
“f (x) är odefinierat”.
Vi ser nu att varje program beräknar en partiell funktion (åtminstone om
vi bestämmer vad som är programmets utdata). Det är då rimligt att fråga sig
om vi kan utöka de primitivt rekursiva funktionerna till en liknande grupp av
partiella funktioner, så att det vi får precis motsvarar de partiella funktioner vi
kan beräkna med program.
Partiella rekursiva funktioner
Vi får primitivt rekursiva funktioner på tre sätt:
• Som basfunktioner – Öka(x) (och, om vi ska vara noggranna, nollfunktionen och projektionerna).
1 För en funktion finns precis ett tillhörande utdata. För att göra skillnaden ännu tydligare,
så kallar man ibland funktioner för totala funktioner.
1
• Genom sammansättning av primitivt rekursiva funktioner vi redan konstruerat.
• Med schemat för primitiv rekursion: Om vi har konstruerat primitivt
rekursiva funktioner g och h, så kan vi definiera en ny primitivt rekursiv
funktion f genom:
f (0, y, . . .) := h(y, . . .)
f (Öka(x), y, . . .) := g(x, f (x), y, . . .).
Vi utökar nu detta med ytterligare en konstruktion, och får de partiella
rekursiva (eller µ-rekursiva) funktionerna. Sådana får vi på fyra sätt:
• Som basfunktioner – samma basfunktioner som förut.
• Genom sammansättning av partiella rekursiva funktioner vi redan konstruerat.
• Med schemat för primitiv rekursion, men med skillnaden att g och h nu
antas vara partiella rekursiva funktioner vi redan konstruerat.
• Genom minimering: Givet en partiell rekursiv funktion g(x, y, . . .), så får
vi en ny partiell rekursiv funktion µx.g(x, y, . . .), som vi kanske mer begripligt kan skriva som att vi definierar en ny funktion f genom
f (y, . . .) := Sök minsta x sådant att g(x, y, . . .) = 0.
I det enklaste fallet, så får vi från en funktion g(x) ett tal Sök minsta x
sådant att g(x) = 0 (eller, om inget sådant tal finns, en sökning som aldrig
slutar).
Vi har alltså bara lagt till “minimeringsoperatorn” µ2 till konstruktionerna
för primitivt rekursiva funktioner.
Vi beräknar ett uttryck av typen
Sök minsta x sådant att f (x) = 0.
genom att pröva med alla tal i tur och ordning. Med andra ord, vi beräknar
först f (0), och kontrollerar om det blir noll. Blir det det, så är vi färdiga, och
resultatet av minimeringen är 0. Annars beräknar vi f (1), och kontrollerar
om det blir noll. Om det blir det, så är resultatet av minimeringen 1, och
annars fortsätter vi, och försöker med f (2), f (3), och så vidare. Men vad händer
om f inte alltid producerar ett värde? Eftersom vi alltid arbetar med ivriga
beräkningar, så måste vi alltid beräkna f (0) fullständigt – det räcker inte att
se att det inte kan bli noll. Detta innebär att om det finns ett indata utan
utdata innan f blir noll, så kommer inte sökningen att stanna. Till exempel
om f (0) = f (1) = 1, f (3) = 0, men f (2) inte är definierat, så kommer en
beräkning av Sök minsta x sådant att f (x) = 0, efter att den i tur och ordning
beräknat f (0) och f (1) och konstaterat att de inte är noll, att “fastna” i att
försöka beräkna f (2) och aldrig ge ett svar.
Här är det viktigt att vi gör ivriga beräkningar!
2 Den
grekiska bokstaven µ motsvarar m – första bokstaven i minimering.
2
Exempel: Vi har på föreläsningarna skrivit en funktion Prima?, som svarar
på frågan om ett tal är primtal. Med hjälp av denna kan vi skriva funktionen
NästaPrimtalEfter, som givet ett tal n beräknar det första primtalet större än n:
NästaPrimtalEfter(n) := Sök minsta x sådant att Icke(Prima?(x) och x > n) = 0.
(Vi behöver anropet till Icke eftersom vi söker efter ett x som ger resultatet 0).
Sedan kan vi lätt definiera en funktion som beräknar det n-te primtalet:
NtePrimtalet(0) := 2
NtePrimtalet(Öka(x)) := NästaPrimtalEfter(NtePrimtalet(x)).
Program = partiell rekursiv funktion!?
Det är inte så svårt att övertyga sig om att vi i ett program kan göra en sökning,
så vi ser att alla partiella rekursiva funktioner går att beräkna med program.
Mer överraskande (och lite svårare att visa) är att alla program motsvaras av
en partiell rekursiv funktion.
Vi kan utnyttja detta: När vi vill svara på en fråga om vad som går, eller inte
går, att beräkna, så kan vi använda antingen program, eller partiella rekursiva
funktioner. I slutänden visar det sig ofta att många resonemang blir enklare
med rekursiva funktioner (i synnerhet om vi vill vara försiktiga med alla detaljer
i resonemangen).
Koder för program och den universella funktionen
En viktig idé om datorer är att de inte bara är maskiner som utför beräkningar,
utan att de är programmerbara maskiner. En vanlig miniräknare till exempel,
är en maskin som utför beräkningar – men den är begränsad; allt vi kan göra
med den är att knappa in ett uttryck, och den genomför beräkningen och ger
oss resultatet. En dator, å andra sidan, är ett mycket mer mångsidigt verktyg:
Vi ger datorn både ett program och indata att arbeta med, och den utför programmets instruktioner på de givna indata. På det sättet kan vi, genom att
skriva ett nytt program, få samma maskin att lösa många olika uppgifter.
Hittils i vårt arbete har vi gjort motsvarigheten till att konstruera en ny specialiserad “miniräknare” till varje nytt problem. Kan vi “låna” idén med program
till vårt arbete? Självklart!3 Vi kan koda en definition av en rekursiv funktion
(eller ett program) som data. Därefter behöver vi en motsvarighet till en dator,
nämligen något som kan ta emot ett program, och en uppsättning indata, och
därefter utföra programmets instruktioner. Det vi behöver är den universella
rekursiva funktionen U . Funktionen U är en partiell rekursiv funktion som tar
två argument, (en kod för) en partiell rekursiv funktion f och en lista indata x,
och beräknar U (f, x) = f (x).
Konstruktionen av U beror naturligtvis på precis hur vi kodar rekursiva
funktioner, och blir därför ganska krånglig. Den underliggande idén är däremot
enkel: vi letar efter en beräkning4 som följer och slutför instruktionerna. Från
den beräkningen kan vi därefter läsa av resultatet.
3 Bättre än så, idén kom ursprungligen från det teoretiska arbetet, och infördes därifrån i
de faktiska maskinerna!
4 Som vanligt, egentligen en kod för en beräkning. Det faktum att vi är tvugna arbeta med
koder för nästan allting är vad som gör det hela krångligt.
3
Stopproblemet, och andra frågor om program
När vi har koder för definitioner av partiella rekursiva funktioner, så finns det
givetvis många frågor vi kan vilja ha svar på.
En enkel fråga är “Är det här en primitivt rekursiv funktion?”. Eftersom
det är samma sak som att fråga om definitionen använder minimering (Sök
minsta. . . ), så kan vi besvara den frågan, genom att inspektera definitionen.
En annan fråga är “Givet funktionen f och indata x, är f (x) definierat?”
(eller, alternativt “Är U (f, x) definierat?”). Om vi talar om program, så motsvarar
den frågan “Kommer programmet P att stanna om vi ger det indata x?”.
Därifrån kommer namnet Stopproblemet.
Frågan kan inte besvaras av ett program. Stopproblemet är oavgörbart.
4