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