BERÄKNINGSBARHET FÖR DATALOGER

Storlek: px
Starta visningen från sidan:

Download "BERÄKNINGSBARHET FÖR DATALOGER"

Transkript

1 BERÄKNINGSBARHET FÖR DATALOGER Från λ till P Kent Petersson Institutionen för Datavetenskap Göteborgs Universitet / Chalmers Göteborg, Sweden

2 ii Kent Petersson (epost 2011: Kent.Petersson(AT)gmail.com) Copyright 1987 Kent Petersson

3 iii Förord Denna bok har växt fram ur föreläsningsanteckningar som jag skrivit för kursen Teoretisk Datalogi som ingår i Matematikerlinjens tredje årskurs vid Göteborgs Universitet. Föreläsningsanteckningarna har sin tur växt fram under de fyra år som jag varit ansvarig för kursen. Anledningen till att jag började skriva egna anteckningar var att jag tyckte att de böcker som fanns tillgängliga inte tog upp ämnet beräkningsbarhet ur en datalogisk synvinkel. Det kändes inte meningsfullt att ägna en stor del av kursen till att definiera syntax och semantik för en beräkningsmodell (Turing-maskiner) som var så olik de beräkningsmodeller som studenterna var vana vid. I synnerhet inte när problemen med att beskriva syntax och semantik för vanliga programmeringsspråk är så centrala inom datalogi. Genom att välja en beräkningsmodell som liknar ett vanligt programmeringsspråk får man, som jag ser det, två fördelar: Dels ser studenterna sambandet med sin övriga verksamhet, och dels kan man inom kursens ram ta upp många datalogiska problem som har med programmeringsspråk att göra. Eftersom jag introducerar ett mycket enkelt programmeringsspråk som beräkningsmodell, kan jag till och med formellt definiera både syntax och semantik för detta språk. Jag hoppas att denna utgångspunkt gör att sambandet mellan de problem som tas upp i kursen och vanliga datalogiska problem blir mer framträdande. Jag är mycket tacksam för de synpunkter på tidigare versioner av föreläsningsanteckningarna som jag fått både från elever och från olika medlemmar i Programmeringsmetodikgruppen. Diskussionerna i Programmeringsmetodikgruppen om funktionella språk och typteori har dessutom i mycket stor utsträckning påverkat min inställning till datalogiska problem i allmännhet. Jag är medveten om att boken fortfarande innehåller felaktigheter, en del orsakade av min tanklöshet, andra av texthanteringssystemets fantastiska förmåga att hela tiden göra som jag skriver och inte som jag vill!!! Synpunkter och påpekanden om innehåll och tryckfel mottages därför med stor tacksamhet. Göteborg i december 87 K P Efter att det nu gått en del år sedan jag skrev den här boken tänkte jag lägga upp den så att den blir tillgänglig på nätet. Det är ju möjligt att det finns dataloger som är intresserade av ämnet. Efter en del trassel med svenska bokstäver, som inte behandlades med någon större respekt 1987 när den här boken skrevs, har jag lyckats generera en ny version av boken. Jag har inte gjort någon större genomläsning för att se om något har gått fel vid tryckningen eller om det finns några andra fel. Hittar ni något så kontakta mig

4 iv gärna. Det kan ni göra också om ni har några andra synpunkter eller tyckanden eller bara för skoj skull. Göteborg i november 2011 K P (kent.petersson(at)gmail.com)

5 Innehåll 1 INTRODUKTION 1 2 MÄNGDLÄRA Naturliga tal Sanningsvärden Tecken Heltal Sekvenser eller listor Cartesiska produkter Disjunkta unioner Andra mängdbildningsoperationer Matematiska funktioner Partiella och totala funktioner Injektiva, surjektiva och bijektiva funktioner Komposition av funktioner Funktioner från naturliga tal Funktioner från Bool Funktioner från listor BERÄKNINGSBARA FUNKTIONER Uppräkningsbara mängder En beräkningsmodell programspråket P Definition av programspråk syntax Grammatiker Chomsky hierarkin BNF Formella språk P:s syntax Abstrakt syntax Definition av programspråk semantik P:s semantik v

6 vi INNEHÅLL 3.8 Förkortningar Satsförkortningar Uttrycksförkortningar Testförkortningar Beräkningar med andra dataobjekt P-program som dataobjekt En interpretator för P-program skriven i P Några icke-beräkningsbara funktioner Rekursion BERÄKNINGSBARA MÄNGDER Rekursivt enumerabla mängder Avgörbara mängder Predikatlogik är oavgörbar λ-kalkyl Inledning λ-notation Abstraktion Applikation λ-kalkyl Beräkningsregler Likhetsregler Kombinatorer Representation av naturliga tal och andra dataobjekt Rekursion λ-kalkyl och beräkningsbarhet Översättning från P-program till λ-termer En interpretator för λ-termer skriven i P BERÄKNINGSMODELLER Ändliga tillståndsmaskiner Turing maskiner Rekursiva funktioner LITTERATURHÄNVISNINGAR BETECKNINGAR SATSER OCH DEFINITIONER 245 A NÅGRA BEVISMETODER 257

7 INNEHÅLL vii B NÅGRA SATSER UTAN BEVIS 261

8 Kapitel 1 INTRODUKTION En stor del av dessa föreläsningsanteckningar kommer att handla om beräkningar och framförallt kommer de att handla om vilka beräkningar som överhuvudtaget är möjliga. Vad är då en beräkning? Vi använder ordet i betydelsen: en manipulering av symboler som kan utföras av en mekanisk process. Varje beräkning skall alltså kunna utföras av en beräkningsmaskin, utan någon som helst interaktion utifrån. Ett exempel på en beräkning är när ett program och dess indata exekveras av en dator, ett annat är när vi beräknar värdet av ett aritmetiskt uttryck, t.ex. en division, enligt skolmetoden. För vårt syfte, dvs. att studera vilka beräkningar som är möjliga, räcker det att studera beräkningar som sker enligt följande schema (modell): 1. Maskinen förses med indata till beräkningen. 2. Beräkningen startas. 3. Om beräkningen terminerar, så presenteras resultatet (utdata). Man kan fråga sig om detta inte ger en alltför snäv bild. Varje beräkning som inte terminerar ger ju enligt modellen inget resultat och är därför i någon mening värdelös. Men i datalogi känner vi ju till flera exempel på program som inte är avsedda att terminera, men som samtidigt producerar värdefulla resultat; ett operativsystem till exempel. Ett försvar för den givna modellen är att ett operativsystem alltid producerar ett resultat från en ändlig, inledande sekvens av sina indata, och för varje sådan sekvens finns, i den ovan skisserade modellen, en terminerande beräkning som ger samma resultat. Skillnaden är att beräkningen i vår modell terminerar, medan operativsystemet kan fortsätta att producera resultat. Så alla beräkningar finns med i den skisserade modellen, men det kan vara stor skillnad på hur en beräkning sker i modellen och hur den sker i verkligheten. Det finns en bättre modell som behandlar icke-terminerande beräkningar på ett naturligare 1

9 2 KAPITEL 1. INTRODUKTION sätt Dana Scotts matematiska modell för beräkningar. I den tänker man sig operativsystemets resultat i en viss tidpunkt som en approximation av hela (det oändliga) resultatet, som man naturligtvis aldrig kan erhålla i sin helhet. Tyvärr är Scotts modell alltför komplicerad för att vi skall hinna behandla den i den här kursen. Eftersom varje beräkning skall kunna utföras av en maskin, så kan vi studera beräkningsmaskiner istället för beräkningar. Men vad är då en beräkningsmaskin? Vi har väl alla en intuitiv förståelse för vad det är, men det är svårt, för att inte säga omöjligt, att översätta denna intuition till en exakt definition. Vi skall inte heller studera maskinerna själva, utan snarare beskrivningar av dem. Om varje beräkning skall kunna utföras av en maskin, måste den utföras enligt en på förhand bestämd metod enligt en given algoritm, eller, för att använda vårt vanliga språk, enligt ett givet program. Vi tänker oss alltså ett program som en abstrakt beskrivning av en beräkningsmaskins funktionssätt. Tänk till exempel på hur man använder en dator; en användare behöver inte i detalj veta hur maskinen fungerar, det räcker om hon vet hur det exekverande programmet fungerar. Vi kan därför se ett program som en abstrakt beskrivning av en maskins funktionssätt. Observera att vi inte tänker oss att en beräkningsmaskin ska kunna evaluera olika program; för att kunna arbeta på det sättet, måste programmen vara indata till maskinen och det är ett mer komplicerat problem. Än så länge tänker vi oss att en maskin bara kan utföra en enda algoritm. Från en matematiskt synpunkt, kan vi betrakta ett program som en matematisk funktion. Ett givet program motsvaras då av den funktion som avbildar programmets indata på dess utdata. Eftersom det är möjligt att exekveringen av ett program inte terminerar för alla indata, motsvaras programmet av en partiell funktion, dvs. en funktion som kan vara odefinierad för vissa argument. Vi säger att en partiell funktion f beräknas av ett program Q om 1. Q ger resultatet f(x) för indata x om f är definierad för x, 2. Q inte terminerar för indata x om f inte är definierad för x. Klassen av partiella funktioner som beräknas av något program kallar vi för de beräkningsbara partiella funktionerna. Figur 1.1 ger en bild av hur de olika begreppen beräkning, maskin, program (algoritm) och partiell funktion är relaterade. Genom att se ett program som en partiell funktion gör vi en abstraktion hur programmet evalueras blir ointressant, endast indata-utdata relationen är av intresse. Detta sätt att se ett program är naturligtvis inte det enda sättet, och absolut inte alltid det bästa. Genom att göra en abstraktion bortser vi ju från vissa oväsentliga omständigheter; och vad som är väsentligt eller oväsentligt beror ju i högsta grad på vad vi ska använda resultatet av abstraktionen till. Att se ett

10 Figur 1.1: Begreppen beräkning, maskin, program och funktion 3

11 4 KAPITEL 1. INTRODUKTION program som en funktion innebär till exempel att vi helt bortser från programmets effektivitet. Om ett program producerar utdata på ett beräkningssteg eller om det tar beräkningssteg spelar ingen roll så länge som samma indata ger samma resultat. För vissa användningar är detta betraktelsesätt fördelaktigt, för andra, till exempel komplexitetsanalys, är det naturligtvis oanvändbart. Vårt första mål i kursen är att studera vad som är möjligt att utföra med ett program. Eftersom vi då bara är intresserade av resultatet av programmet, är det fruktbart att studera relationen mellan program och partiella funktioner. Frågan om vad som är möjligt att utföra med ett program kan då formuleras på följande sätt: Finns det partiella funktioner som överhuvudtaget inte kan beräknas? För att visa att en partiell funktion inte kan beräknas måste vi visa att det inte finns något program som beräknar den. Detta förutsätter att vi har en exakt definition av begreppet program. Någon sådan har vi ännu inte gett, hittills har vi bara förklarat vad ett program är i termer av de inexakta begreppen maskin och mekanisk process. Problemet med att ge en definition är att det är omöjligt att veta om den exakt motsvarar det intuitiva begreppet. Inget hindrar ju att vi någon gång i framtiden kan hitta på en ny revolutionerande beräkningsmetod som inte täcks av den givna definitionen. Vad vi däremot kan göra är att ge en definition och påstå att den precis täcker vad vi menar med begreppet program (algoritm). Givet en sådan definition, kan vi visa att den är vettig genom att beskriva en mekanisk metod att evaluera varje definierat objekt. Vad vi däremot inte kan visa är att definitionen är fullständig i den meningen att den genererar alla program. Många olika definitioner har föreslagits alltsedan 1935 när Alonzo Church definierade ett program som ett objekt i λ-kalkyl. Flera andra förslag kom strax efter det mest kända är Alan Turings definition med hjälp av det som nu kallas Turing-maskin. Varje förslag gav upphov till en mängd av beräkningsbara funktioner. Ett argument för att Churchs förslag till definition av begreppet program faktiskt täcker det intuitiva begreppet är att alla de olika förslagen gav upphov till samma mängd av partiella funktioner. Påståendet att denna mängd är precis de intuitivt beräkningsbara funktionerna kallas för Churchs tes, eller Church-Turings tes. Vi kommer att studera flera av förslagen att definiera begreppet program i ett senare avsnitt. Själva kommer vi att definiera begreppet genom att beskriva ett programmeringsspråk som vi kallar P. Mängden av program blir då helt enkelt lika med mängden av P-program, och relationen till de partiella funktionerna får vi från definitionen av språket P:s semantik. Låt oss börja med att se vilka egenskaper som vi naturligt associerar med en beräkningsmaskin för att sedan se vilka konsekvenser dessa egenskaper får. De viktigaste egenskaperna som vi förutsätter om beräkningsmaskiner är:

12 5 1. En beräkningsmaskin är ändlig. 2. En beräkningsmaskin arbetar i diskreta steg och varje steg tar en bestämd tid att utföra. 3. En beräkningsmaskin arbetar alltid deterministiskt. Att en beräkningsmaskin är ändlig betyder att den i varje ögonblick bara kan innehålla ( minnas ) en ändlig mängd symboler. Däremot behöver det inte finnas någon på förhand given övre begränsning på maskinens storlek (minnesstorlek). Eftersom vi förutsätter att en maskin arbetar i diskreta steg och att varje steg tar en bestämd tid att utföra, kan vi se att en beräkning endast terminerar om den kan utföras med ett ändligt antal steg. Vi utesluter alla former av analoga maskiner eftersom dessa inte arbetar i diskreta steg och man därför inte kan få exakta resultat. Att en maskin arbetar deterministiskt betyder bara att givet indata är ett eventuellt resultat välbestämt, eller, med andra ord, en maskin uppför sig alltid på samma sätt för samma indata. Om beräkningsmaskiner inte vore deterministiska, så skulle vi inte kunna använda definitionen ovan av vad som menas med att ett program beräknar en partiell funktion. Eftersom en beräkningsmaskin är ändlig kan den bara behandla en ändlig mängd symboler i varje steg, och eftersom den arbetar i diskreta steg, så måste en terminerande beräkning bestå av ett ändligt antal sådana steg. Oändliga indata kan därför inte hinna behandlas under en beräkning och oändliga resultat kan inte hinna produceras. En lämplig modell för indata och utdata till beräkningsmaskiner är därför de naturliga talen. Eftersom varje ändlig sekvens av tecken från ett alfabet (ändlig mängd) kan kodas som ett naturligt tal, finns det till varje beräkning vars indata och utdata är sekvenser av tecken, en motsvarande beräkning mellan naturliga tal. Vi förlorar därför inga beräkningar genom att bara studera beräkningar vars indata och utdata utgörs av naturliga tal. Språket P, dvs. vår beräkningsmodell, kommer därför bara att beskriva beräkningar med naturliga tal.

13 6 KAPITEL 1. INTRODUKTION

14 Kapitel 2 MÄNGDLÄRA Det här kapitlet är i stor utsträckning en repetition av grundläggande matematiska begrepp. En stor del av föreläsningsanteckningarna handlar om olika skillnader mellan de matematiska begreppen funktion och mängd, och motsvarande datalogiska begrepp, program och typ. Eftersom dessa skillnader finns i utkanterna av begreppens definitioner är det viktigt att ha exakta formuleringar av begreppens definitioner som man kan gå tillbaka till om något är oklart. Vi formulerar därför en del av de grundläggande matematiska definitionerna i detta avsnitt och introducerar samtidigt de beteckningar som vi i fortsättningen kommer att använda Eftersom det mesta (väl?) redan är känt görs framställningen något schematisk. 2.1 Naturliga tal Vi börjar med de naturliga talen. I mängdlära behöver man inte införa några andra objekt än mängder; en mängd är antingen tom eller har andra mängder som element. De naturliga talen kan till exempel representeras av följande mängder: 0 representeras av 1 { } 2 {, { }} 3 {, { }, {, { }}}. Vi skall inte införa de naturliga talen som mängder, det finns det ingen anledning till. Vi ser istället de naturliga talen som primitiva objekt och introducerar dem med följande induktiva klausuler: E1: 0 är ett naturligt tal. 7

15 8 KAPITEL 2. MÄNGDLÄRA E2: Om m är ett naturligt tal, så är också s(m) ett naturligt tal. s(m) betyder efterföljaren (successor) till m dvs. m + 1. E3: Alla naturliga tal erhålls genom att tillämpa E1 och E2 ett ändligt antal gånger. Vi har nu induktivt definierat precis vad som är ett naturligt tal. Men vi är inte färdiga, vi måste också definiera när två tal är lika. Vi gör detta genom följande klausuler: L1: 0 är inte lika med s(n) för något naturligt tal n. L2: De två talen s(m) och s(n) är lika om och endast om m och n är lika. Vi använder N som beteckning på den mängd av objekt som genereras av de här klausulerna, n N för att beteckna att n är ett element i N, samt m = n N respektive m n N för att säga att m och n är lika respektive olika naturliga tal. Om vi skärskådar klausulerna E1-E3 och L1-L2, så ser vi att från E1 får vi att 0 är ett naturligt tal, dvs. 0 N. När vi vet det, kan vi använda E2 för att få fram att Dessutom får vi från L1 att s(0) är ett naturligt tal, dvs. s(0) N. 0 och s(0) är olika naturliga tal, dvs. 0 s(0) N. Om vi går vidare kan vi tillämpa E2 igen och få s(s(0)) är ett naturligt tal, dvs. s(s(0)) N dessutom får vi från L1, L2 och tidigare resultat att s(s(0)) 0 N och s(s(0)) s(0) N och så vidare. Vi ser att klausulerna genererar objekten 0, s(0), s(s(0)),... och att alla dessa är olika. Ett annat sätt att karakterisera de naturliga talen är Peanos fem axiom. Klausulerna E1, E2, L1 och L2 är i stort sett de fyra första axiomen, men det femte, som är känt under namnet matematisk induktion, är formulerat på ett annorlunda sätt:

16 2.2. SANNINGSVÄRDEN 9 B1: Om man kan bevisa att ett påstående gäller för 0, dvs. att C(0) är sann, och om man kan bevisa att C(m) C(s(m)) för ett godtyckligt naturligt tal m, då kan man dra slutsatsen att C(n) är sann för alla naturliga tal n. Det följer omedelbart av de klausuler som vi använde för att definiera de naturliga talen att detta är en korrekt bevisprincip. Vi kommer naturligtvis inte uteslutande att använda denna unära presentation av de naturliga talen, utan vi kommer också att använda den vanliga decimala presentationen, men vi ser hela tiden denna som en förkortning av den unära presentationen. De naturliga talen är naturligtvis intressanta i sig själva, men sättet att introducera dem är också intressant. Om vi betraktar klausul E1-E3 igen, kan vi se att E1 introducerar ett atomärt objekt medan E2 introducerar nya objekt med hjälp av redan definierade objekt. Klausul E3 förefaller så självklar att man ibland inte ens bryr sig om att formulera den, men den är mycket viktig; utan den kan man inte bevisa speciellt mycket. Bevisprincipen matematisk induktion bygger till exempel i hög grad på E3. Vi säger att klausulerna E1-E3 och L1-L2 tillsammans utgör en induktiv definition av mängden av naturliga tal; vi kallar E1 för en atomär klausul, E2 för en induktiv klausul samt E3 för en extrem klausul. Klausul L1-L2 ger, som vi redan sett, att alla tal som inte genererats på samma sätt är olika eller, uttryckt annorlunda, två tal är lika om och endast om de genererats på samma sätt. Mängder vars element genererats med induktiva klausuler och som har denna egenskap kallar vi för fritt genererade mängder. Dessa mängder är förhållandevis enkla att hantera och de flesta mängder som vi kommer att introducera är av den här typen. 2.2 Sanningsvärden Vi skall fortsätta med att definiera några andra mängder som vi kommer att använda längre fram och vi definierar dem på precis samma sätt som vi definierade de naturliga talen. Mängden av sanningsvärden, eller Bool som vi kommer att kalla den, har bara två element: true och false. Om vi introducerar dessa på samma sätt som vi introducerade de naturliga talen, får vi två atomära klausuler: E1: true är ett sanningsvärde, dvs. true Bool. E2: false är ett sanningsvärde, dvs. false Bool. Det finns inga induktiva klausuler och extrem klausulen har följande utseende:

17 10 KAPITEL 2. MÄNGDLÄRA E3: Alla sanningsvärden genereras av E1 och E2. För att beskriva när två sanningsvärden är lika respektive olika behöver vi följande klausul: L1: true och false är olika sanningsvärden, dvs. false true Bool. På samma sätt som vi för de naturliga talen kunde motivera en bevisprincip, matematisk induktion, kan vi nu motivera följande bevisprincip för sanningsvärden. B1: Om vi kan visa att ett påstående C(b) gäller för b = true, dvs. om vi kan visa att C(true) är sann, och om vi kan visa att C(false) är sann, så är C(b) sann för alla sanningsvärden b. Vi kallar denna bevisprincip för fallanalys eller fallanalys för sanningsvärden. Alla fritt genererade mängder utan induktiva klausuler och där de atomära klausulerna inte refererar till element i andra mängder kan genereras på detta enkla sätt. Vi kallar dessa mängder för uppräkningsmängder och de har en motsvarighet i många programmeringsspråk, ta till exempel uppräkningstypen (blue, green, red) i Pascal. 2.3 Tecken Ett ytterligare exempel på en uppräkningsmängd som har en motsvarighet i de flesta programmeringsspråk är Char, mängden av tecken. Dess storlek gör den tyvärr något ohanterlig. Om vi baserar den på ASCII-alfabetet så finns 128 element och därför 128 atomära klausuler, en extrem klausul, samt ett större antal (o)likhetsklausuler. Vi skriver därför bara ut några av dessa: E1: NUL är ett tecken, dvs. NUL Char. E33:! är ett tecken, dvs.! Char. E66: A är ett tecken, dvs. A Char. E128: DEL är ett tecken, dvs. DEL Char. E129: Alla tecken genereras av klausulerna E1, E2,..., E128. L?: NUL är inte samma tecken som A, dvs. NUL A Char. L?: A är inte samma tecken som B, dvs. A B Char.

18 2.4. HELTAL Heltal Mängden av heltal, Int, är ett exempel på en mängd som inte är fritt genererad, åtminstone inte som vi introducerar den. Vi har följande klausuler: E1: Om n är ett naturligt tal, så är +n ett heltal. E2: Om n är ett naturligt tal, så är n ett heltal. E3: Alla heltal genereras av E1 och E2. Likhetsklausulerna har följande utseende: L1 +m och +n är lika heltal om och endast om m och n är lika naturliga tal. L2: m och n är lika heltal om och endast om m och n är lika naturliga tal. L3: +m och n är lika heltal om och endast om både m och n är lika med 0. Att mängden inte är fritt genererad framgår av klausul L3, +0 och 0 är inte genererade av samma klausul med ändå lika heltal. Bevisprincipen för heltalen skrivs på följande sätt: B1: Om vi kan bevisa ett påstående C(i) för i = +n, där n är ett godtyckligt naturligt tal och om vi kan bevisa påståendet för i = m, där m också är ett godtyckligt naturligt tal, så kan vi dra slutsatsen att C(i) är sann för alla heltal i. Övning 2.1 Går det att introducera mängden av heltal som en fritt genererad mängd? Hur? 2.5 Sekvenser eller listor De mängder som vi har introducerat hittills har varit atomära, de har inte berott på några andra mängder. Vi skall nu fortsätta med mängdoperationer som givet en eller flera mängder skapar nya mängder. Antag att vi har en mängd A. Vi vill introducera mängden av listor, eller sekvenser, över A, med beteckningen List(A). Vi gör detta på nästan precis samma sätt som vi introducerade de tidigare mängderna. Först har vi en atomär klausul: E1: nil (den tomma listan) är en lista, dvs. nil List(A). Sedan har vi en induktiv klausul:

19 12 KAPITEL 2. MÄNGDLÄRA E2: Om a är ett element i A och l är en lista över A, så är cons(a, l) en lista över A, dvs. om a A och l List(A) så gäller att cons(a, l) List(A). I funktionella språk använder man ofta a.l som en alternativ beteckning för cons(a, l). Och slutligen extrem klausulen: E3: Alla listor över A genereras av E1 och E2. I många funktionella språk inför man ytterligare en beteckning för listor, istället för att skriva cons(a 1,... cons(a n, nil)...)) så använder man beteckningen [a 1,..., a n ]. Övning 2.2 Formulera de naturliga likhetsreglerna för listor över A (jfr. med likhetsreglerna för de naturliga talen). Övning 2.3 Formulera den bevisprincip som för listor motsvarar matematisk induktion för naturliga tal. 2.6 Cartesiska produkter Om A och B är två mängder så kan vi bilda mängden av (ordnade) par, eller den Cartesiska produkten, av A och B, (beteckning A B). Elementen i den här mängden är (ordnade) par a, b, där a är element i A och b är element i B. Om vi uttrycker detta i termer av klausuler får vi en atomär klausul och en extrem klausul. E1: Om a A och b B så gäller det att a, b A B. E2: Alla ordnade par genereras av klausul E1. Likheten mellan två par ges av följande klausul: L2: Två par a, b och a, b är lika om och endast om a = a A och b = b B. Vi inför namnet explicit definierade mängder för de mängder som genereras av atomära klausuler i vilka man refererar till tidigare introducerade mängder; A B är ett exempel på en sådan mängd. Övning 2.4 Formulera bevisprincipen för ordnade par.

20 2.7. DISJUNKTA UNIONER Disjunkta unioner Om A och B är två mängder, kan vi bilda den disjunkta unionen av A och B (beteckning A + B). Elementen genereras av klausulerna: E1: Om a A så är inl(a) ett element i A + B, dvs. inl(a) A + B. E2: Om b B så är inr(b) ett element i A + B, dvs. inr(b) A + B. E3: Alla element genereras av E1 och E2. Beteckningen inl står för INject Left och inr står för INject Right. Eftersom både E1 och E2 är atomära klausuler så är A + B en explicit definierad mängd. Likheten mellan elementen ges av följande klausuler: L1: Två element inl(a) och inl(a ) är lika om och endast om a = a A. L2: Två element inr(b) och inr(b ) är lika om och endast om b = b B. L3: Två element inl(a) och inr(b) är aldrig lika. Bevisprincipen för elementen i en disjunkt union har följande utseende: B1: Om vi kan bevisa ett påstående C(x) för x = inl(a), där a är ett godtyckligt element i A, och om vi kan bevisa påståendet för x = inr(b) för ett godtyckligt b B, så är C(x) sann för alla x A + B. Den disjunkta unionen A + B skiljer sig från den vanliga unionen A B genom att man för ett godtyckligt element a A + B direkt kan avgöra om elementet kommer från A eller B. Elementen i A + B ser därför inte ut som elementen i A eller elementen i B, utan är märkta (eng. tagged) så att man ser varifrån de kommer. Ett konkret exempel kan kanske förklara situationen: Exempel: Mängden N N består av elementen 0, 1, 2, 3,... och det gäller att N N = N. Men N + N innehåller elementen inl(0), inr(0), inl(1), inr(1),... och N + N kan därför inte vara lika med N eftersom två mängder är lika endast om de innehåller samma element. I våra vanliga programmeringsspråk finns det ingen direkt motsvarighet till den vanliga unionen, medan den disjunkta unionen motsvaras av poster med varianter. Mängden Int + Int motsvaras till exempel i Pascal av typen record case { l,r } of l : (inl : Integer); r : (inr : Integer) end

21 14 KAPITEL 2. MÄNGDLÄRA Den stora skillnaden är att vi i Pascal själva kan namnge de olika komponenterna, medan vi i en disjunkt union har fördefinierade namn, inl och inr. I ett programmeringspråk som ska användas praktiskt är Pascals lösning naturligtvis mycket bättre. 2.8 Andra mängdbildningsoperationer De mängder och mängdbildningsoperationer vi hittills har introducerat har direkta motsvarigheter i många programmeringsspråk, till exempel Pascal och ML. Vi ska nu ytterst kortfattat beskriva några andra mängdoperationer som inte har en sådan direkt motsvarighet i våra vanliga programmeringsspråk. För att ge en mer fullständig bild av den del av mängdlära som vi behöver, så radar vi bara upp de mer matematiska mängdoperationerna. 1. Om A = {A 1, A 2,...} är en mängd av mängder, så är unionen av alla element i A, i=1a i, en mängd. 2. Om A och B är mängder, så är snittet, A B, en mängd. 3. Om A är en mängd, så är potensmängden av A, dvs. mängden av alla delmängder av A, P(A), en mängd. 4. Om C(x) är ett påstående för alla x A, så är {x A C(x)} en mängd, mängden av alla x A för vilka C(x) är sann. 5. Om A är en mängd av icke-tomma, disjunkta mängder A 1, A 2,..., så kan vi bilda en mängd med exakt ett element från varje A i (urvalsaxiomet). Övning 2.5 Ge en induktiv definition av mängden av binära träd BinTree (utan information i noder och löv).

22 2.9. MATEMATISKA FUNKTIONER Matematiska funktioner När det gäller matematiska funktioner formulerar olika författare ofta sina definitioner lite olika, ibland använder man olika beteckningar, och kanske i värsta fall samma beteckning för olika saker. Ord som definitionsmängd, domän, etc. betyder därför inte alltid samma sak. Skillnaden är ofta inte stor men kan vålla förvirring. Detta är ännu ett skäl till att denna sektion är så utförlig och innehåller så många definitioner som du kanske redan känner till Partiella och totala funktioner Intuitivt brukar funktioner beskrivas som avbildningar från en mängd A till en mängd B. Istället för att använda det intuitiva begreppet avbildning definierar vi funktionsbegreppet helt i termer av mängder. Definition 2.1 Låt A och B vara två godtyckliga mängder. Då är f en partiell funktion från A till B om 1. f är en delmängd av A B, 2. x, y f och x, y f medför att y = y B. En funktion f är alltså en mängd av ordnade par med egenskapen att det till ett givet x A finns som mest ett y B sådant att x, y f. Mängden g = { 1, 1, 2, 4, 3, 9 } N N är ett exempel på en partiell funktion från N till N, medan { 1, 1, 1, 4, 1, 9 } är ett exempel på en mängd som inte är en partiell funktion. Den tomma mängden är en partiell funktion från varje mängd A till varje mängd B; vi kommer att använda beteckningen Λ för denna partiella funktion. Mängden av alla partiella funktioner från A till B betecknar vi A B. Den mängd som i definitionen ovan betecknades A kallar vi för funktionens definitionsmängd och B kallar vi för funktionens värdemängd. Vidare kallas mängden av alla x A, för vilka det existerar ett y B sådant att x, y f, för f:s domän, med beteckning dom(f). Slutligen kallar vi mängden av alla y B, för vilka det existerar ett x A sådant att x, y f för f:s resultatmängd. Vi använder beteckningen ran(f) (range) för denna mängd. I symboler har vi att dom(f) = {x A ( y) (y B) & ( x, y f)}

23 16 KAPITEL 2. MÄNGDLÄRA ran(f) = {y B ( x) (x A) & ( x, y f)} I exemplet ovan blir dom(g) = {1, 2, 3} ran(g) = {1, 4, 9} Två funktioner är lika då motsvarande mängder är lika, detta går också att formulera på följande sätt: Två partiella funktioner f, g A B är lika om 1. dom(f) = dom(g) 2. f(x) = g(x) för alla x dom(f). Om definitionsmängden är lika med domänen för en partiell funktion f, säger vi att f är en total funktion. Mängden av totala funktioner från A till B betecknas A B. Ibland kommer vi att använda ordet funktion utan bestämning, det betyder då total funktion. Det är viktigt att komma ihåg att funktioner definieras som mängder av par. Vi kommer däremot inte att använda notationen för par när vi ska beteckna en funktion. Istället för att skriva x, y f så använder vi beteckningen f(x) = y Den här beteckningen betyder som vanligt att x dom(f) och att y är f:s värde för argumentet x, eller med andra ord, att y är värdet av att applicera funktionen f på argumentet x. Om f är odefinierad för x, dvs. x dom(f), så skriver vi lite slarvigt f(x) = odefinierad Vi kommer också att definiera funktioner med hjälp av denna notation. Vi skriver till exempel f(x) = x istället för att använda motsvarande mängdbeteckning f = { x, y N N y = x 2 + 1}

24 2.9. MATEMATISKA FUNKTIONER 17 Figur 2.1: En injektiv funktion Injektiva, surjektiva och bijektiva funktioner Vi skall nu fortsätta med att definiera tre viktiga klasser av funktioner: de injektiva, de surjektiva och de bijektiva funktionerna. Den första klassen innehåller alla funktioner vars värden är unika, dvs. de funktioner som avbildar olika argument på olika värden. Definition 2.2 En injektiv (partiell) funktion (eng. one-to-one) från A till B är en funktion som alltid avbildar två olika element i dom(f) på två olika element i B. Funktionen f N N, med definition f(x) = x är ett exempel på en injektiv funktion. Nästa klass av funktioner innehåller de funktioner där varje element i värdemängden är bild av åtminstone ett element i domänen. Definition 2.3 I en surjektiv (partiell) funktion (eng. onto) från A till B är varje element i B bild av något element i A. Vi skulle också kunna säga att en funktion är surjektiv om dess värdemängd är lika med dess resultatmängd. Vi ser att funktionen add N N N med definition add(x, y) = x + y är en surjektiv funktion, eftersom alla naturliga tal kan skrivas som en summa av två tal. De totala surjektiva och de totala injektiva funktionerna kan relateras till varandra genom följande sats:

25 18 KAPITEL 2. MÄNGDLÄRA Figur 2.2: En partiell surjektiv funktion Sats 2.4 Låt A och B vara två icke-tomma mängder. 1. Om f A B är en total injektiv funktion, så finns en total surjektiv invers till f, dvs. det finns en total surjektiv funktion g B A sådan att g(f(x)) = x för alla x A. 2. Om g B A är surjektiv, så finns en total injektiv funktion f A B sådan att g(f(x)) = x för alla x A. Bevis: 1. Eftersom f är injektiv är varje element i B som mest bild av ett element i A. Då A vet vi dessutom att det existerar ett element a A. Definiera nu g enligt: g(x) = { y, om det finns något y A sådant att f(y) = x a, annars. g är en funktion eftersom f är injektiv, den är total eftersom den är definierad för alla argument, den är surjektiv eftersom f är total och likheten g(f(x)) = x följer direkt från definitionen av g. 2. För varje a A definierar vi en delmängd B a av B enligt: B a = {b B g(b) = a} dvs. B a består av alla element i B som avbildas på a av funktionen g B A (se figur 2.3). Eftersom g är en surjektiv funktion är varje a A bild av åtminstone ett b B. För varje mängd B a gäller därför att B a. Använd

26 2.9. MATEMATISKA FUNKTIONER 19 Figur 2.3: Mängden B a nu urvalsaxiomet och välj ut ett element b a ur varje B a och definiera sedan f A B enligt: f(x) = b x där b x är det utvalda elementet i mängden B x. Eftersom varje b B bara kan ingå i en av mängderna B x blir f injektiv, och eftersom det finns en mängd B x för varje x A blir f total. Dessutom följer att g(f(x)) = x för alla x A från f:s definition. Om vi kombinerar de totala injektiva och de totala surjektiva funktionerna, får vi en ny klass, de bijektiva funktionerna. En funktion i denna klass är både injektiv och surjektiv. Definition 2.5 En bijektion eller en bijektiv funktion (eng. ono-to-one correspondance) är en total funktion som är både injektiv och surjektiv. Om det existerar en bijektion f mellan två mängder A och B, så associerar f entydigt ett element i B med varje element i A och vice versa Komposition av funktioner Komposition är den enda operation som vi definierar på partiella funktioner. Definition 2.6 Om g A B och f B C är partiella funktioner, så är kompositionen av f och g, med beteckning f g, en partiell funktion från A till C. Funktionen f g definieras av (f g)(x) = f(g(x))

27 20 KAPITEL 2. MÄNGDLÄRA Figur 2.4: En bijektiv funktion om x dom(g) och g(x) dom(f) och annars är den odefinierad. Domänen av f g är alltså en delmängd till domänen av g. Övning 2.6 Ibland används beteckningen B A för A B. Varför? Tips: Om A och B är ändliga mängder hur många funktioner finns i A B. Övning 2.7 Vilka funktioner finns i mängderna a. N b. Bool Bool c. Bool Bool d. Bool? Övning 2.8 Vilka funktioner i övning 2.7 är injektiva, vilka är surjektiva? Övning 2.9 Vad är ran(f g)? Övning 2.10 Visa att kompositionen av två surjektiva (injektiva) partiella funktioner är surjektiv (injektiv).

28 2.9. MATEMATISKA FUNKTIONER Funktioner från naturliga tal Vi ska nu se att bevisprincipen matematisk induktion ger upphov till en metod att definiera totala funktioner från mängden av naturliga tal till en godtycklig mängd. Som vi såg i föregående avsnitt, får vi en total funktion f N A genom att till varje naturligt tal n tillordna ett värde i A. Ett sätt att göra denna tillordning går till på följande sätt: 1. Bestäm vilket värde som ska tillordnas 0, säg att vi väljer värdet g A. 2. Bestäm vilket värde som ska tillordnas s(n) (dvs. n + 1) i termer av n och f(n). Detta sker genom att definiera en total funktion h N A A och sedan tillordna n + 1 värdet h(n, f(n)). Om vi skriver detta med symboler, får vi: Definition 2.7 Om g A och h N A A är en total funktion, kan vi definiera en ny funktion f N A med primitiv rekursion enligt följande schema: f(0) = g f(s(n)) = h(n, f(n)) Jämför detta schema med den rekursiva funktionsdefinitionen i Pascal: FUNCTION f (n:integer) : A; BEGIN IF n=0 THEN f:=g ELSE f:=h(n-1,f(n-1)) END; Om vi skriver ut funktionens värde för några olika argument får vi: f(0) = g f(1) = f(0 + 1) = h(0, f(0)) = h(0, g) f(2) = f(1 + 1) = h(1, f(1)) = h(1, h(0, g)) f(3) = f(2 + 1) = h(2, f(2)) = h(2, h(1, h(0, g))) Funktionen sum N N, som är sådan att. sum(n) = n

29 22 KAPITEL 2. MÄNGDLÄRA är ett exempel på en funktion som kan definieras med hjälp av primitiv rekursion: Här är g och h valda enligt: sum(0) = 0 sum(n + 1) = sum(n) + n + 1 g = 0 h(x, y) = y + x + 1 Vi förutsätter här att addition redan har definierats. Vi får nu att: sum(0) = 0 sum(1) = sum(0 + 1) = sum(0) = = 1 sum(2) = sum(1 + 1) = sum(1) = = 3 sum(3) = sum(2 + 1) = sum(2) = = 6 Schemat för definition med primitiv rekursion kan utökas till att gälla också funktioner med två argument. Schemat för att definiera f A N B blir: f(x, 0) = g(x) f(x, n + 1) = h(x, n, f(x, n)) där g A B och h A N B B Till exempel kan vi definiera addition och multiplikation enligt: x + 0 = x x + (n + 1) = (x + n) + 1. x 0 = 0 x (n + 1) = (x n) + x En funktion f som kan definieras med primitiv rekursion är inte bara total, den är också beräkningsbar om g och h är beräkningsbara. Om det går att beräkna g och h, kan vi använda metoden i tabellen ovan för att beräkna f(n); f(0) får vi direkt från definitionen, från f(0) får vi sedan f(1) genom att beräkna h(0, f(0)), osv. På detta sätt kan vi fortsätta tills vi kommer fram till f(n). En förutsättning för den här argumenteringen är naturligtvis att vi mekaniskt kan skilja på de två formerna ett naturligt tal kan ha, dvs. 0 och s(n), samt att vi mekaniskt kan få komponenten n ur ett sammansatt objekt s(n). Denna förutsättning är fundamental.

30 2.9. MATEMATISKA FUNKTIONER 23 Vi introducerade ovan primitiv rekursion som en metod att definiera funktioner f A N B. Denna metod att definiera funktioner är mycket viktig när vi nu går vidare och definierar mängden av primitivt rekursiva funktioner, vilken vi betecknar PRF. PRF är en delmängd av mängden av funktioner från tuppler av naturliga tal till naturliga tal; dvs. om f är en funktion f PRF, så gäller det att f N n N, där N 1 = N och N n+1 = N N n. Definitionen av PRF är ytterligare ett exempel på en induktiv definition av en mängd, eftersom PRF definieras av följande klausuler: 1. Den konstanta funktionen zero som alltid ger resultatet 0 tillhör mängden PRF. Det gäller alltså att zero PRF, zero N N och zero(x) = Efterföljarfunktionen tillhör också PRF; Vi har att: succ PRF, succ N N och succ(x) = x Dessutom tillhör alla projektionsfunktioner PRF. En projektionsfunktion projicerar en n-tuppel på en av dess axlar, dvs. selekterar ut en av komponenterna. För alla 1 i n så gäller att: proj n i PRF, proj n i N n N och proj n i (x 1,..., x n ) = x i. 4. Genom att komponera funktioner från PRF, kan vi bilda nya primitivt rekursiva funktioner. Om g, f 1,..., f m PRF, g N m N och f i N n N, så gäller att funktionen compos(g, f 1,..., f m ) tillhör PRF; compos(g, f 1,..., f m ) N n N compos(g, f 1,..., f m )(x 1,..., x n ) = g(f 1 (x 1,..., x n ),..., f m (x 1,..., x n )) 5. Slutligen får vi en funktion i PRF om vi definierar den med hjälp av primitiv rekursion. Om g, h PRF, g N n N och h N n+2 N, så är rec(g, h) en funktion i mängden PRF; rec(g, h) N n+1 N rec(g, h)(x 1,..., x n, 0) = g(x 1,..., x n ) rec(g, h)(x 1,..., x n, y + 1) = h(x 1,..., x n, y, rec(g, h)(x 1,..., x n, y)) Här tillåter vi även fallet n = 0. Då blir g en nollställig funktion, d.v.s. en funktion med noll argument, vilket vi tolkar som att g N. Vi kan efter lite eftertanke se att alla funktioner i mängden PRF är totala och beräkningsbara. Om vi har en funktion f PRF kan vi, genom att tillämpa de olika klausulerna ovan, mekaniskt beräkna värdet av f(n 1,..., n m ).

31 24 KAPITEL 2. MÄNGDLÄRA Det är kanske inte så lätt att se hur man skall använda de induktiva klausulerna för att visa att en funktion tillhör PRF, så låt oss se på ett exempel. Exempel: Vi visar att addition är en primitivt rekursiv funktion. Redan tidigare visade vi att vi kan skriva addition på följande sätt: add N 2 N add(x, 0) = x add(x, y + 1) = s(add(x, y)) Det är uppenbart att vi måste använda primitiv rekursion (klausul 5) för att konstruera add. Vi får: add(x, y) = rec(g, h)(x, y) där g(x) = x och h(x, y, z) = s(z) För att vara säker på att add tillhör PRF, måste vi visa att både g och h tillhör PRF. g är identitetsfunktionen, dvs. projektionsfunktionen för 1-tuppler, g = proj 1 1 Funktionen h är lite besvärligare, den är en komposition av efterföljarfunktionen och projektionsfunktionen som väljer ut det tredje elementet i en trippel. Vi får att: h = compos(succ, proj 3 3) eftersom h(x, y, z) = s(z) = s(proj 3 3(x, y, z)) = compos(succ, proj 3 3)(x, y, z) enligt klausul 3 respektive klausul 4 i definitionen av PRF ovan. add är alltså lika med rec(proj 1 1, compos(succ, proj 3 3)) och den tillhör PRF. Övning 2.11 Definiera funktionen pred (predecessor) med hjälp av primitiv rekursion. Det skall gälla att pred(n) är lika med 0 om n = 0 och lika med n 1 annars. Vad är g och h? Övning 2.12 Samma sak för n! (=1 om n = 0, och = n annars) och iszero(n) (=1 om n = 0, och =0 annars). Övning 2.13 Ge en primitivt rekursiv definition av

32 2.9. MATEMATISKA FUNKTIONER 25 a. x n b. x. n (= x n om x>n, 0 annars) Övning 2.14 Definiera med hjälp av tidigare definierade funktioner : a. eq N N N {0, 1} (eq N (x, y) = 1 om x = y N och 0 annars) b. max N N N och min N N N Övning 2.15 Definiera x mod y (resten vid division av x med y) och x div y (heltalsdivision) med hjälp av primitiv rekursion och tidigare definierade funktioner. Övning 2.16 Definiera med primitiv rekursion: a. Funktionen N (N N) N med definition (n, f) = { 1, om det finns ett x n sådant att f(x) 1 0, annars. b. N (N N) N med definition { 1, om f(x) 1 för varje x n (n, f) = 0, annars. Övning 2.17 Bevisa att sum är en total funktion. Visa också att varje funktion som kan definieras med primitiv rekursion är total om g och h är totala funktioner. Övning 2.18 Visa att följande funktioner tillhör PRF: a. föregångarfunktionen, pred(x). b. multiplikation, mult(x, y). Övning 2.19 Vad gör den primitivt rekursiva funktionen: rec(0, compos(add, compos(succ, proj 2 1), proj 2 1)) N N

33 26 KAPITEL 2. MÄNGDLÄRA Funktioner från Bool På samma sätt som bevismetoden matematisk induktion gav upphov till en metod att definiera funktioner (primitiv rekursion), så ger fallanalys för sanningsvärden upphov till en metod att definiera totala, beräkningsbara funktioner från Bool. Antag att vi vill definiera en total funktion f Bool A, då kan vi använda följande metod: 1. Definiera vilket värde som ska tillordnas false, säg att vi väljer g A. 2. Definiera vilket värde som ska tillordnas true, säg att vi väljer h A. I symboler får vi följande schema: f(false) = g f(true) = h Övning 2.20 Definiera följande funktioner: a. not Bool Bool b. if then else Bool A A A c. and Bool Bool Bool Funktioner från listor Vi får också för listor en motsvarighet till att definiera funktioner med primitiv rekursion över de naturliga talen. För listor kan vi definiera en funktion f List(A) B på följande sätt: 1. Bestäm vilket värde som ska tillordnas nil, säg att vi väljer g B. 2. Bestäm vilket värde som ska tillordnas a.l (dvs. cons(a, l)) genom att ge en funktion h A List(A) B B. Värdet som tillordnas a.l blir h(a, l, f(l)). Detta definitionsschema kallar vi för primitiv rekursion över listor och vi skriver det på följande sätt: f(nil) = g f(a.l) = h(a, l, f(l)) Längdfunktionen för listor, length List(A) N, definieras med primitiv rekursion över listor på följande sätt: dvs. g = 0 och h(x, y, z) = 1 + z. length(nil) = 0 length(a.l) = 1 + length(l)

34 2.9. MATEMATISKA FUNKTIONER 27 Övning 2.21 Introducera metoder att definiera funktioner från a. disjunkta unioner b. heltal motsvarande metoderna att definiera funktioner från naturliga tal och listor som introducerades tidigare. Övning 2.22 Ger de här metoderna beräkningsbara funktioner? Varför (inte)? Övning 2.23 Definiera en funktion dd Int N som fördubblar sitt argument om det är större än 0 och kvadrerar det annars. Övning 2.24 a. Definiera en funktion ddlist List(Int) List(N) som applicerar funktionen dd på alla element i en heltalslista. b. Definiera en funktion sumlist List(N) N som ger summan av alla tal i en lista. Vad är g och h? Övning 2.25 Utöka metoden att definiera funktioner med primitiv rekursion över listor till att också gälla funktioner f A List(B) C. Använd sedan metoden för att definiera en funktion som konkatenerar (slår samman) två listor. Övning 2.26 Definiera en funktion reverse List(A) List(A) som vänder på en lista. Övning 2.27 Formulera en metod att definiera funktioner från mängden av binära träd som introducerades i övning 2.5 (sid 14). Använd sedan metoden att definera en funktion depth BinT ree N som ger det maximala djupet (höjden?) av argumentet. Övning 2.28 Ge en induktiv definition av mängden PRF List (mängden av primitivt rekursiva funktioner över listor av naturliga tal). PRF List motsvarar mängden PRF men är definierad över listor istället för över naturliga tal. Övning 2.29 Låt oss införa följande schema att definiera funktioner: f(nil) = g f(a.l) = h(a, f(l)) Kan man definiera samma funktioner med detta schema som man kan med primitiv rekursion över listor?

35 28 KAPITEL 2. MÄNGDLÄRA

36 Kapitel 3 BERÄKNINGSBARA FUNKTIONER Det som vi ska sysselsätta oss med i detta kapitel berör gränserna för vad som över huvudtaget är möjligt att utföra med hjälp av en dator (eller annan beräkningsmaskin). En första tanke är kanske att alla välspecificerade problem går att lösa med hjälp av en datamaskin, men så är inte fallet. Vi börjar i avsnitt 3.1 att med hjälp av begreppet uppräkningsbar mängd visa att det måste finnas funktioner som inte är beräkningsbara. Vi fortsätter sedan med att precisera begreppet beräkningsbar funktion och kan efter en utflykt bland programmeringsspråkens syntax och semantik explicit definiera några funktioner som inte går att beräkna. 3.1 Uppräkningsbara mängder Begreppet uppräkningsbar mängd används för att beteckna att en mängd har ett visst antal element. Intuitivt är en mängd A uppräkningsbar om den inte har fler element än mängden av naturliga tal. Vi skall i detta avsnitt visa hur man genom ett ganska enkelt resonemang kan visa att det måste finnas funktioner i mängden N N som inte kan beräknas av ett program i t.ex. Pascal. Vi börjar med att definiera vad en uppräkningsbar mängd är. Definition 3.1 En mängd A är uppräkningsbar (eng. countable) om A = eller om det finns en total surjektiv funktion f N A, dvs. en funktion som avbildar de naturliga talen på hela A. Funktionen f ger alltså till varje tal n ett element f(n) A på ett sådant sätt att varje element i A är bild av åtminstone ett n N. Vi säger att f räknar upp elementen i A eftersom f ger en uppräkning: f(0), f(1), f(2),..., där varje element 29

37 30 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER i A förr eller senare kommer med. För att visa att en mängd är uppräkningsbar gäller det alltså att hitta en total surjektiv funktion från N till denna mängd. Eftersom identitetsfunktionen (id N N N) är surjektiv, följer t.ex. att N själv är uppräkningsbar. En alternativ bevismetod är att visa att det finns en total injektiv funktion g A N, dvs. en funktion som tillordnar varje element a A ett unikt tal g(a) N. Att detta fungerar kan vi se av följande sats. Sats 3.2 Mängden A är uppräkningsbar om och endast om det finns en total injektiv funktion g A N. Bevis: ( ) Antag att A är en uppräkningsbar mängd. Om A =, kan vi välja g = Λ N som är total och trivialt injektiv. Om A, finns enligt definitionen av uppräkningsbar mängd en total surjektiv funktion f N A och enligt sats 2.4 finns det då också en total injektiv funktion g A N. ( ) Antag att g A N är en total injektiv funktion. Om A = så är A uppräkningsbar. Om A finns enligt sats 2.4 en total surjektiv funktion f N A. Alltså är A uppräkningsbar. När vi nu har klarat av definitionen av vad som menas med en uppräkningsbar mängd kan vi bevisa följande sats: Sats 3.3 List(N) är en uppräkningsbar mängd. Bevis: Vi skall bevisa satsen genom att konstruera en injektiv funktion f List(N) N. Detta innebär att vi till varje lista av naturliga tal skall tillordna ett unikt tal n N. Vi gör detta på följande sätt: Aritmetikens fundamentalsats säger att varje heltal större än 2 har en unik primtalsuppdelning. Vi väljer därför talet n enligt följande: 1. Om listan är tom tillordnar vi den talet Om listan har utseendet [a 1, a 2,..., a m ] så tillordnar vi den talet n där: och där p m är det m:te primtalet. n = 2 a a 2+1 p am+1 m Eftersom ett heltal inte kan ha två olika primtalsuppdelningar kan två olika listor aldrig tilldelas samma tal.

38 3.1. UPPRÄKNINGSBARA MÄNGDER 31 Vi går vidare med att ge ett exempel på en mängd som inte är uppräkningsbar. Exemplet är mängden av alla totala funktioner från N till N. Denna mängd innehåller för många element för att vara uppräkningsbar. Detta ges av följande sats: Sats 3.4 Mängden av totala funktioner från N till N, dvs. mängden N N, är inte uppräkningsbar. Bevis: Antag motsatsen, dvs. att N N är uppräkningsbar, och härled en motsägelse från detta. Eftersom N N enligt antagandet är uppräkningsbar, finns en total surjektiv funktion f N (N N). Det går därför att definiera h N N på följande sätt: h(x) = (f(x))(x) + 1 Observera den dubbla applikationen; funktionen f tillhör mängden N (N N) så om vi applicerar f på x N får vi som resultat en funktion i mängden N N, som i sin tur kan appliceras på x N. Funktionen h är en total funktion från N till N och den borde därför finnas med någonstans i den uppräkning som ges av f. Det måste därför finnas ett n N, för vilket f(n) = h Men det är lätt att visa att h är skild från varje funktion i den uppräkning som ges av f: h skiljer sig från f(0) för argumentet 0 eftersom h(0) = (f(0))(0) + 1 (f(0))(0) h skiljer sig från f(1) för argumentet 1 eftersom h(1) = (f(1))(1) + 1 (f(1))(1) h skiljer sig från f(2) för argumentet 2 eftersom h(2) = (f(2))(2) + 1 (f(2))(2). h skiljer sig från f(n) för argumentet n eftersom h(n) = (f(n))(n) + 1 (f(n))(n) h kommer därför inte med någonstans i uppräkningen, och vi får en motsägelse eftersom f skulle räkna upp alla element i mängden N N. Antagandet måste därför vara falskt och N N kan inte vara uppräkningsbar.

39 32 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER Metoden som används i beviset brukar kallas Cantors diagonalmetod och den kan användas vid många liknande tillfällen. Namnet blir kanske mer naturligt om vi skriver upp funktionerna som en lista av tal med värdet för argumentet 0 först, sedan värdet för argumentet 1, och så vidare. f(0) = (f(0))(0) (f(0))(1) (f(0))(2) (f(0))(3)... f(1) = (f(1))(0) (f(1))(1) (f(1))(2) (f(1))(3)... f(2) = (f(2))(0) (f(2))(1) (f(2))(2) (f(2))(3)... f(3) = (f(3))(0) (f(3))(1) (f(3))(2) (f(3))(3).... h = (f(0))(0) + 1 (f(1))(1) + 1 (f(2))(2) + 1 (f(3))(3) Funktionen h skiljer sig från f(n) eftersom de har olika värden för argumentet n, och funktionsvärdet (f(n))(n) finns ju alltid i diagonalen på uppräkningen. Vi har visat att det finns fler totala funktioner från N till N än det finns naturliga tal. Men hur många partiella funktioner finns det då? Svaret blir en enkel konsekvens av följande sats: Sats 3.5 Om A är en uppräkningsbar mängd och B är en delmängd av A, så är också B uppräkningsbar. Bevis: Om B = är B uppräkningsbar; om B finns ett element b B och eftersom A är uppräkningsbar och skild från finns en total surjektiv funktion f A N A. Definiera nu f B N B enligt: { fa (x), om f f B (x) = A (x) B b, annars. f B är surjektiv eftersom f A är surjektiv och B A. Alltså är B en uppräkningsbar mängd. Mängden av partiella funktioner från N till N är alltså inte heller den uppräkningsbar. För om N N skulle vara uppräkningsbar så skulle också dess delmängd N N vara uppräkningsbar, vilket enligt sats 3.4 inte är fallet. Men då kan inte heller N N vara uppräkningsbar. Korrolarium 3.6 Mängden av partiella funktioner från N till N, dvs. N N, är inte uppräkningsbar Det finns alltså fler partiella funktioner på N än det finns naturliga tal. Hur många program finns det då? Vi ger svaret på den frågan genom att bevisa en generalisering av satsen som säger att List(N) är en uppräkningsbar mängd. Från att bara gälla listor av naturliga tal generaliserar vi den till att gälla listor av element ur en godtycklig uppräkningsbar mängd.

40 3.1. UPPRÄKNINGSBARA MÄNGDER 33 Sats 3.7 Om A är en uppräkningsbar mängd så är List(A) uppräkningsbar. Bevis: Vi börjar med att antaga att att A är uppräkningsbar; Om A = är List(A) = {nil} och då naturligtvis uppräkningsbar. Om A finns det en surjektiv funktion f N A och därför, enligt sats 2.4, finns det en total injektiv funktion g A N. Definiera nu h List(A) N på följande sätt: h([a 1,..., a m ]) = 1 2 g(a 1)+1 p g(am)+1 m (m 0) Eftersom g är injektiv och två olika heltal inte kan ha samma primtalsuppdelning (aritmetikens fundamentalsats) kommer två olika listor aldrig att avbildas på samma heltal; h är alltså injektiv och List(A) är uppräkningsbar. Efter denna långa sekvens av satser och bevis har vi nu kommit fram till en punkt där vi kan tillämpa satserna på vårt datalogiska problem. Om vi gör antagandet att ett program är en ändlig sekvens av tecken, så ser vi att mängden av program, som vi kallar Prog, är uppräkningsbar. Mängden av tecken, Char, är ju ändlig och därför uppräkningsbar och Prog List(Char). Vi har visat: Korrolarium 3.8 Om ett program är en ändlig sekvens av tecken, så är mängden av program uppräkningsbar. Under det inte speciellt starka antagandet att ett program bara består av en ändlig sekvens av tecken har vi visat att det inte finns tillräckligt många program för att de ska räcka för en jämförelse med de partiella funktionerna. Än så länge har vi bara jämfört storleken av mängderna och från resultatet av den jämförelsen kommer nästa fråga: Om vi låter varje program motsvaras av den partiella funktion som avbildar indata på utdata, vad är det då för partiella funktioner som inte motsvaras av något program? För att kunna besvara den frågan måste vi definiera, för det första, vad vi menar med ett program, och för det andra, vilken partiell funktion som ska associeras med ett givet program. Dessa två frågor är utgångspunkt för resten av detta kapitel. Övning 3.1 Visa att varje ändlig mängd är uppräkningsbar. Övning 3.2 Visa att N N är en uppräkningsbar mängd. Övning 3.3 Visa att om A är uppräkningsbar och det finns en bijektion mellan A och B, så är också B uppräkningsbar. Övning 3.4 Varför kan man inte koda en lista av naturliga tal [a 1,..., a m ], med talet n = 1 2 a1 p am m?

41 34 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER 3.2 En beräkningsmodell programspråket P Vårt första problem är hur Prog, mängden av alla program, skall definieras. Ett sätt vore att definiera den som mängden av alla program i alla programmeringsspråk. Men det skulle ju bli totalt meningslöst, eftersom man aldrig skulle kunna veta vad som inte tillhörde mängden. Ett bättre sätt vore att se vad som är gemensamt för alla programmeringsspråk och försöka finna ett språk i vilket alla program åtminstone skulle kunna simuleras. Med andra ord, att försöka finna en modell för alla program. Vi skall välja det senare sättet att definiera Prog, men vilket språk skall vi välja? En programmerare skulle antagligen direkt välja sitt favoritspråk; Pascal, Lisp, ml eller vad det nu kan vara. Men vårt mål är inte att använda språket till att skriva program som löser en speciell uppgift, utan att studera egenskaper och begränsningar för program i allmänhet. Det är därför viktigt att språket är så enkelt som möjligt. En viktig sak som vi vill göra är att formellt definiera vilken partiell funktion i mängden N N som varje program beräknar. Att göra detta för ett programspråk som Pascal skulle ta alltför lång tid, om vi över huvudtaget skulle klara av det. Ett absolut krav är därför att språket skall vara så litet som möjligt och inte innehålla några onödiga konstruktioner; vi skall bara behandla beräkningar med naturliga tal och därför är till exempel alla andra datatyper onödiga. Förutom att det är nödvändigt med ett litet språk av de skäl som nämnts, så är det teoretiskt intressant att veta vilka konstruktioner i ett språk som är nödvändiga och vilka som kan uttryckas med hjälp av andra. Man ser snart att de krav vi har ställt upp inte uppfylls av något vanligt programmeringsspråk. Om man tittar tillbaka, så har många beräkningsmodeller föreslagits, de första redan på 1930-talet, till exempel λ-kalkyl (A. Church), rekursiva funktioner, som föreslogs av S.C. Kleene, Turing maskiner (A. Turing) och symbolbehandlingssystem (E. Post, A.A. Markov). Alla dessa skiljer sig dock ganska väsentligt från dagens programmeringsspråk. Vi definierar därför en helt ny beräkningsmodell som består av ett programmeringsspråk som vi kallar P. En av anledningarna till att vi definierar en egen beräkningsmodell, som liknar dagens programmeringsspråk, är att själva definitionen av språket ger en inblick i hur man formellt kan definiera ett programmeringsspråk. Även om vi nu definierar en egen beräkningsmodell, skall vi i ett senare avsnitt komma tillbaka till de beräkningsmodeller som nämnts ovan. När vi definierar språket P, skall vi göra det i två steg: 1. Syntax, dvs. vilka sekvenser av tecken som är välformade (syntaktiskt korrekta P-program). 2. Semantik, dvs. vad de syntaktiskt korrekta programmen betyder. Syntaxen kommer vi som vanligt att beskriva med hjälp av en BNF-grammatik och semantiken kommer vi att definiera med hjälp av en operationell beskrivning. Den

42 3.2. EN BERÄKNINGSMODELL PROGRAMSPRÅKET P 35 operationella beskrivningen kommer att bestå av regler som ger betydelsen av ett program genom att definiera dess värde för varje indata. Men innan vi specialiserar oss på språket P, skall vi studera vilka metoder som finns att definiera syntaxen för ett godtyckligt språk.

43 36 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER 3.3 Definition av programspråk syntax När man studerar studerar språk (naturliga språk) brukar man dela in området i tre olika delar: 1. Syntax, det som handlar om formen av fraser (uttryck, meningar) (eng. sentences, strings, words) i språket, dvs. vad som är en välformad fras. 2. Semantik, det som handlar om betydelsen av fraserna i språket. 3. Pragmatik, det som handlar om användningen av språket. När det gäller programmeringsspråk studerar man nästan uteslutande de två första delarna. Dessutom är gränslinjen mellan vad som är syntax och vad som är semantik inte helt klar. Typfel, som i uttrycket kent + 33, räknas till exempel som ett syntaktiskt fel i Pascal, men inte i Lisp! En princip som när det gäller programmeringsspråk brukar styra vad som räknas till syntax respektive semantik är att syntaktisk korrekthet skall vara mekaniskt avgörbar, t.ex. med hjälp av ett program (en syntaxanalysator) Grammatiker De minsta enheterna i ett språk kallar vi för symboler eller tecken (eng. symbols, tokens, characters) och den icke-tomma, ändliga mängden av dessa kallas för ett alfabet (eng. alphabet). Om vi betraktar ett språk från en syntaktisk utgångspunkt, så är ett språk över ett alfabet inget annat än en delmängd av alla listor över alfabetet. För att beskriva denna delmängd, som i alla intressanta fall är oändlig, använder vi en grammatik (eng. grammar). När man sysslar med syntax och grammatiker använder man ofta ett något annorlunda beteckningssätt än det som vi tidigare har introducerat. För listor (sekvenser) använder man följande notation: Istället för List(A) skriver man A och mängden av icke-tomma listor över A, dvs. {l List(A) l nil}, betecknas A +. Dessutom använder man ɛ, λ eller Λ för den tomma strängen, dvs. istället för nil, utesluter hakparenteserna runt en lista samt kommatecknet mellan elementen, dvs. man skriver listan [a 1, a 2,..., a n ] som: a 1 a 2... a n Detta medför att man får samma beteckning på ett element som på listan som bara består av detta elementet, dvs. det gäller att om a A så a A (och a A + )

44 3.3. DEFINITION AV PROGRAMSPRÅK SYNTAX 37 Man brukar inte heller använda någon speciell beteckning för att konkatenera (slå ihop) två strängar. Om α och β är två strängar så betyder α β helt enkelt sammanslagningen av α och β. Dessa beteckningar är naturligtvis bekväma att använda, men ganska farliga, speciellt när man är ovan. Slutligen används beteckningen α för längden av en lista (sträng) α. Ett exempel kan vara på sin plats. Exempel: Om Σ = {0, 1}, så är Σ = {ɛ, 0, 1, 00, 01, 10, 11, 000,...} Σ + = {0, 1, 00, 01, 10, 11, 000,...} och om α = 00 och β = 11, så blir α β = 0011 och α β = 4. Nog med beteckningar. Vårt mål är att införa ett sätt att definiera syntaxen för ett språk och för att göra detta använder vi en grammatik. Man kan se en grammatik som en matematisk maskin som genererar alla fraser i ett språk. Formellt består en grammatik av 4 delar: 1. En ändlig mängd av terminala symboler (eng. terminal symbols), V T. 2. En ändlig mängd av icke-terminala symboler (eng. nonterminal symbols), V N, sådana att V T V N =. 3. En speciell icke-terminal symbol S V N som kallas startsymbolen. 4. En ändlig mängd O av produktioner (eller omskrivningsregler) (eng. productions, rewriting rules) med utseendet α β där α är en icke-tom sträng över V och β är en sträng över V, dvs. α V + och β V där V = V N V T. En grammatik G är alltså en fyrtuppel V N, V T, S, O. Men det är språk som vi är intresserade av, så nu måste vi beskriva vilket språk en grammatik genererar. För att göra detta definierar vi först vad en härledning är. Låt oss antaga att vi har en grammatik G = V N, V T, S, O. Om vi då har en sträng γ α ρ, där γ, ρ V och α V +, samt en produktion α β O, så är γ α ρ γ β ρ en enstegs härledning (eng. immediate derivation). Vi säger att produktionen α β har applicerats på strängen γ α ρ för att ge γ β ρ. En härledning, eller en flerstegshärledning, (eng. derivation) av β från α är en sekvens av enstegshärledningar α α 1

45 38 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER som vi oftast skriver: α 1 α 2. α n β α α 1 α n β. Vi inför relationen α β mellan strängar för att beteckna att vi kan härleda β från α i 0 eller flera steg. Relationen α + β betecknar att vi kan härleda β från α i ett eller flera steg. Så det gäller alltid att α α, men inte alltid att α + α. Med matematisk terminologi är + det transitiva höljet av och det reflexiva, transitiva höljet av. Om vi har en grammatik G V N, V T, S, O så definierar den ett språk L(G), där L(G) innehåller alla fraser som kan härledas från startsymbolen i grammatiken. Så β L(G) om och endast om β VT och S β där S är startsymbolen. Med andra ord L(G) = {β V T : S β} Vi säger att L(G) är ett språk över alfabetet V T eftersom alla fraser i L(G) består av tecken från V T. Notera att två grammatiker G 1 och G 2, kan generera samma språk, trots att grammatikerna är olika. Det kan alltså gälla att L(G 1 ) = L(G 2 ), trots att G 1 G 2. Exempel: där Som ett första exempel på en grammatik ta: V N = {A, S} H = V N, V T, S, O H V T = {a, b, c} Denna grammatik genererar språket O H = {S AaS, S A, A bc} L(H) = {bc, bcabc, bcabcabc,...} Vi kan till exempel härleda strängen bcabc på följande sätt: S AaS bcas bcaa bcabc En härledning presenteras ofta i form av ett träd (härledningsträd, parse -träd). Härledningen ovan kan presenteras i form av trädet i figur 3.1.

46 3.3. DEFINITION AV PROGRAMSPRÅK SYNTAX 39 A S S A bc a bc Figur 3.1: Härledningsträd för bcabc Exempel: Som ett exempel på en mer komplicerad grammatik tar vi G = V N, V T, S, O G där: V N = {S, B, C} V T = {a, b, c} O G = {S asbc, S abc, CB BC, ab ab, bb bb, bc bc, cc cc} Den här grammatiken genererar språket L(G) VT som består av alla strängar av formen a n b n c n, dvs. n stycken a:n följda av n stycken b:n och n stycken c:n. Vi kan till exempel härleda strängen aabbcc på följande sätt: S asbc aabcbc aabbcc aabbcc Chomsky hierarkin aabbcc aabbcc aabbcc En grammatik är ett mycket kraftfullt verktyg för att definiera ett språks syntax, långt mycket kraftfullare än vad vi behöver för programmeringsspråk. Med hjälp av en grammatik kan vi till exempel definiera språk som det inte går att göra mekaniska syntaxanalysatorer för, dvs. det finns grammatiker som genererar så

47 40 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER svåra språk att man inte mekaniskt kan avgöra om en sträng tillhör språket eller ej. Det är därför viktigt att införa andra, mindre kraftfulla, kategorier av språk. Beroende på hur produktionerna i en grammatik ser ut, så delar vi in grammatikerna i följande klasser (Chomsky hierarkin): typ 0: En typ 0 grammatik har inga restriktioner. typ 1: Om det för varje produktion α β i grammatiken gäller att α β, dvs. att vänstersidan inte är längre än högersidan, så kallar vi grammatiken för en typ 1 grammatik, eller en sammanhangsberoende grammatik (eng. context-sensitive). Om startsymbolen inte förekommer i någon produktions högerled, så tillåter vi dessutom produktionen S ɛ. typ 2: Om det för varje produktion α β gäller att α V N, så har vi en typ 2 grammatik, eller en sammanhangsoberoende grammatik (eng. contextfree). typ 3: Om alla produktionerna har formen A ab, A a eller A ɛ där A och B är icke-terminala symboler och a en terminal symbol, så har vi en typ 3 eller en reguljär grammatik (eng. regular grammar). Ett alternativt sätt att formulera restriktionen för sammanhangsberoende grammatiker är att kräva att varje produktion skall ha utseendet α A β α γ β där α, β V, γ V + och A V N. Resultatet blir inte samma mängd av grammatiker, men de två mängderna generar precis samma klass av språk. Dessutom förstår man kanske bättre varför grammatikerna kallas sammanhangsberoende; den icke-terminala symbolen A kan ju bara producera strängen γ i sammanhanget (omgivningen) α β. När vi använder Chomskys uppdelning på språk istället för grammatiker, betyder det bara att vi kallar ett språk som genereras av en typ X grammatik för ett typ X språk. Vi får följande relation mellan de olika språkmängderna {typ-3 språk} {typ-2 språk} {typ-1 språk} {typ-0 språk} När man säger att ett språk är ett typ-1 språk menar man oftast att det finns en typ-1, men ingen typ-2 grammatik, som genererar språket. Programmeringsspråk är i de flesta fall sammanhangsberoende (typ-1). I Pascal är till exempel satsen

48 3.3. DEFINITION AV PROGRAMSPRÅK SYNTAX 41 x := 25 bara syntaktiskt korrekt om den föregås (står i ett sammanhang) av en deklaration VAR x : integer Tyvärr är det näst intill omöjligt att formulera en grammatik som tar hänsyn till de sammanhangsberoende delarna av ett programmeringsspråk som Pascal. Därför beskriver man oftast ett programmeringsspråks syntax i två steg. Först ger man en sammanhangsoberoende grammatik och sedan lägger man till de sammanhangsberoende restriktionerna. Dessa uttrycks ofta i form av kommentarer formulerade i naturligt språk. I K. Jensen, N.Wirth: Pascal: user manual and report finns till exempel följande restriktioner uttryckta i naturligt språk: ˆ Expressions which are members of a set must all be of the same type,... ˆ Procedure statements... In the case of a variable parameter, the actual parameter must be a variable... ˆ The expression between the symbols if and then must be of type Boolean. ˆ For statements... The control variable, the initial value, and the final value must be of the same scalar type (or subrange thereof), and must not be altered by the repeated statement. Det är otillfredsställande att det är så svårt att formellt definiera de sammanhangsberoende delarna av ett språk. Flera olika förslag att generalisera begreppet grammatik har framförts för att åtgärda detta, två exempel är tvånivågrammatiker (som användes för att definiera algol 68) och attributgrammatiker. En annan möjlighet man har är att använda de metoder man har utarbetat för att beskriva ett språks semantik. Det som vi här har kallat sammanhangsberoende syntaktiska restriktioner kallas då ofta för statisk semantik. Det finns tyvärr ingen möjlighet att här beskriva hur dessa beskrivningsmetoder fungerar. För vidare information se de böcker som nämns i litteraturhänvisningarna. Betrakta den sammanhangsoberoende grammatiken G = {A, B, S}, {a, b, c}, S, O G där O G = {S Ac, S ab, A ab, B bc}. Den här grammatiken genererar det triviala språket L(G) = {abc}. Mer intressant är dock att strängen abc kan genereras på två helt olika sätt. 1. Dels kan den genereras av härledningen: Se härledningsträdet i figur 3.2. S Ac abc

49 42 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER a A b S c Figur 3.2: Härledningsträd 1 a S b B c Figur 3.3: Härledningsträd 2 2. Dels kan den genereras av härledningen Se härledningsträdet i figur 3.3. S ab abc En sammanhangsoberoende (typ-2) grammatik är tvetydig (eng. ambiguous) om den tillåter att en fras genereras så att den får två olika härledningsträd. Tvetydiga grammatiker skall undvikas så mycket som möjligt. Att man kan härleda en fras så att dem får två olika härledningsträd kan innebära att frasen har två olika betydelser! I den ursprungliga definitionen av algol 60 fanns det en tvetydighet, allmänt kallad the dangling ELSE problem. Grammatiken var formulerad ungefär på följande sätt: <IF-sats> IF <uttryck> <then-del> <IF-sats> IF <uttryck> <then-del><else-del>

50 3.3. DEFINITION AV PROGRAMSPRÅK SYNTAX 43 <then-del> THEN <sats> <else-del> ELSE <sats> En sats som IF b THEN IF b THEN c ELSE c kan i den här grammatiken härledas på två olika sätt, med två helt olika betydelser! Vi lämnar de två härledningarna som övning BNF För att definiera ett programmeringsspråks syntax använder man oftast en variant av typ 2 grammatiker som brukar kallas BNF-form (Backus Normal Form eller Backus Naur Form). Följande konventioner brukar då användas: 1. Icke-terminala symboler skrivs innanför <>-parenteser, t.ex. <program>, <uttryck>. Ett alternativt sätt att skilja icke-terminala och terminala symboler är att skriva de terminala symbolerna innanför -tecken, till exempel: eller att använda olika textstilar if uttryck then sats if uttryck then sats 2. Istället för använder man symbolen ::=. 3. Istället för att skriva flera produktioner med samma vänsterled α ::= β 1 α ::= β 2. α ::= β n så använder man symbolen vilken kan utläsas eller, dvs. man skriver ovanstående produktioner på följande sätt: α ::= β 1 β 2... β n 4. När man har en komponent i en produktion som kan uteslutas, så använder man hakparenteser <if-sats> ::= IF <expr> THEN <sats> [ ELSE <sats> ]

51 44 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER betyder samma sak som <if-sats> ::= IF <expr> THEN <sats> IF <expr> THEN <sats> ELSE <sats> 5. Istället för upprepning med hjälp av rekursion <satslista> ::= <sats> <sats> ; <satslista> använder man symbolen {... }*, dvs. man skriver <satslista> ::= <sats> { ; <sats> }* Ibland, t.ex. i Unix-manualen, skriver man upprepning med hjälp av..., så istället för skriver man <fil-lista> ::= <empty> <filnamn> <fil-lista> <fil-lista> ::= <filnamn> Formella språk Det område där man studerar grammatiker och andra syntaktiska aspekter på språk kallas för Formella Språk. Några av de frågor man studerar där är: ˆ Givet ett språk, vilken klass tillhör det? ˆ Är en given grammatik tvetydig? ˆ Om ett språk genereras av en tvetydig grammatik, finns det då en entydig grammatik som genererar samma språk. Språk som bara genereras av tvetydiga grammatiker kallas för fundamentalt tvetydiga (eng. inherently ambiguous). ˆ Går det att skriva en syntaxanalysator till ett givet språk. En syntaxanalysator är ett program som avgör om en sträng α Σ tillhör ett språk L(G) Σ. ˆ Givet en grammatik, kan man generera en syntaxanalysator automatiskt? Jfr. Yacc och Lex i Unix. Vi formulerar några satser som visar vad man kan bevisa om språk och grammatiker.

52 3.3. DEFINITION AV PROGRAMSPRÅK SYNTAX 45 Sats 3.9 Om G är en typ-1 grammatik, så kan man mekaniskt avgöra om en sträng α tillhör L(G) eller ej. Sats 3.10 Om G 1 och G 2 är två godtyckliga sammanhangsoberoende (typ-2) grammatiker, så går det inte att mekaniskt avgöra om L(G 1 ) = L(G 2 ). Sats 3.11 Det går inte att mekaniskt avgöra om en godtycklig sammanhangsoberoende (typ-2) grammatik är tvetydig. Sats 3.12 Det går inte att mekaniskt avgöra, t.ex. med hjälp av ett Pascal program, om ett godtyckligt språk som genereras av en sammanhangsberoende (typ-1) grammatik kan genereras av en sammanhangsoberoende (typ-2) grammatik. Sats 3.13 Det går inte att mekaniskt avgöra om ett sammanhangsoberoende (typ- 2) språk kan genereras av en reguljär (typ-3) grammatik. Sats 3.14 Inget reguljärt språk är tvetydigt. Till slut, det finns andra svårigheter med att definiera programmeringsspråk än de rent syntaktiska; J. Stoy skriver i boken Denotational Semantics: The Scott- Strachey Approach to Programming Language Theory: There is a temptation to become preoccupied with questions in the theory of syntax, partly because there is some well-developed mathematics there to build on (Chomsky grammars, for example). People who succumb can frequently be recognised by there excessive preoccupation with the actual symbols as written. At the extreme, they do not allow one to speak of a program s being wrong : either it is syntactically incorrect, in which case it is not a program at all; or it is a perfectly good program, though perhaps not the one the programmer expected to write. Övning 3.5 Vilket språk genereras av följande grammatik: där {A, B}, {0, 1}, A, R R = {A 0A, A 1B, B 1B, B 1} Vad händer om man lägger till produktionen A 0? Övning 3.6 Definiera en grammatik som genererar språket {β {0, 1} + (β = 0 n 1 n ) & (n 1)}

53 46 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER Härled några olika strängar. Övning 3.7 Formulera en grammatik som genererar språket {β {0, 1} + β innehåller lika många 0:or som 1:or} Härled strängarna , och Rita härledningsträd. Övning 3.8 Utför de två olika härledningarna av IF b THEN IF b THEN c ELSE c som nämndes på sidan 43. Övning 3.9 Vilken språktyp i Chomsky hierarkin tillhör språket i a. övning 3.5 b. övning 3.6 c. övning 3.7

54 3.4. P:S SYNTAX P:s syntax Vi är nu redo att definiera syntaxen för språket P. Den ges av följande BNFgrammatik: <program> ::= PROGRAM ( <variabel> ) <sats> RESULT ( <variabel> ) <variabel> ::= X<naturligt-tal> <naturligt-tal> ::= <siffra> <siffra> <naturligt-tal> <siffra> ::= <sats> ::= <tilldelnings-sats> <while-sats> <sekvens> <tilldelnings-sats> ::= <variabel> := <uttryck> <uttryck> ::= 0 SUCC(<variabel>) PRED(<variabel>) <while-sats> ::= WHILE <villkor> DO <sats> END <villkor> ::= <variabel> 0 <sekvens> ::= <sats> ; <sats> Observera att alla variabler har formen X0, X1, X2, X3,... Siffrorna kallar vi för variabelns index. Språket kan vid en första anblick verka lite torftigt, i synnerhet om man tänker på att det är avsett att klara av alla beräkningar. Det innehåller bara en enda datatyp, de naturliga talen, med konstanten 0 och följande operationer: 1. en operation för att ge efterföljaren till ett naturligt tal (SUCC). 2. en operation för att ge föregångaren till ett naturligt tal (PRED).

55 48 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER 3. en operation som avgör om ett naturligt tal är på formen 0 eller på efterföljarform ( 0). Förutom dessa operationer finns det två olika slags satser, tilldelningssatsen och while-satsen, samt ett sätt att sekventiera satser, det är allt. Det är väl knappast troligt att ens en Basic-programmerare skulle tro att alla beräkningar kan utföras i detta lilla språk. Men vänta ska ni få se!

56 3.5. ABSTRAKT SYNTAX Abstrakt syntax När vi hittills har diskuterat syntaktiska frågor har vi hela tiden lagt stor vikt vid de terminala symbolerna; vi har varit intresserade av det exakta utseendet av programmen (fraserna i språket). När vi är intresserade av program som sekvenser av terminala symboler, säger vi att vi är intresserade av ett språks konkreta syntax. Vid många studier av språk, i synnerhet teoretiska sådana, är vi mer intresserade av strukturen hos fraserna i språket. Jämför följande fragment från ett Pascal, ett C respektive ett Lisp program. FOR i:=1 TO x DO BEGIN y:=sin(x); P(i,y,y) END FOR (i=1; i<=x; i++) { y=sin(x); P(i,y,y); } (do ((i 1 (add1 i))) (null (and (equal i x) (lessp i x))) (setq y (sin i)) (P i y y)) -- Pascal -- C -- Lisp Även om dessa satser inte ser speciellt lika ut, har de (nästan) samma syntaktiska struktur. Det är faktiskt C och Lisp som har mest lika struktur, även om det kanske inte ser så ut! Om vi vill framhäva ett språks struktur, så glömmer vi ofta de terminala symbolerna. Vi definierar istället språket med hjälp av en induktiv definition, såsom vi tidigare definierade till exempel de naturliga talen. Vi kallar en sådan definition språkets abstrakta syntax. Givet en grammatik G motsvarar varje icke-terminal symbol A en mängd fraser; de fraser som genereras från A. Den här mängden av fraser kallas ibland för en syntaktisk kategori. Vi skall nu se hur man så enkelt som möjligt kan definiera de syntaktiska kategorierna i ett språk. I P motsvarar den icke-terminala symbolen <program> den syntaktiska kategorin (mängden av syntaktiska objekt) Prog. Det gäller också att symbolen <sats> motsvarar mängden Sats <uttryck> Uttryck <villkor> Villkor <variabel> Var

57 50 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER Om vi inte är intresserade av precis hur elementen i Prog, Sats, etc. ser ut, så kan vi lika gärna studera mängder Prog, Var, etc., vars element står i en-entydig korrespondens med elementen i Prog, Var etc. Om vi uttrycker detta lite mera formellt, så betyder det helt enkelt att vi lika gärna kan definiera och studera mängder Prog, Var etc. som är sådana att det finns bijektioner mellan Prog och Prog, Var och Var, etc. Det här är en vanlig teknik som används i många sammanhang; istället för att studera en mängd A, så studerar vi mängden A, och eftersom det finns en bijektion f A A motsvarar varje element i A entydigt ett element i A. Varje resultat om elementen i A har därför en precis motsvarighet, via f, för elementen i A. Ofta är bijektionen så naturlig att vi inte ens bryr oss om att formulera den explicit. Eftersom vi naturligtvis vill ha en så enkel definition av Prog, Var etc. som möjligt, definierar vi dem som de fritt genererade mängder som fås från följande klausuler: Var E1: Om n N så var(n) Var. Villkor E1: Om x Var så villkor(x) Villkor. Uttryck E1: 0 Uttryck. E2: Om x Var så succ(x) Uttryck. E3: Om x Var så pred(x) Uttryck. Sats E1: Om x Var och e Uttryck så tilld(x, e) Sats. E2: Om t Villkor och c Sats så while(t, c) Sats. E3: Om c 1 Sats och c 2 Sats så sekv(c 1, c 2 ) Sats. Prog E1: Om x 1 Var, x 2 Var och c Sats så prog(x 1, c, x 2 ) Prog. Elementen i mängderna presenteras ofta i form av träd, abstrakta syntaxträd, till exempel kan prog(var(0), tilld(var(1), 0), var(1)) presenteras i form av trädet i figur 3.4. Vi har nu tre olika sätt att presentera ett P-program: 1. Som en sekvens av terminala symboler: PROGRAM (X0) X1 := SUCC(X0); X2 := PRED(X1) RESULT(X2)

58 3.5. ABSTRAKT SYNTAX 51 var 0 prog tilld var var Figur 3.4: Abstrakt syntaxträd för programmet PROGRAM(X0) X1:=0 RESULT(X1) 2. Som ett element i Prog : prog(var(0), sekv(tilld(var(1), succ(var(0))), tilld(var(2), pred(var(1)))) var(2)) 3. Eller som trädet i figur 3.5. En stor vinst med att arbeta med mängderna Prog, Var etc. är att vi får enkla bevisprinciper för att bevisa att alla program, variabler etc. har en viss egenskap. Vi bryr oss inte om att formulera dessa bevisprinciper här, de formuleras på precis samma sätt som för de fritt genererade mängderna som definierades i avsnitt 2.1. Genom att använda den abstrakta syntaxen kan vi helt bortse från problemet med att översätta ett program från en sträng av tecken till dess logiska struktur. Detta är långt ifrån något enkelt problem och genom att bortse från det kan vi koncentrera oss på andra problem. Att syssla med översättningen från konkret till abstrakt syntax överlåter vi åt dem som sysslar med syntaxanalysatorer och översättarteknik. Eftersom vi har definierat mängderna Prog, Var etc. med hjälp av induktiva definitioner, kan vi formulera metoder för att definiera totala funktioner från mängderna, precis som vi i avsnitt formulerade metoder att definiera funktioner från naturliga tal (primitiv rekursion). En total funktion f Var A tillordnar ett element i A till varje variabel var(n). Detta kan göras med hjälp av en funktion g N A, dvs. istället för att

59 52 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER var 0 prog var tilld succ sekv tilld var pred var 2 1 var 2 var 0 1 Figur 3.5: Abstrakt syntaxträd

60 3.5. ABSTRAKT SYNTAX 53 tillordna ett element i A till varje variabel så tillordnar vi ett element i A till varje naturligt tal. Så om g N A kan vi definiera f på följande sätt: f(var(n)) = g(n) Precis samma metod kan vi använda för villkor. Om g Var A kan vi definiera f Villkor A genom f(villkor(x)) = g(x) Eftersom det finns tre klausuler för att definiera ett uttryck, får vi också tre olika fall när vi ska definiera en funktion f Uttryck A. f(0) = h 1 f(succ(x)) = h 2 (x) f(pred(x)) = h 3 (x) där h 1 A, h 2 Var A och h 3 Var A Eftersom mängden Sats är äkta induktiv, får vi en mer komplicerad metod att definiera en funktion f Sats A. Vi får dela upp definitionen i tre fall: f(tilld(x, e)) = h 1 (x, e) f(while(v, c)) = h 2 (v, c, f(c)) f(sekv(c 1, c 2 )) = h 3 (c 1, c 2, f(c 1 ), f(c 2 )) där h 1 Var Uttryck A, h 2 Villkor Sats A A och h 3 Sats Sats A A A Slutligen kan vi definiera en funktion f Prog A, i termer av en funktion g Var Sats Var A. f(prog(x 1, c, x 2 )) = g(x 1, c, x 2 ) Som ett exempel på en funktion som kan definieras med den här metoden kan vi ta funktionen som givet ett program ger det högsta variabelindexet som förekommer i programmet som resultat. Antag att max N N N redan är definierad. maxindex var (var(n)) = n maxindex villkor (villkor(x)) = maxindex var (x) maxindex uttryck (0) = 0 maxindex uttryck (succ(x)) = maxindex var (x) maxindex uttryck (pred(x)) = maxindex var (x)

61 54 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER maxindex sats (tilld(x, e)) = max(maxindex var (x), maxindex uttryck (e)) maxindex sats (while(v, c)) = max(maxindex villkor (v), maxindex sats (c)) maxindex sats (sekv(c 1, c 2 )) = max(maxindex sats (c 1 ), maxindex sats (c 2 )) maxindex prog (prog(x 1, c, x 2 )) = max(maxindex var (x 1 ), max(maxindex sats (c), maxindex var (x 2 )))

62 3.6. DEFINITION AV PROGRAMSPRÅK SEMANTIK Definition av programspråk semantik Innan vi försöker definiera P:s semantik, skall vi se vilka metoder som finns att beskriva (programmerings-)språks semantik överhuvudtaget. De flesta (alla?) programmeringsspråk som du har kommit i kontakt med hittills har haft semantiken beskriven i naturligt språk. I Pascal rapporten (K. Jensen, N Wirth: Pascal: User Manual and Report) finns till exempel följande text för att definiera betydelsen av några konstruktioner i språket: ˆ The assignment statement serves to replace the current value of a variable by a new value specified as an expression. ˆ The case statement consists of an expression (the selector) and a list of statements, each being labelled by a constant of the type of the selector. It specifies that the one statement be executed whose label is equal to the current value of the selector. De här förklaringarna bygger på att man redan vet vad som menas med begrepp som variabel, värde, konstant, etc. och att man förstår vad som menas med uttryck som a new value specified as an expression. I många sammanhang räcker en sådan här förklaring, men i andra sammanhang är den otillräcklig. Vi kan peka på några skäl till att försöka åstadkomma en mer formell semantisk beskrivning av olika programmeringsspråk: 1. En person som ska implementera språket (skriva en kompilator, interpretator) måste ha en exakt definition av hela språket. En exakt definition är naturligtvis också en hjälp för en vanlig användare av språket. 2. En exakt definition är också utgångspunkt för en programmeringslogik där man kan bevisa att program har olika egenskaper, till exempel att det uppfyller en specifikation. 3. För att diskutera olika konstruktioner i programmeringsspråk och bedöma hur komplexa de är, måste man kunna definiera en konstruktion exakt. 4. Som utgångspunkt (indata) för ett program som automatiskt genererar interpretatorer (och kompilatorer), ungefär som formaliserade syntaktiska definitioner är utgångspunkt för automatiska generatorer av syntaxanalysatorer (t.ex Lex och Yacc). Att exakt definiera meningen av ett vanligt programmeringsspråk är mycket svårt. Vi skall i detta avsnitt bara översiktligt studera några olika metoder att beskriva programmeringsspråks semantik. Vi kommer sedan att ge en exakt definition av

63 56 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER P:s semantik, vilket faktiskt är genomförbart beroende på att P är ett mycket enkelt språk. Ett programmeringsspråk är avsett för att uttrycka program, vilka tillsammans med indata kan exekveras för att ge resultat. Det första sättet att beskriva ett språks semantik är att helt enkelt definiera hur ett (godtyckligt) program exekveras för ett (godtyckligt) indata. Om man uttrycker semantiken på detta sätt, eller från denna utgångspunkt, brukar man säga att man definierar semantiken operationellt, eller att man ger en operationell semantik för språket. Man kan se en operationell semantik som en (abstrakt, implicit) interpretator för språket. Men eftersom man kan använda många olika slags språk och formalismer för att beskriva hur ett program skall exekveras, kan en operationell semantik vara direkt exekverbar medan en annan kan vara skriven i ett språk som inte går att exekvera eller är omöjligt att implementera effektivt. Det mest konkreta man kan göra är naturligtvis att implementera en interpretator i ett annat programmeringsspråk, men detta leder ofta till helt ohanterliga semantiska definitioner. En variant på detta, som kan användas för att reducera komplexiteten av ett språk, är att definiera ett språks semantik i en delmängd av sig själv! När man har gjort detta måste man gå vidare och definiera semantiken för delmängden, men detta är naturligtvis enklare än att försöka definiera hela språket på en gång. I Pascal kan man till exempel definiera semantiken av en del konstruktioner i termer av andra konstruktioner och på så sätt reducera språket till en hanterlig(?) kärna. Semantiken av konstruktionen REPEAT c UNTIL v kan till exempel definieras i termer av sekventiering och while-satsen REPEAT c UNTIL v c ; WHILE not v DO c Historiskt sett är de operationella definitionerna de äldsta; som ett exempel kan man ta beskrivningen av Lisp, som John McCarthy gjorde i början av 60-talet, samt definitionen av pl/1 som gjordes i mitten av 60-talet av ibm i Wien. pl/1:s definition blev helt ohanterlig, den kallades enligt uppgift för Wiens telefonkatalog, men problemen behöver ju inte bara bero på att man använde en operationell semantik. Ytterligare ett exempel på en operationell semantik är Peter Landins försök att beskriva algol 60:s semantik genom att översätta algol 60-program till ett slags λ-uttryck och sedan definiera en maskin för att exekvera dessa. Operationell semantik fick ett mycket dåligt rykte efter arbetet med pl/i, även om det mesta av detta nu är borttvättat. Som en reaktion på problemen med den operationella metoden att definiera programmeringsspråks semantik försökte man finna en mer abstrakt beskrivning av semantiken, och det är inte svårt att förstå att man försökte använda matematiska metoder för att åstadkomma detta.

64 3.6. DEFINITION AV PROGRAMSPRÅK SEMANTIK 57 Vi skall nu se lite på hur detta kan göras. Den metod som vi översiktligt skall beskriva kallas för denotationssemantik, eller matematisk semantik, och kan enkelt beskrivas på följande sätt: Vi tänker oss att definiera ett språks semantik genom att beskriva en funktion för att översätta syntaktiska fraser i språket till syntaktiska fraser i ett annat språk. Om vi tänker på naturliga språk, så skulle vi kunna säga att vi har ett exempel på en semantisk beskrivning enligt denna metod om vi har en översättningsmetod för att översätta engelska fraser till svenska fraser. En sådan metod ger då en semantisk definition av engelska, åtminstone för de som förstår svenska. När det gäller programmeringsspråk, så översätter man programmeringsspråkets konstruktioner till matematiska objekt (matematiskt språk). Man vill helt enkelt översätta de syntaktiska konstruktionerna i programmeringsspråket till de matematiska objekt som konstruktionerna står för (eng. denotes). En mycket viktig egenskap hos semantiska beskrivningar av det här slaget är att semantiken för en sammansatt syntaktisk konstruktion alltid definieras som en komposition av semantiken för de ingående komponenterna. När man började att studera denotationssemantik i slutet av 60-talet fanns det dock ett litet problem. De matematiska objekt som man behövde fanns inte (eller hade inte upptäckts välj själv!). Det är naturligt att låta meningen av t.ex. en funktion i Lisp vara en matematisk funktion. Men i Lisp kan man alltid applicera en funktion på sig själv och detta går inte att göra med vanliga matematiska funktioner. Problemet löstes av Dana Scott, som i slutet av 60-talet konstruerade matematiska objekt med de önskade egenskaperna. Förutom dess betydelse som en grund för denotationssemantik, har Scotts arbete spelat en stor roll för den allmänna förståelsen av programmeringsspråk och beräkningar. En ytterligare metod, som vi kan tänka oss att använda för att definiera ett programmeringsspråks semantik kallas för den axiomatiska metoden. I stort sett går den ut på att man definierar ett formellt logiskt system för att bevisa egenskaper hos olika program. Axiomen och härledningsreglerna i det logiska systemet brukar kallas för bevisregler (eng. proof rules) och metoden har bl.a. använts för att definiera (en delmängd) av Pascal. 1 1 C.A.R Hoare och N. Wirth: An Axiomatic Definition of the Programming Language PAS- CAL, Acta Informatica 2, sid (1973).

65 58 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER 3.7 P:s semantik Låt oss nu se hur vi skall definiera P:s semantik. P-programmet: PROGRAM(X1) c1 ; c2 ;... RESULT(X2) evalueras informellt på följande sätt: Först läses ett indata i form av ett naturligt tal som tilldelas variabeln X1, sedan evalueras satserna c1, c2,... ungefär som i Pascal, och till sist skrivs värdet av variabeln X2 ut. Ett program tar alltså ett naturligt tal som indata och ger ett naturligt tal som utdata. Eftersom ett program kan innehålla satser som till exempel: X0 := 0; X0 := SUCC(X0); WHILE X0 0 DO X0 := SUCC(X0); X0 := PRED(X0) END så är det uppenbart att det finns program i P vars evaluering inte terminerar. Ett program beräknar därför alltid en partiell funktion i N N. Mer om detta senare. För att rigoröst definiera semantiken för språket P, måste vi beskriva betydelsen av varje syntaktiskt riktigt P-program. Vi skall göra det genom att ge en operationell semantik för språket. Detta innebär att betydelsen av varje program förklaras genom en uppsättning beräkningsregler. Genom att tillämpa dessa regler kan vi bestämma resultatet av ett program för ett givet indata, om programmet har något resultat. Reglerna som vi skall använda har antingen formen: F F [1] eller G 1 G 1... G n G n [2] F F Regel [1] betyder att F evalueras till F och [2] betyder att F evalueras till F om G 1 evalueras till G 1,... och om G n evalueras till G n. Vi kallar varje G i G i i [2] för en premiss till regeln och F F för regelns konklusion. För Pascal gäller till exempel intuitivt följande regler: true or false true a true b false a and b false

66 3.7. P:S SEMANTIK 59 Vi definierar resultatet av ett godtyckligt program i P genom att för varje syntaktisk konstruktion i språket ge regler för att bestämma värdet av fraser av den konstruktionen. I allmänhet kan vi dock inte säga vad värdet av en sats är, om vi inte vet vilka värden de olika variablerna står för. I imperativa språk som P har vi dessutom problemet att en variabel kan stå för olika värden vid olika tillfällen under en evaluering. Om vi till exempel skall beskriva vad värdet av en tilldelningssats är, så måste detta ske för en given värdetilldelning till de variabler som finns i satsens högerled. Resultatet blir inte heller ett värde i form av ett naturligt tal eller sanningsvärde, utan resultatet blir en ny värde-variabel tilldelning, som i sin tur kan användas för att beräkna nästa sats. Vi kallar en tilldelning av värden till olika variabler för ett tillstånd (eng. state). Om en variabel är tilldelad ett värde i ett tillstånd σ, så säger vi att variabeln är definierad i σ. Två tillstånd är lika om samma variabler är definierade i båda och om de tillordnar samma värden till varje definierad variabel. Ett tillstånd är därför, matematiskt sett, en partiell funktion från variabler till värden. Vi kommer att låta σ beteckna ett godtyckligt tillstånd samt använda State som beteckning för mängden av tillstånd, som i vårt fall är en mängd partiella funktioner från V ar, mängden av variabler, till N: σ State = Var N För att beteckna ett tillstånd, så använder vi den vanliga mängdbeteckningen, dvs., { variabel 1, värde 1, variabel 2, värde 2,...} Det tillstånd där variablerna X0, X1 och X2 alla har värdet 1 skrivs alltså: { X0, 1, X1, 1, X2, 1 } Det tillstånd som tillordnar alla variabler värdet 0 är av speciellt intresse; vi kommer att använda beteckningen Ω för detta tillstånd. Det gäller alltså att: Ω(x) = 0, för varje variabel x. Vi definierar en funktion på tillstånd som vi kommer att ha stor användning av.

67 60 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER update Var N State State update(x, n, σ) = σ, där σ State, dvs. σ Var N, { n, om x = y σ (y) = σ(y), annars, och dom(σ ) = dom(σ) {x} Istället för update(x, n, σ) kommer vi att använda beteckningen Vi inför också beteckningen σ[x n] σ[x n, y m] istället för (σ[x n])[y m]. Man kan läsa σ[x n] som tillståndet σ förutom att x avbildas på n. För att få värdet av en variabel x som är definierad i ett tillstånd σ, applicerar vi bara tillståndet på variabeln, dvs. σ(x) är värdet av x i σ. Direkt från definitionen av tillstånd kan vi se att det för alla tillstånd σ, naturliga tal n och variabler x gäller att Speciellt gäller för tillståndet Ω att Dessutom gäller det att σ[x n](x) = n Ω[x 0] = Ω σ[x n, x m] = σ[x m] För att beteckna en kombination av en sats c och ett tillstånd σ, använder vi beteckningen: c, σ Samma beteckning använder vi också för ett program p och ett indata n, ett villkor v tillsammans med ett tillstånd σ och ett uttryck e tillsammans med ett tillstånd σ, vilka alltså betecknas: p, n, v, σ respektive e, σ

68 3.7. P:S SEMANTIK 61 Efter dessa förberedelser är vi nu redo för den semantiska beskrivningen av språket P. Vi börjar med att definiera vad värdet av ett program är. Om c är en godtycklig sats, x 1 och x 2 godtyckliga variabler och n ett godtyckligt heltal, så kan vi beskriva alla program-indata kombinationer med uttrycket PROGRAM(x 1 ) c RESULT(x 2 ), n Beräkningsregeln för den syntaktiska konstruktionen <program> är: PROGRAM c, Ω[x 1 n] σ PROGRAM(x 1 ) c RESULT(x 2 ), n σ(x 2 ) [P] där c Sats, x 1 Var, x 2 Var, σ State och n N. I ord får vi: Om resultatet av att evaluera satsen c i tillståndet Ω[x 1 n] blir tillståndet σ, så blir resultatet av att evaluera programmet PROGRAM(x 1 ) c RESULT(x 2 ) med det naturliga talet n som indata, lika med σ(x 2 ), dvs. värdet av variabeln x 2 i tillståndet σ. Tillståndet Ω[x n] som används är alltså det tillstånd där variabeln x har tillordnats talet n medan alla andra variabler har tillordnats värdet 0. Observera att [P] inte är en beräkningsregel, utan ett regelschema som ger betydelsen av alla program. Det är ett regelschema eftersom x 1 och x 2 står för godtyckliga variabler, c för en godtycklig sats och n för ett godtyckligt indata. Genom att välja olika värden för x 1, x 2, c och n kan vi få olika instanser av beräkningsschemat. Om vi till exempel väljer så får vi instansen x 1 = X1, x 2 = X2, c = X2 := 0 n = 34 X2 := 0, Ω[X1 34] σ PROGRAM(X1) X2:=0 RESULT(X2), 34 σ(x2) Att vi använder tillståndet Ω när vi evaluerar satsen i programmet betyder i klartext att alla variabler initieras till 0. I de program vi kommer att skriva skall vi inte utnyttja detta. Anledningen till att vi har gjort på detta sätt, och inte bara sagt att en variabel inte är definierad innan den tilldelats ett värde, är att det i

69 62 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER så fall skulle bli något krångligare att definiera semantiken. En beräkning skulle antingen inte terminera, terminera med ett fel (en oinitierad variabel använd) eller terminera med ett riktigt värde. Genom att starta evalueringen med tillståndet Ω, har vi uteslutit det andra fallet. Det är däremot dålig programmeringsstil att lita på att variabler initieras, då detta inte sker i alla programmeringsspråk (och kanske inte ens i alla implementeringar av samma språk). Vi vet nu värdet av ett godtyckligt program, om vi bara visste värdet av en godtycklig sats. Enligt grammatiken för P är en sats antingen en tilldelningssats, en while-sats eller en sekvens av två satser. Vi behöver därför beräkningsregler för dessa tre fall vi börjar med beräkningsregeln för tilldelningssatser. TILLDELNINGSSATSEN e, σ n x:=e, σ σ[x n] [T] där e Uttryck, n N, σ State och x Var I beskrivningen av hur en tilldelningssats beräknas har vi förutom variabler och update-funktionen på tillstånd, bara använt kunskapen om hur ett uttryck evalueras. Vi fortsätter därför med att definiera beräkningsscheman för uttryck. UTTRYCK 0, σ 0 [E1] SUCC(x), σ s(σ(x)) [E2] PRED(x), σ 0, om σ(x) = 0 [E3] PRED(x), σ n, om σ(x) = s(n) [E4] där σ State, x Var och n N Genom att kombinera [T] och [E1] reglerna kan vi nu bevisa att Beviset går till på följande sätt: X1 := 0, σ σ[x1 0] 0, σ 0 X1 := 0, σ σ[x1 0]

70 3.7. P:S SEMANTIK 63 är en instans av [T]. Denna regel betyder ju att konklusionen är sann om premissen är sann, dvs. om vi kan visa att det som står ovanför strecket gäller, så har vi bevisat att det som står under strecket också gäller. Men premissen är en instans av [E1] och därför sann. Vi fortsätter med beräkningsscheman för while-satsen. WHILE-SATS v, σ false WHILE v DO c END, σ σ v, σ true c, σ σ WHILE v DO c END, σ σ WHILE v DO c END, σ σ där v Villkor, σ State, σ State, σ State och c Sats [W1] [W2] Sedan har vi beräkningsscheman för villkor. VILLKOR x 0, σ true, om σ(x) 0 [V1] x 0, σ false, om σ(x) = 0 [V2] där x Var och σ State Observera att om v, σ alltid evalueras till true, så kan vi aldrig använda reglerna för while-satsen för att bestämma värdet av WHILE v DO c END, eftersom vi hela tiden måste använda regel [W2], och en av premisserna till denna är en ny beräkningsformel för while-satsen. Detta är något luddigt uttryckt, det är kanske enklare att förstå vad som menas med hjälp av ett exempel. För att veta vad evalueras till i omgivningen behöver vi veta, för det första att: och WHILE X1 0 DO X2:=0 END σ = { X1, 1, X2, 0 } X1 0, σ true X2 := 0, σ σ

71 64 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER som vi får direkt från [V1] och från [E1] kombinerat med [T]; för det andra måste vi veta vad WHILE X1 0 DO X2:=0 END evalueras till i omgivningen σ. Men det är ju precis det vi startade med! Vi får alltså aldrig fram något resultat från beräkningsreglerna om testen aldrig ger värdet false, eller, med andra ord, while-satsen har då inget värde. Slutligen har vi beräkningsschemat för sekventiering. SEKVENTIERING c 1, σ σ c 2, σ σ c 1 ;c 2, σ σ [S] där c 1 Sats, c 2 Sats, σ State, σ State och σ State Så om satsen c 1 i tillståndet σ evalueras till σ och satsen c 2 i tillståndet σ evalueras till σ, så evalueras sekvensen c 1 ; c 2 i tillståndet σ till σ. Exempel: Eftersom 0, σ 0 och SUCC(X1), σ[x1 0] s(0) är instanser av [E1] respektive [E2], följer X1 := 0, σ σ[x1 0] och X1 := SUCC(X1), σ[x1 0] σ[x1 s(0)] direkt från [T]. Från dessa följer sedan X1 := 0; X1 := SUCC(X1), σ σ[x1 s(0)] av [S]-regeln. Ett sätt att presentera ett bevis av en beräkningsformel, dvs. att en formel F F är sann, är som en härledning i form av ett träd. A 5 [V2] [E4] [A] [V 1] A 1 A 2 A 4 [S] [W 1] A 6 A 7 [W 2] A 8

72 3.7. P:S SEMANTIK 65 Ett streck betyder att en beräkningsregel har använts, med regelnamnet indikerat längst till höger på strecket. Varje A i i härledningen står för en formel F F och de löv i trädet som betecknas med [A] är antaganden, till exempel A 2 ovan. I en härledning är det tillåtet att introducera godtyckliga antaganden av formen F F. En härledning av den här formen är ett bevis av att den formel som står i roten av trädet är sann, under förutsättning att alla antaganden är sanna och naturligtvis under förutsättning att beräkningsreglerna har använts korrekt. Beviset som gjordes ovan kan skrivas som följande härledning: 0, σ 0 [E1] X1 := 0, σ σ[x1 0] [T ] SUCC(X1), σ[x1 0] 1 [E2] X1 := SUCC(X1), σ[x1 0] σ[x1 1] [T ] X1 := 0; X1 := SUCC(X1), σ σ[x1 1] När vi nu presenterat alla beräkningsregler för P, kan vi formellt definera vilken partiell funktion som ett program beräknar. Definition 3.15 Ett P-program Q beräknar den partiella funktionen P[Q] N N med definition: P[Q](m) = { n, om det finns ett n sådant att Q, m n odefinierad, annars. För att denna definition skall vara meningsfull, måste varje program-indata kombination ha ett unikt värde. Detta behöver inte alltid vara fallet, utan är en konsekvens av de beräkningsregler som vi har valt för P. Den metod som vi har använt för att beskriva semantiken kan också användas för indeterministiska programmeringsspråk. Antag att vi i språket vill inkludera ett uttryck AMB(x 1, x 2 ), där x 1 och x 2 är variabler, och värdet av AMB(x 1, x 2 ) ibland är värdet av x 1 och ibland värdet av x 2. Vi skulle då kunna definiera två beräkningsregler: 1. AMB(x 1, x 2 ), σ σ(x 1 ) 2. AMB(x 1, x 2 ), σ σ(x 2 ) Men då blir inte längre värdet av en program-indata kombination entydigt bestämt. Med hjälp av beräkningsreglerna kan vi till exempel både visa att: och att: PROGRAM(X0) X1 := 0; X2 := AMB(X0, X1) RESULT(X2), 1 0 PROGRAM(X0) X1 := 0; X2 := AMB(X0, X1) RESULT(X2), 1 1 [S]

73 66 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER Vi kommer inte att formellt bevisa att de beräkningsregler som vi valt för P alltid ger ett unikt resultat, utan bara i samband med att vi beskriver en mekanisk metod att finna värdet av en program-indata kombination påpeka att varje steg i vår metod att finna värdet av ett program är entydigt. Under en beräkning kan vi därför aldrig hamna i en situation där vi behöver välja mellan två regler som båda är tillämpbara. Vi kan nu definiera när en funktion är P-beräkningsbar. Definition 3.16 En partiell funktion f N N är P-beräkningsbar om och endast om det finns ett P-program som beräknar den. Och vi kan också formulera en variant av Churchs tes för P-program: En partiell funktion är (intuitivt) beräkningsbar om och endast om den är P-beräkningsbar. Som vi nämnde i inledningen, kan vi aldrig bevisa att varje intuitivt beräkningsbar funktion kan beräknas av ett P-program. Vad vi däremot kan visa är att varje P-program kan evalueras helt mekaniskt. Vi skall därför beskriva hur man mekaniskt kan hitta ett programs resultat för ett givet indata. Observera att beräkningsreglerna inte explicit säger hur ett program skall evalueras, utan bara ger en relation mellan program-indata kombinationer och resultat. För alla som sett och använt Pascal, är det väl helt klart att det till varje P-program finns en maskin som kan evaluera det. Språket P är ju i stort sett en delmängd av Pascal. Den enda större skillnaden är att man i P använder naturliga tal som dataobjekt i stället för heltal. Det finns inte heller någon fixerad övre begränsning av de tal som kan behandlas. Att det oftast finns en övre begränsning på heltalen i Pascal har bara med effektivitet att göra; i våra ML och LISP implementeringar finns till exempel inga sådana begränsningar. Dessutom har vi ju som en förutsättning att beräkningsmaskiner kan vara godtyckligt stora, dvs. ha en godtyckligt stor minneskapacitet, så det finns alltid möjlighet att lagra ett stort tal i minnet. Vi skall ändå ägna en del tid åt att beskriva en metod för att mekaniskt finna ett P-programs värde för ett givet indata. En anledning är att denna metod ger en viss övning i att hantera härledningar, eftersom vi kan se en beräkning av ett P-program som en härledning utan antaganden. Till exempel: SUCC(X0), Ω[X0 3] 4 [E2] X0 := SUCC(X0), Ω[X0 3] Ω[X0 4] [T ] PROGRAM (X0) X0:=SUCC(X0) RESULT(X0), 3 4 [P ] En maskin för ett P-program måste därför, åtminstone implicit, konstruera en härledning av ovanstående form för att bestämma ett programs värde. Vi skall

74 3.7. P:S SEMANTIK 67 informellt se hur man kan göra detta genom att visa hur man mekaniskt finner en härledning för några olika exempel. Idén i dessa exempel får sedan var och en själv översätta till principer för det allmänna fallet. Exempel: Härledningen för exemplet ovan konstrueras på följande sätt: Vi startar nerifrån med det som är känt, dvs. program och indata. Den enda beräkningsregel som vi kan erhålla dessa med är [P], därför kan vi starta med följande: X0 := SUCC(X0), Ω[X0 3]? PROGRAM(X0) X0:=SUCC(X0) RESULT(X0), 3? [P ] Observera att vi ännu inte kan fylla i värdet i högerledet för någon av beräkningsformlerna. Den enda regel som är tillämpbar på premissen är [T], och vi kan därför bygga ännu ett steg uppåt. SUCC(X0), Ω[X0 3]? X0 := SUCC(X0), Ω[X0 3]? [T ] PROGRAM(X0) X0:=SUCC(X0) RESULT(X0), 3? [P ] Men nu kan vi fylla i högerledet på det som står överst, eftersom vi kan tillämpa regel [E2]; det är dessutom även här den enda möjliga regeln. Vi får då: SUCC(X0), Ω[X0 3] 4 [E2] X0 := SUCC(X0), Ω[X0 3]? [T ] PROGRAM(X0) X0:=SUCC(X0) RESULT(X0), 3? [P ] Slutligen kan vi fylla högerleden, dvs. byta ut frågetecknen, på de båda understa beräkningsformlerna. Vi får då den önskade härledningen. SUCC(X0), Ω[X0 3] 4 [E2] X0 := SUCC(X0), Ω[X0 3] Ω[X0 4] [T ] PROGRAM(X0) X0:=SUCC(X0) RESULT(X0), 3 4 [P ] Det här var ett ganska enkelt exempel med en tilldelningssats. Det blir lite svårare med sekvenser, eftersom [S]-schemat har två premisser. Exempel: följande: Om ett program består av en sekvens, så kan vi börja med att fylla i X0 := 0, Ω[X0 3]? X0 := SUCC(X0),?? [S] X0 := 0; X0 := SUCC(X0), Ω[X0 3]? PROGRAM(X0) X0:=0; X0:=SUCC(X0)RESULT(X0), 3? [P ]

75 68 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER Vi måste nu fortsätta med den första premissen för att få fram vilket tillstånd vi skall använda i den andra. Vi får: 0, Ω[X0 3] 0 [E1] X0 := 0, Ω[X0 3] Ω [T ] X0 := SUCC(X0),?? [S] X0 := 0; X0 := SUCC(X0), Ω[X0 3]? PROGRAM(X0) X0:=0; X0:=SUCC(X0) RESULT(X0), 3? [P ] Vi kan då fylla i tillståndet i den andra premissen för att få: 0, Ω[X0 3] 0 [E1] X0 := 0, Ω[X0 3] Ω [T ] SUCC(X0), Ω 1 [E2] X0 := SUCC(X0), Ω Ω[X0 1] [T ] X0 := 0; X0 := SUCC(X0), Ω[X0 3] Ω[X0 1] PROGRAM(X0) X0:=0; X0:=SUCC(X0) RESULT(X0), 3 1 Exempel: Den enda satstyp som vi ännu inte behandlat är while-satsen. Den är ytterligare något knepigare, eftersom det finns två beräkningsscheman, och man inte från while-satsens utseende kan avgöra vilken av reglerna man ska använda. Vi tar ytterligare ett exempel, och börjar med att införa definitionen: W WHILE X0 0 DO X0:=PRED(X0) END och kan sedan starta härledningen med att fylla i följande: W, Ω[X0 1]? PROGRAM(X0) W RESULT(X0), 1? [P ] Vi kan ännu inte avgöra om vi skall tillämpa regel [W1] eller [W2]. Men båda har en beräkningsformel för villkoret som premiss, så vi kan börja med den. [S] [P ] X0 0, Ω[X0 1] true [V 1]? W, Ω[X0 1]? PROGRAM(X0) W RESULT(X0), 1? Nu kan vi avgöra vilken regel vi skall använda, i det här fallet blir det [W2]. Den andra premissen blir en vanlig tilldelningssats som vi redan vet hur vi ska finna [W?] [P ]

76 3.7. P:S SEMANTIK 69 värdet på. Vi får: PRED(X0), Ω[X0 1] 0 [E3] (1) [V 1] X0 := PRED(X0), Ω[X0 1] Ω [T ] W, Ω? W, Ω[X0 1]? PROGRAM(X0) W RESULT(X0), 1? [W 2] [P ] Den tredje premissen är återigen en while-sats och den behandlar vi precis som förra gången. Men nu blir resultatet av villkoret false och vi måste därför använda [W1]regeln. Vi får då: [V 2] X0 0, Ω false (1) (2) W, Ω Ω W, Ω[X0 1] Ω PROGRAM(X0) W RESULT(X0), 1 0 Vi har nu informellt visat hur varje typ av sats, uttryck och villkor skall behandlas. När vi valt vilket schema som har varit tillämpbart har det till slut aldrig funnits något alternativ, och varje ny information som vi fyllt i härledningen har varit entydigt bestämd. En maskin skulle därför kunna utföra uppgiften, vilket vi hade som mål att visa. Vi kan sammanfatta detta på följande sätt: Om vi har ett P-program Q och ett naturligt tal n, så kan vi helt mekaniskt hitta ett tal m och ett bevis av beräkningsformeln Q, n m om det existerar ett sådant tal. Vi kallar ett sådant här bevis för en beräkning av Q, n och använder beteckningen Q, n för denna. I de sammanhang där det klart framgår att vi menar beräkningen av Q, n och inte program-indata kombinationen, utesluter vi... -beteckningen och skriver Q, n. En svårighet som maskinen (eller vi) kan komma i under en beräkning, är följande: När maskinen beräknar en while-sats, blir villkoret aldrig false, dvs. den genererar bara nya premisser hela tiden. Maskinen har då hamnat i en oändlig loop. Den kommer att fortsätta att generera nya premisser i oändlighet och något resultat kommer aldrig att produceras. Om en beräkning på detta sätt hamnar i en oändlig loop, så säger vi att beräkningen divergerar, om motsatsen gäller säger vi att beräkningen konvergerar. [W 1] [W 2] [P ]

77 70 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER Definition 3.17 En beräkning av P-programmet Q med indata n, dvs. Q, n, konvergerar (terminerar) om det existerar ett naturligt tal m sådant att Q, n m. Vi kommer att använda Q, n (eller Q, n ) för att beteckna detta. Om det inte existerar ett sådant naturligt tal säger vi att beräkningen av Q, n divergerar, vilket vi betecknar Q, n (eller Q, n ). Övning 3.10 Gör härledningar som visar att a. X0:=PRED(X0), σ[x0 s(n)] σ[x0 n] b. X0:=0; X0:=SUCC(X0), σ σ[x0 1] c. PROGRAM(X0) X0:=PRED(X0) RESULT(X0), 0 0 d. X0:=0; WHILE X0 0 DO X0:=SUCC(X0) END, σ σ[x0 0] Övning 3.11 Genomför de två härledningarna (se sid 66): a. PROGRAM(X0) X1:=0; X2:=AMB(X0,X1) RESULT(X2), 1 0 b. PROGRAM(X0) X1:=0; X2:=AMB(X0,X1) RESULT(X2), 1 1 Övning 3.12 Om man studerar grammatiken för P, så kan man se att den är tvetydig, dvs. man kan generera en del fraser på två olika sätt. Antag att c 1, c 2 och c 3 är tre satser. Då kan vi generera satsen c 1 ; c 2 ; c 3 på följande sätt: (1) <sats> ::= <sats> ; <sats> ::= c 1 ; <sats> ::= c 1 ; <sats> ; <sats> ::= c 1 ; c 2 ; c 3 (2) <sats> ::= <sats> ; <sats> ::= <sats> ; c 3 ::= <sats> ; <sats> ; c 3 ::= c 1 ; c 2 ; c 3 Visa att semantiskt sett så spelar det ingen roll hur man uppfattar att c 1 ; c 2 ; c 3 har genererats, dvs. visa att värdet blir lika i de båda fallen. Övning 3.13 Går det att definiera P:s syntax med en entydig grammatik? Hur?

78 3.7. P:S SEMANTIK 71 (htbp) <sats> <sats> <sats> <sats> <sats> c 1 c 2 c 3 Figur 3.6: Härledning (1) (htbp) <sats> <sats> <sats> <sats> <sats> c 1 c 2 c 3 Figur 3.7: Härledning (2)

79 72 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER Övning 3.14 Ett alternativt sätt att definiera semantiken för P är att definiera en kortare beräkningsrelation. Istället för att definiera relationen M N med betydelsen: M beräknas till värdet N, kan man introducera en relation M P N som uttrycker att M kan beräknas till N i ett steg. Man definierar denna beräkningsrelation genom att säga hur ett steg i beräkningen utförs. Beräkningen av en tilldelningssats kan till exempel definieras x:= 0, σ σ[x 0] x:= SUCC(y), σ σ[x s(σ(y))] x:= PRED(y), σ σ[x pred(σ(y))] Beräkningen av en sekvens definierar vi för varje möjlig förstakomponent i sekvensen. Här följer definitionen om förstakomponenten är en tilldelningssats: x:= 0;c, σ c, σ[x 0] x:= SUCC(y); c, σ c, σ[x s(σ(y))] x:= PRED(y); c, σ c, σ[x pred(σ(y))] Hur skall beräkningsrelationen definieras för: a. en WHILE-sats, b. en sekvens där första komponenten är en WHILE-sats, c. en sekvens där första komponenten är en sekvens? Det är nu mycket enkelt att definiera vad som menas med en beräkning av en sats, det är bara en sekvens av konfigurationer 2 k 1, k 2, k 3,... som är sådana att k i k i+1. En fullständig beräkning är en beräkning där den sista (terminala) konfiguration inte är relaterad till någon annan konfiguration (dvs. inte kan evalueras längre). Hur skall man definiera en beräkning för ett program och ett indata? Hur ser en terminal konfiguration ut? Vad blir beräkningen av d. PROGRAM(X0) X0:=0 RESULT(X0) för indata 0, e. X0:=SUCC(X0); X0:=PRED(X0) i tillståndet Ω, f. PROGRAM(X0) WHILE X0 0 DO X0:=PRED(X0) END RESULT(X0) för indata 2? Kan en fullständig beräkning ge olika resultat? 2 x:= 0, σ är ett exempel på en konfiguration

80 3.8. FÖRKORTNINGAR Förkortningar Vårt nästa mål är att visa att språket P är minst lika kraftfullt som Pascal, Lisp och ml. Vi skall göra det genom att visa hur ett antal beräkningar kan programmeras i P och hur en del konstruktioner från dessa högnivåspråk kan översättas till P-fraser. För att kunna skriva program i P, utan att hela tiden komma in på smådetaljer, så måste vi ha en möjlighet att bygga upp program modulärt. Vi kommer därför att tillåta oss att definiera förkortningar (makron), som vi sedan använder i programmen. Vi kommer att använda tre slags förkortningar, den första för satser, den andra för uttryck och den tredje för villkor Satsförkortningar En satsförkortning definieras på följande sätt: CMD <förkortning> MACRO: -- <deklarationer> se detta som en kommentar <sats> END Till exempel: CMD ASSIGN (Y1, Y2) MACRO: -- Y1 : Var, Y2 : Var Y1 := SUCC(Y2); Y1 := PRED(Y1) END Om vi vill vara på den säkra sidan med syntaxen för förkortningar, så skulle vi kunna kräva att alla förkortningar skall ha utseendet: <id>(<var>,...,<var>); men vi gör det hela lite informellt och tillåter infix, postfix och kringfix notation för förkortningar samt använder parenteser för gruppering. Vi antar att det finns en syntaxanalys som entydigt kan analysera varje makroanrop och binda aktuella parametrar till motsvarande formella parametrar i makrodefinitionen. Om det är tvetydigt hur parametrarna skall bindas, så gäller regeln: Parametrarna binds från vänster och en parameter binds alltid till en så kort textsträng som möjligt. Om vi till exempel har definitionen och makroanropet EXPR Y1 + Y2 MACRO:... RESULT(Y3) X0 := X1+X2+X3

81 74 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER så binds Y1 till textsträngen X1 och Y2 till X2+X3. Vi kommer också att bruka konventionen att i makrodefinitioner alltid använda variabler med namn Yi, där i är ett tal. Vi kallar variablerna i en makrodefinition för makrovariabler för att skilja dem från variablerna i språket P. Betydelsen av ett makroanrop i ett program fås genom att helt enkelt ersätta anropet med satserna i förkortningens definition. Dessutom skall naturligtvis makrovariablerna i definitionen, dvs. Y1, Y2,... och Yn, textmässigt ersättas med parametrarna i anropet. En variabel i ett makro behöver alltså inte stå för en variabel i P utan kan stå för en godtycklig text, till exempel en sats eller ett villkor. Detta gör att man med makrodefinitioner, utan att anstränga sig, kan skriva helt obegripliga program. Det är därför viktigt att vi använder förkortningsmekanismen med ett visst omdöme. Vi kommer bara att låta makrovariabler stå för texter som är kompletta syntaktiska enheter i P, dvs. för variabler, villkor, uttryck eller satser. Vi kommer också att i varje makrodefinition indikera vad för slags syntaktiska objekt vi tänker att de olika variablerna står för genom en slags variabeldeklaration. Vi kallar ett P-program som innehåller ett makroanrop för ett MP-program. Betydelsen av MP-programmet PROGRAM(X1) ASSIGN(X2,X1) RESULT(X2) blir då P-programmet (utan makroanrop) PROGRAM(X1) X2:=SUCC(X1); X2:=PRED(X2) RESULT(X2) Genom att införa förkortningar på detta sätt behöver vi inte förändra programmeringsspråkets semantik på något sätt, utan vi kan definiera betydelsen av förkortningarna helt syntaktiskt. Ett makroanrop av en satsförkortning i ett program expanderas precis likadant oavsett i vilken omgivning (sammanhang) anropet står. Syntaktiska operationer som har denna egenskap kallas sammanhangsoberoende (eng. context free). Betydelsen av en förkortning ges genom dess expansion och beräkningsreglerna för det ursprungliga språket. Vi kan därför härleda (bevisa) speciella beräkningsregler för en förkortning utgående från de ursprungliga beräkningsreglerna och definitionen av förkortningen.

82 3.8. FÖRKORTNINGAR 75 Om x och y är variabler i P och σ ett tillstånd, så kan vi kombinera de två härledningarna: SUCC(y), σ s(σ(y)) [E2] x := SUCC(y), σ σ[x s(σ(y))] [T ] (1) för att få: PRED(x), σ[x s(σ(y))] σ(y) [E4] x := PRED(x), σ[x s(σ(y))] σ[x σ(y)] (1) (2) x := SUCC(y); x := PRED(x), σ σ[x σ(y)] [S] Men eftersom förkortningen ASSIGN(x, y) expanderas till så följer satsen: x := SUCC(y); x := PRED(x) [T ] (2) Sats 3.18 Om x och y är variabler och σ ett tillstånd, så gäller beräkningsregeln ASSIGN(x, y), σ σ[x σ(y)] Vi kan formulera denna sats för satsförkortningar i allmänhet: Sats 3.19 Om satsförkortningen QM(t 1,..., t n ) expanderas till satsen c(t 1,..., t n ) och om vi för alla tillstånd σ kan finna en härledning som visar att så gäller det att c(t 1,..., t n ), σ σ QM(t 1,..., t n ), σ σ Ett ytterligare exempel på en makrodefinition är: CMD IF Y1 0 THEN Y2 FI MACRO: -- Y1 : Var, Y2 : Sats WHILE Y1 0 DO Y2; Y1 := 0 END END

83 76 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER Observera att makrovariablerna i en makrodefinition står för godtyckliga texter. I definitionen av IF-förkortningen måste Y1 stå för en variabel och Y2 för en hel sats för att makrodefinitionen skall expanderas till en syntaktiskt korrekt sats. Observera också att expanderas till IF X1 0 THEN X1 := 0; X1 := SUCC(X1) FI (3.1) WHILE X1 0 DO X1 := 0; X1 := SUCC(X1); X1 := 0 END; Detta medför att X1 alltid är 0 efter det att 3.1 har beräknats. Evalueringen av satsförkortningen har alltså en sidoeffekt, dvs. den ändrar på värdet av en variabel som används i det program där makroanropet sker. Den variabel som är första parameter i ett IF-anrop kommer alltid att ha värdet 0 efter en evaluering av förkortningen. Detta är naturligtvis inget teoretiskt problem, men ändå tillräckligt irriterande för att vi ska lägga ner lite energi på att förbättra situationen. Det grundläggande problemet är att det inte kan finnas några lokala variabler i en makrodefinition. Men variabler är något som hör till språket P och det vill vi ändra så lite som möjligt. En lösning är att definiera en något krångligare expansion av ett makroanrop. Antag att vi har en definition av en förkortning som innehåller makrovariablerna Y1, Y2,..., Yn, till exempel förkortningen F(Y1, Y2,..., Yn), men satsen som definierar förkortningen innehåller ytterligare m makrovariabler, Yn + 1,..., Yn+m. Antag vidare att expansionen sker i ett program där k är det högsta variabelindex som förekommer. I expansionen av ett makroanrop F(T 1,..., T n ) kan ju inte Yn + 1,..., Yn + m bytas ut mot texten i motsvarande aktuella parametrar eftersom det inte finns några sådana parametrar. Vi bestämmer därför att Yn + 1 byts ut mot Xk+1, som är en variabel som inte tidigare använts i programmet med makroanropet, att Yn + 2 byts mot Xk + 2, osv. När ett makroanrop expanderas, så kommer alltså alla extra makrovariabler i definitionen att bytas ut mot obegagnade variabler i programmet. Med denna nya möjlighet kan vi nu skriva om definitionen av IF-förkortningen på följande sätt: CMD IF Y1 0 THEN Y2 FI MACRO: -- Y1 : Var, Y2 : Sats ASSIGN(Y3,Y1); WHILE Y3 0 DO Y2; Y3 := 0 END

84 3.8. FÖRKORTNINGAR 77 END När ett makroanrop IF X1 0 THEN X1:=1 FI nu expanderas och evalueras kommer inte värdet av variabeln X1 att sättas till 0. Observera hur vi i IF-förkortningen har använt den tidigare definierade förkortningen ASSIGN(Y1,Y2). Det finns alltså inget som hindrar att vi i en makrodefinition använder tidigare definierade förkortningar. Notera också att med lokala variabler i satsförkortningarna, så blir förkortningarna inte längre sammanhangsoberoende; sammanhanget, i detta fall det omgivande programmets maximala variabelindex, påverkar expansionen av ett makroanrop. Exempel: Låt oss se hur ett MP-program expanderas. Resultatet av att expandera MP-programmet: PROGRAM(X0) X1 := 0; IF X0 0 THEN X1 := SUCC(X1) RESULT(X1) blir till exempel: PROGRAM(X0) X1 := 0; ASSIGN(X2,X0); WHILE X2 0 DO X1 := SUCC(X1); X2 := 0 END RESULT(X1) Vi fortsätter med att definera ytterligare en satsförkortning. CMD IF Y1 0 THEN Y2 ELSE Y3 FI MACRO:

85 78 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER END -- Y1 : Var, Y2 : Sats, Y3 : Sats ASSIGN(Y5,Y1); Y4 := 0; Y4 := SUCC(Y4); WHILE Y5 0 DO Y2; Y5 := 0; Y4 := 0 END; WHILE Y4 0 DO Y3; Y4 := 0 END Tanken bakom denna förkortning är naturligtvis att definiera något som uppträder som om det hade följande beräkningsregler: x 0, σ false c, σ σ IF x 0 THEN c ELSE c FI, σ σ x 0, σ true c, σ σ IF x 0 THEN c ELSE c FI, σ σ Man skulle kunna säga att dessa regler utgör en specifikation av IF-förkortningen. Genom att tillämpa beräkningsreglerna för P kan vi visa att förkortningen ovan uppfyller denna specifikation. Vi gör detta genom att härleda att definitionen ovan uppfyller beräkningsreglerna. Vi visar helt enkelt att implementeringen har de egenskaper som specifikationen kräver. Vi härleder den ena delen här och lämnar den andra som övning. Av utrymmesskäl delar vi upp härledningen i flera bitar: Exempel: I stället för premissen x 0, σ false antar vi att σ(x) = 0, eller att σ(y1) = 0 eftersom x = Y1 i härledningen. Vi börjar med att introducera följande definitioner s 0 Y4 := 0; Y4 := SUCC(Y4) s 1 ASSIGN(Y5, Y1); s 0 s 2 WHILE Y5 0 DO Y2; Y5 := 0; Y4 := 0 END s 3 WHILE Y4 0 DO Y3; Y4 := 0 END σ 1 σ[y5 σ(y1)] = σ[y5 0] σ 2 σ[y5 0, Y4 1] σ 3 σ [Y4 0]

86 3.8. FÖRKORTNINGAR 79 [se ovan] ASSIGN(Y5, Y1), σ σ 1 s 0, σ 1 σ 2 [S] (1) s 1, σ σ 2 [V 1] Y5 0, σ 2 false [W 1] (2) s 2, σ 2 σ 2 Y4 0, σ 2 true [V 2] (3 : 1) Y3, σ 2 σ [A] 0, σ 0 [E1] Y4 := 0, σ σ 3 Y3; Y4 := 0, σ 2 σ 3 [T ] [S] (3 : 2) Y4 0, σ 3[Y4 0] false s 3, σ 3 σ 3 [V 1] [W 1] (3 : 3) (3 : 1) Y3, σ 2 σ [A] (3 : 2) (3 : 3) s 3, σ 2 σ 3 [W 2] (3) (1) (2) s 1 ; s 2, σ σ 2 [S] s 1 ; s 2 ; s 3, σ σ 3 Y3, σ 2 σ [A] Men s 1 ; s 2 ; s 3 är precis makrodefinitionen av IF Y1 0 THEN Y2 ELSE Y3 FI, så härledningen visar att, om σ(y1) = 0 (3) [S] och Y3, σ[y5 0, Y4 1] σ så gäller det att IF Y1 0 THEN Y2 ELSE Y3 FI, σ σ [Y4 0]

87 80 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER Men Y4 och Y5 kommer att expanderas till obegagnade variabler i varje program där förkortningen används. De kommer därför bara att användas i det programavsnitt som blir resultat av att expandera IF-förkortningen och kan därför aldrig påverka någonting i det omgivande programmet. Sett från programmet med makroanropet kan man därför se härledningen ovan som ett bevis av följande härledda beräkningsschema för IF-förkortningen. Om σ(x) = 0, så gäller att: Uttrycksförkortningar c, σ σ IF x 0 THEN c ELSE c FI, σ σ Uttrycksförkortningar blir något krångligare att hantera. Syntaxen för att introducera en uttrycksförkortning är snarlik den för satsförkortningar: EXPR förkortning MACRO: -- deklarationer sats RESULT( makrovariabel ) Till exempel: EXPR Y1 + Y2 MACRO: -- Y1: Var, Y2 : Var WHILE Y2 0 DO Y1 := SUCC(Y1); Y2 := PRED(Y2) END RESULT(Y1) Vi kräver att makrovariabeln i RESULT-delen står för en P-variabel. Den introducerade förkortningen kan användas överallt där ett uttryck förväntas, naturligtvis under antagandet att argumenten är P-variabler. Anledningen till att uttrycksförkortningarna blir krångligare att hantera är att vi inte vill att ett anrop av en uttrycksförkortning skall ha någon sidoeffekt. En uttrycksförkortning kan bara anropas i en tilldelningssats, till exempel X1 := X2 + X3; Betydelsen av detta anrop får man genom att expandera det på följande sätt:

88 3.8. FÖRKORTNINGAR 81 Antag att anropet skett i ett program som har k som sitt högsta variabelindex, antag vidare att förkortningen innehåller variablerna Y1,..., Ym och antag slutligen att parametrarna i makroanropet är t 1,..., t m, där t i antingen är ett uttryck eller en variabel. Istället för satsen med makroanropet får vi en sekvens av satser som börjar med: c 1 ;... ; c m där c i är satsen Xk + i:=t i om t i är ett uttryck och ASSIGN(Xk + i,t i ) om t i är en variabel. Variablerna Xk+1 till Xk+m är nya i det program där makroanropet skett. Efter dessa satser följer sedan texten i makrodefinitionen med makrovariablerna utbytta mot de variabler som motsvarar de formella parametrarna, dvs. Yi är utbytt mot Xk +i. Slutligen får vi en sats som tilldelar förkortningens resultat till den motsvarande P-variabeln. Om makroanropet har formen så får vi satsen Xi := QM(t 1,..., t m ) ASSIGN(Xi,Xj) där Xj är den P-variabel som motsvarar makrovariabeln i RESULT-delen av förkortningen. Meningen av detta kanske enklast framgår av ett exempel. Exempel: Om vi använder expansionsregeln ovan för att expandera förkortningen i följande MP-program: så får vi: PROGRAM(X1) X2 := X1+X1 RESULT(X2) PROGRAM(X1) ASSIGN(X3,X1); ASSIGN(X4,X1); WHILE X4 0 DO X3 := SUCC(X3); X4 := PRED(X4) END; ASSIGN(X2,X3) RESULT(X2)

89 82 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER Observera att detta inte är ett P-program, det innehåller ju fortfarande makroanropen ASSIGN(X3,X1), ASSIGN(X4,X1) och ASSIGN(X2,X3). Fortsatt expansion ger PROGRAM(X1) X3 := SUCC(X1); X3 := PRED(X3); X4 := SUCC(X1); X4 := PRED(X4); WHILE X4 0 DO X3 := SUCC(X3); X4 := PRED(X4) END; X2 := SUCC(X3); X2 := PRED(X2) RESULT(X2) Som synes behövs det inte många förkortningar innan det expanderade programmet exploderar. För att slippa problemet med att variabler syntaktiskt sett inte är uttryck, så kan vi införa den till synes meningslösa förkortningen EXPR VAR(Y1) MACRO: -- Y1 : Var RESULT(Y1) Denna förkortning förvandlar en variabel till ett uttryck, genom att använda expansionsmekanismen för uttrycksförkortningar. Med denna förkortning kan vi nu skriva till exempel X2 := VAR(X1) istället för att använda ASSIGN-förkortningen. Vi kommer i fortsättningen gladeligen att strunta i detta VAR( ) och använda varibler överallt där det förväntas ett uttryck renläriga kan själva sätta in ett VAR( ) överallt där det behövs för att få allt att gå ihop! Hela denna röra med olika slags förkortningar och problem med att variabler inte är uttryck kan naturligtvis undvikas genom att språket P redan från början görs mer innehållsrikt; och för ett praktiskt språk skulle man naturligtvis göra detta. Men nu är vi intresserade av få P så litet som det överhuvudtaget är möjligt, och vi råkar därför allt som oftast in i en massa triviala problem när vi vill använda P till något som liknar praktisk programmering. Vi tillåter oss därför en hel del

90 3.8. FÖRKORTNINGAR 83 informell notation som i princip inte behövs men som är ovärderlig om man vill försöka få framställningen någorlunda läsbar. När vi använder ett naturligt tal i decimal notation, så antar vi att det finns ett lämpligt antal definitioner av utseendet EXPR 1 MACRO: Y1 := 0; Y1 := SUCC(Y1) RESULT(Y1) EXPR 2 MACRO: Y1 := 1; Y1 := SUCC(Y1) RESULT(Y1) Testförkortningar Till sist så definierar vi syntaxen för testförkortningen: TEST förkortning MACRO: -- deklarationer sats RESULT( variabel ) Till exempel: TEST (Y1 0) & (Y2 0) MACRO: -- Y1 : Var, Y2 : Var Y3 := 0; IF Y1 0 THEN IF Y2 0 THEN Y3 := SUCC(Y3) FI FI RESULT(Y3) Som en konvention låter vi alltid resultatet av en testförkortning vara lika med 0 om testen är falsk och 1 om den är sann. Ett testmakroanrop i ett program expanderas på ungefär samma sätt som de andra förkortningarna. Vi kommer till och med att förklara hur ett testmakroanrop expanderas i termer av uttrycksförkortningar. Om QM är en testförkortning, så låt

91 84 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER QM vara motsvarande uttrycksförkortning, dvs. den förkortning som fås genom att byta ut ordet TEST mot EXPR i inledningen av definitionen. Den sats som innehåller ett testmakroanrop, till exempel, WHILE QM DO c END och som finns i ett program där k är högsta variabelindex, kommer att expanderas till: Xk + 1 := QM ; WHILE Xk DO c; Xk + 1 := QM END som i sin tur expanderas enligt reglerna för expansion av uttrycksförkortningar. Uttrycks-förkortningar och testförkortningar definieras med nästan exakt samma syntax och det är enkelt att se att det till varje uttrycksförkortning finns en motsvarande testförkortning och vice versa. De två olika förkortningarna skiljer sig bara genom att de kan förekomma på olika platser i ett P-program; uttrycksförkortningar förekommer i tilldelningssatser och testförkortningar i while-satser. Anledningen till att vi över huvud taget har infört två olika typer av förkortningar är ju att expansionen av de två typerna sker på olika sätt. Vi skall i fortsättningen slå ihop de två typerna och inte definiera några testförkortningar utan bara använda uttrycksförkortningar. Vi låter helt enkelt den plats där förkortningen används bestämma hur expansionen skall ske. Om anropet sker i en tilldelningssats kommer det att expanderas som en uttrycksförkortning och om anropet sker i en while-sats så kommer expansionen att ske som för en testförkortning. Både uttrycksförkortningar, testförkortningar och satsförkortningar är sammanhangsberoende, deras expansion är ju i hög grad beroende på i vilken omgivning ett anrop förekommer. Vi fortsätter med att definiera ett makro för förkortningen Y1<Y2. EXPR Y1 < Y2 MACRO: -- Y1 : Var, Y2 : Var WHILE (Y1 0) & (Y2 0) DO Y1 := PRED(Y1); Y2 := PRED(Y2) END; IF Y2 0 THEN Y3 := 1 ELSE Y3 := 0 FI RESULT(Y3) Övning 3.15 Definiera en satsförkortning SKIP med beräkningsregel

92 3.8. FÖRKORTNINGAR 85 SKIP, σ σ Övning 3.16 Gör en sats-förkortning som motsvarar Pascals FOR-sats. Övning 3.17 Bevisa följande härledda beräkningsschema om σ(x) 0: c, σ σ IF x 0 THEN c ELSE c FI, σ σ Övning 3.18 Bevisa härledda beräkningsscheman för IF Y1 0 THEN Y2 FI. Övning 3.19 Gör en sats-förkortning för REPEAT Y1 UNTIL Y2=0 med samma betydelse som i Pascal. Härled också beräkningsscheman för satsen. Övning 3.20 Antag att G är en förkortning som beräknar g och att H(Y1,Y2) är en förkortning som beräknar h(y1, Y2). Skriv ett P-program som beräknar funktionen f, som definierats med primitiv rekursion enligt: f(0) = g f(n + 1) = h(n, f(n)) Övning 3.21 Definiera följande uttrycksförkortningar, låt Y1 och Y2 stå för uttryck: a. Y1 Y2 (Y1 Y2 om Y1 Y2, Y2 Y1 annars) b. Y1 Y2 c. Y1 Y2 (Y1 Y2 ) d. Y1 DIV Y2 (med samma betydelse som i ml och Pascal) e. Y1 MOD Y2 (med samma betydelse som i ml och Pascal) Övning 3.22 Definierna också uttrycksförkortningar för a. ÄR-PRIMTAL?(Y1) (resultatet = 1 om Y1 är ett primtal, = 0 annars) b. PRIMTAL(n) (ger det n:te primtalet) c. PRIMEXP(m, n) (ger exponenten för det m:te primtalet i primtalsuppdelningen av talet n, dvs. PRIMEXP(m, n) = k om n = p a 1 1 p k m )

93 86 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER Övning 3.23 Definiera förkortningar för följande: a. SG(Y1) (resultatet = 0 om Y1=0, = 1 annars) b. NOT(Y1) (satslogisk negation) c. (Y1 = Y2) (= 1 om Y1=Y2, = 0 annars) d. (Y1 <> Y2) e. (Y1 Y2) (= Y2 om Y1>0, = 1 annars; dvs. satslogisk implikation) f. (Y1 OR Y2) (satslogisk disjunktion) g. (Y1 & Y2) (Y1 and Y2; dvs. satslogisk konjunktion) Övning 3.24 Expansionen av en testförkortning förklarades i termer av uttrycksförkortningar. Om vi nu bara har en typ av förkortningar, hur fungerar det?

94 3.9. BERÄKNINGAR MED ANDRA DATAOBJEKT Beräkningar med andra dataobjekt Hittills har vi bara använt naturliga tal som dataobjekt och alla beräkningar som har beskrivits har handlat om naturliga tal. Vi skall nu visa hur beräkningar med andra dataobjekt, till exempel tecken och strängar (listor av tecken), kan simuleras i P genom att de nya objekten kodas som naturliga tal. Det finns två olika aspekter på användningen av andra dataobjekt än naturliga tal i beräkningar. För det första, så vill vi definiera vad som menas med att en partiell funktion f A B är beräkningsbar då A och B inte är delmängder av N. För det andra, så vill vi kunna introducera andra dataobjekt, till exempel tecken och listor, i P-program som beräknar partiella funktioner i N N. Med andra ord, vi vill dels definiera begreppet beräkningsbarhet för andra mängder än N N, och dels ha en möjlighet att använda tecken, listor och andra dataobjekt internt i P-program vars indata och utdata är naturliga tal. Man kan jämföra det senare fallet med hur det är i Pascal, datatyperna SET OF INTEGER och RECORD X,Y : REAL END kan där användas internt, även om dataobjekt av dessa typer aldrig kan förekomma som indata eller utdata i textfiler. Vi börjar med att utvidga begreppet beräkningsbar partiell funktion till att gälla andra partiella funktioner än de i N N. Antag till exempel att vi har en partiell funktion g från listor av tecken till listor av tecken, dvs. g List(Char) List(Char) Vad menar vi då med att denna funktion är beräkningsbar? För att se att g är beräkningsbar, så måste vi i princip konstruera en maskin som beräknar funktionen. Istället för att göra detta för varje g, så kan vi göra på följande sätt. Definiera en kodnings- och en avkodnings-funktion för listor av tecken: e List(Char) List(Char) N (encode) d List(Char) N List(Char) (decode) Båda funktionerna måste vara beräkningsbara, dvs. givet en lista av tecken så skall man mekaniskt kunna finna talet som representerar listan, och givet ett tal så skall man mekaniskt kunna finna den lista som talet representerar. Om vi har definierat dessa funktioner och visat att de är beräkningsbara, så säger vi helt enkelt att en partiell funktion g List(Char) List(Char) är beräkningsbar om det existerar en P-beräkningsbar partiell funktion f N N sådan att g = d List(Char) f e List(Char) där h h betecknar den funktion som vi får genom att komponera de två funktionerna h och h, dvs. den funktion för vilket det gäller att h h (x) = h(h (x)), se även sid 19.

95 88 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER Vi kan generalisera denna metod till att gälla godtyckliga mängder A och B, och vi definierar därför vad som menas med en beräkningsbar partiell funktion från A till B på följande sätt. Definition 3.20 Den partiella funktionen g A B är P-beräkningsbar om det finns en P-beräkningsbar partiell funktion f N N, en beräkningsbar kodningsfunktion e A A N och en beräkningsbar avkodningsfunktion d B N B sådana att: g = d B f e A Tanken är naturligtvis att vi en gång för alla skall definiera kodnings- och avkodningsfunktioner för olika mängder. För att kunna göra detta så måste villkoret d A (e A (x)) = x gälla för alla x A för att vi inte skall tappa några P-beräkningsbara funktioner. Vi har nu fått en hanterlig definition av vad som menas med P-beräkningsbara partiella funktioner mellan godtyckliga mängder. Vår nästa fråga gäller hur vi ska introducera andra dataobjekt än de naturliga talen i språket P. Om vi i P vill beskriva en beräkning från naturliga tal till naturliga tal som internt använder element ur en matematisk mängd A, till exempel listor, så måste vi först definiera en datalogisk motsvarighet till denna mängd; vi måste definiera en datatyp. Matematiskt sett så tänker vi oss att en mängd antingen definieras direkt med de primitiva operationerna i mängdlära, såsom vi definierade mängden av partiella funktioner, eller också att den definieras med induktiva klausuler, såsom vi tidigare definierade de naturliga talen. En datatyp definieras traditionellt inom datalogi genom sina primitiva operationer. Anledningen till att vi inte som i mängdlära bara behöver definiera objekten i mängden är att vi i programmering alltid måste definiera program explicit. Vi kan i allmänhet inte definiera ett program genom att bilda en delmängd till en Cartesisk produkt så som man i mängdlära kan definiera en funktion. I mängdlära kan vi till exempel definiera pred-funktionen på följande sätt: { x, y N N s(x) = y} I datalogi använder vi inte sådana här implicita definitioner, utan vi definierar programmen konstruktivt; vi bygger upp dem med hjälp av de primitiva operationer som är effektivt implementerade på våra datorer. Vi kommer att använda en form av algebraiska specifikationer för att definiera (specificera) datatyper. Vi introducerar först de primitiva operationerna med sina typer och ger sedan de likheter som gäller i datatypen. Vi kan till exempel definiera datatypen Bool på följande sätt:

96 3.9. BERÄKNINGAR MED ANDRA DATAOBJEKT 89 MODULE TYPE: Bool type OPERATIONS: false Bool true Bool if b then a else a A, om b Bool och a, a A EQUATIONS: if true then a else a = a if false then a else a = a END och sedan fortsätta att definiera N, MODULE TYPE: N type OPERATIONS: 0 N s(n) N, om n N pred(n) N, om n N n 0 Bool, om n N EQUATIONS: pred(0) = 0 N pred(s(n)) = n N 0 0 = false Bool s(n) 0 = true Bool END För många mängder kan man finna en exakt motsvarande datatyp, men ibland är detta inte möjligt. Ett exempel där vi inte kan göra detta är mängden N N. Denna är, som vi tidigare sett, överuppräknelig (dvs. större än alla uppräkningsbara mängder) och det finns helt enkelt ingen möjlighet att konstruera dataobjekt som motsvarar varje element i mängden. Om vi ska kunna finna en datatyp som motsvarar en mängd, så måste mängden åtminstone vara uppräkningsbar. Vi skall här inte gå närmare in på hur olika datatyper definieras, utan bara studera varför de primitiva operationerna för N valts som de gjorts. I P konstrueras objekten i N med hjälp av de två operatorerna 0 och SUCC. Man kan jämföra dessa med det som brukar kallas konstruerare i samband med algebraiska specifikationer av datatyper. De två operatorerna 0 och SUCC är naturligtvis tänkta att motsvara 0 respektive s i Peanos två första axiom vilket de också gör, som vi kan se från beräkningsreglerna för uttryck. Förutom operatorerna 0 och

97 90 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER SUCC, så innehåller P operatorerna PRED och 0 för naturliga tal. Dessa har inte valts av en slump, utan enligt följande mönster. Operatorn 0 finns med som en primitiv operator i P eftersom man i beräkningar med naturliga tal måste kunna skilja på tal som konstruerats med hjälp av 0-operatorn och tal som konstruerats med SUCC-operatorn. Motiveringen för PRED-operatorn är att man måste kunna komma åt delarna av ett konstruerat objekt. Om ett objekt har konstruerats med hjälp av 0-operatorn, så har det inga delar, men om det konstruerats med SUCCoperatorn, så innehåller det en komponent, nämligen det som är argument till SUCC-operatorn. Det är denna komponent som PRED-operatorn har till uppgift att ge som resultat. Enligt denna motivering, så skulle PRED-operatorn endast behöva vara definierad för tal större än 0, dvs. de tal som har konstruerats med hjälp av SUCC-operatorn. Men det är alltid enklare att arbeta med totala operationer, därför har vi definierat PRED även för 0. Observera att detta var ett val som vi gjorde när vi definierade språket P och vi kunde lika väl ha gjort på något annat sätt. Vilken betydelse en viss konstruktion har i P kan man alltid få reda på genom beräkningsreglerna och om vi hade valt att ge en operator en annan definition så skulle vi naturligtvis fått andra beräkningsregler. Vi skall nu se hur man kan definiera beräkningsbara kodnings- och avkodningsfunktioner från/till några olika mängder samt hur man kan implementera några datatyper i P i termer av naturliga tal. Vi kan definiera en kodningsfunktion e Bool Bool N enligt: e Bool (false) = 0 e Bool (true) = 1 Med hjälp av primitiv rekursion definierar vi också en avkodningsfunktion d Bool N Bool enligt: d Bool (0) = false d Bool (s(n)) = true De här funktionerna fungerar som kodnings- respektive avkodnings-funktion eftersom d Bool är invers till e Bool. Det är enkelt att se att det gäller för alla b Bool att d Bool (e Bool (b)) = b. Vi definierar inte explicit någon kodnings- eller avkodnings-funktion för tecken, utan vi använder ASCII koden för detta ändamål. e Char Char N ger ett teckens ASCII-kod medan d Char N Char ger ett tecken givet dess kod. En implementering av en datatyp A i termer av en datatyp B, som vi antar redan finns implementerad, består av beräkningsbara funktioner som implementerar de primitiva operationerna i datatypen. En implementering är korrekt om de ekvationer som ska gälla för operationerna gäller i implementeringen. När

98 3.9. BERÄKNINGAR MED ANDRA DATAOBJEKT 91 det gäller implementeringar i språket P, så har vi ju från början bara en implementerad datatyp, N, så alla objekt i alla andra datatyper som vi vill implementera måste till slut representeras som naturliga tal. De primitiva operationerna implementeras lämpligen i form av uttrycksförkortningar, som givet representationer av argumenten producerar en representation av resultatet. Så om vi vill implementera datatypen Bool, som motsvarar mängden av sanningsvärden, måste vi först representera alla värden i Bool. Vi gör detta genom att introducera följande två förkortningar: och EXPR TRUE MACRO: Y1 := 1 RESULT(Y1) EXPR FALSE MACRO: Y1 := 0 RESULT(Y1) Observera att denna representation är konsistent med konventionen för testförkortningar att resultatet är lika med 0 om villkoret är falskt (se sidan 83). Vi måste också definiera förkortningar för de primitiva operationerna. Men först måste vi bestämma vilka primitiva operationer som vi behöver. Om vi följer samma mönster som för N, så ser vi att vi behöver en operation som avgör hur ett objekt i datatypen Bool har konstruerats. Vi behöver alltså en operator =FALSE i P, vilken avgör hur ett objekt har konstruerats. En mer generell operation för att avgöra hur ett objekt har konstruerats är IF THEN ELSE-uttrycket, som användes när vi definierade datatypen Bool ovan. Det är uppenbart att vi med IF THEN ELSE-uttrycket lätt kan programmera =FALSE-operationen. Vi kommer därför att använda IF-uttrycket som en primitiv operation för datatypen Bool även om den bryter mönstret en aning. EXPR IF Y1 THEN Y2 ELSE Y3 MACRO: -- Y1 : Uttryck, Y2 : Uttryck, Y3 : Uttryck -- Y1 repr Bool IF Y1 0 THEN Y4 := Y2 ELSE Y4 := Y3 FI RESULT(Y4) Eftersom konstruerarna inte innehåller några komponenter, så behövs inga fler operatorer. Om vi expanderar satsen

99 92 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER så får vi vilket är samma sak som dvs. x := IF TRUE THEN e 1 ELSE e 2 IF 1 0 THEN Xmax+1 := e 1 ELSE Xmax+1 := e 2 FI; ASSIGN(x,Xmax+1) x := e 1 IF TRUE THEN e 1 ELSE e 2 = e 1 På liknande sätt kan vi övertyga oss om att IF FALSE THEN e 1 ELSE e 2 = e 2 Så båda ekvationerna i datatypen Bool är uppfyllda i implementeringen. Nästa datatyp som vi ska implementera i P är Int, dvs. datatypen med heltal som dataobjekt. Dataobjekten i Int genereras som vi tidigare sett av: 1. Om n är ett naturligt tal så är +n ett heltal. 2. Om n är ett naturligt tal så är n ett heltal. De operationer som vi behöver är: 1. i Int 0, som avgör om ett heltal är större än eller lika med abs(i) som ger talet n som resultat, om i = +n eller i = n. Vi kan skriva datatypen som MODULE TYPE: Int type OPERATIONS: +n Int, om n N n Int, om n N i Int 0 Bool, om i Int abs(i) N, om i Int

100 3.9. BERÄKNINGAR MED ANDRA DATAOBJEKT 93 END EQUATIONS: +n Int 0 = true Bool s(n) Int 0 = false Bool 0 Int 0 = true Bool abs(+n) = n N abs( n) = n N Vi kan implementera datatype Int i P genom följande förkortningar: EXPR [+Y1] Int MACRO: -- Y1 : Uttryck Y2 := 2*Y1 RESULT(Y2) EXPR [ Y1] Int MACRO: -- Y1 : Uttryck Y2 := 2*Y1; Y2 := PRED(Y2) RESULT(Y2) EXPR Y1 Int 0 MACRO: -- Y1 : Uttryck IF Y1 0 THEN Y2 := Y1 MOD 2; Y2 := (Y2=0) ELSE Y2 := TRUE FI RESULT(Y2) Implementeringen av den sista operationen lämnas som övning. Den representation av heltal som definieras av dessa förkortningar definierar också kodnings- och avkodnings-funktioner för heltalen. Antag att A och B är två datatyper. Då kan vi definiera datatypen A B på följande sätt. För varje a A och b B så är paret av a och b, dvs. a, b, ett objekt i datatypen A B. Vi har en konstruerare,,, i datatypen. Eftersom det bara finns en operation som konstruerar objekt i A B, så behövs det bara primitiva operationer för att kunna selektera komponenterna från ett sådant objekt. Vi behöver de primitiva operationerna: fst(p) A, snd(p) B, om p A B om p A B

101 94 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER med följande egenskaper: fst( a, b ) = a snd( a, b ) = b Om vi skriver detta lite mer formellt, så får vi MODULE TYPE: A B type, om A type och B type OPERATIONS: a, b A B, om a A och b B fst(p) A, om p A B snd(p) B, om p A B EQUATIONS: fst( a, b ) = a A snd( a, b ) = b B END Om vi har implementeringar av A och B i P, så har vi förkortningar som ger representationen av objekten i A och B. Då kan vi implementera A B med hjälp av följande förkortningar: EXPR Y1,Y2 MACRO: -- Y1 : Uttryck, Y2 : Uttryck -- Y1 repr A, Y2 repr B Y3 := 2**Y1; Y4 := 3**Y2; Y3 := Y3*Y4 RESULT(Y3) Vi antar här att Y1 och Y2 är uttryck vars värden är naturliga tal som representerar objekt i A respektive B. Detta indikerar vi i makrodefinitionen genom att skriva Y1 repr A respektive Y2 repr B. EXPR FST(Y1) MACRO: -- Y1 : Uttr -- Y1 repr A B Y2 := PRIMEXP(1,Y1) -- exp för 1:a primtalet (2) RESULT(Y2) EXPR SND(Y1) MACRO: -- Y1 : Uttr -- Y1 repr A B Y2 := PRIMEXP(2,Y1) -- exp för 2:a primtalet (3) RESULT(Y2)

102 3.9. BERÄKNINGAR MED ANDRA DATAOBJEKT 95 Lägg märke till att tilldelningarna: ger upphov till satserna: x := e 1,e 2 ; x := FST(x) Xmax+1 := 2 ** e 1 ; Xmax+2 := 3 ** e 2 ; Xmax +1 := Xmax +1 * Xmax +2; ASSIGN(x,Xmax +1); Xmax +3 := PRIMEXP(1,x); ASSIGN(x,Xmax +3) från vilka det följer att x = e 1, dvs. ekvationen e 1 = FST( e 1, e 2 ) är uppfylld i implementeringen. En datatyp för List(A) innehåller följande primitiva operationer: 1. Konstruerarna nil och. (cons) kommer direkt från klausul E1 och E2 i definitionen av listor; nil List(A) och a.l List(A), om a A och l List(A). 2. Vi behöver en operation för att avgöra hur en lista har konstruerats: och det ska gälla att: null(nil) = true null(l) Bool om l List(A) null(a.l) = false, för alla a A och l List(A). 3. Förutom operatorn null, så behöver vi två operationer för att selektera komponenterna ur de listor som konstruerats med. -operatorn. hd(l) A, om l List(A) Det gäller för dessa operationer att: tl(l) List(A), om l List(A) hd(a.l) = a tl(a.l) = l

103 96 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER Med andra ord, om A är en datatyp, så kan vi definiera datatypen List(A) på följande sätt: MODULE TYPE: List(A) type, om A type OPERATIONS: nil List(A) a.l List(A), om a A och l List(A) null(l) Bool, om l List(A) hd(l) A, om l List(A) tl(l) List(A), om l List(A) EQUATIONS: null(nil) = true Bool null(a.l) = false Bool hd(a.l) = a A tl(a.l) = l List(A) END Vi lämnar implementeringen av denna datatyp som övning. Övning 3.25 Med den motivering som gavs på sidan 90 för att introducera operatorn 0 så skulle vi lika gärna kunna valt en operator =0. Skulle det fungera? Försök att börja bygga upp förkortningar på motsvarande sätt som vi gjorde i föregående avsnitt. Övning 3.26 Hur representeras paret 3, 4? Övning 3.27 Betrakta implementeringen av datatypen N N. Det finns många tal som inte representerar något par a, b. Är det möjligt att mekaniskt avgöra om ett givet tal n representerar ett par eller ej? Hur i så fall? Finns det någon annan representation där varje tal representerar ett par? Övning 3.28 Implementera operationen ABS(Y1). Övning 3.29 Ge argument som visar att implementeringen av datatypen Int är korrekt. Övning 3.30 Implementera datatypen List(A) i P under förutsättning att det finns en implementering av A. Övning 3.31 Definiera och implementera en datatyp för den disjunkta unionen av typerna A och B.

104 3.10. P-PROGRAM SOM DATAOBJEKT P-program som dataobjekt I denna sektion skall vi visa att man kan implementera den datatyp som motsvarar mängden av P-program och att man därför kan skriva P-program som använder andra P-program, eller rent utav sig själva, som dataobjekt. Detta är inget speciellt kontroversiellt, vi är vana vid det från vårt vanliga programmeringsarbete. Tänk till exempel på att om vi skriver ett listningsprogram, så kan vi använda det till att göra en utskrift av sig själv; en kompilator skriven i språket X, som översätter X-program till maskinkod, kan ta sig själv som indata. För att implementera en datatyp för P-program så måste vi först ha en representation av P-program i termer av naturliga tal. Processen att systematiskt tilldela syntaktiska objekt, som P-program, ett naturligt tal brukar kallas för Gödel numrering, eftersom detta sätt att koda syntaktiska objekt som naturliga tal är en viktig del i beviset av Gödels ofullständighetssats. Denna sats är ett av de klassiska resultaten i matematisk logik, och den säger, enkelt uttryckt, att det inte är möjligt att konstruera ett formellt logiskt system för de naturliga talen som både är konsistent och fullständigt, dvs. ett system där man, för varje påstående A, antingen kan bevisa att A är sann eller att A är sann, samtidigt som man inte kan bevisa att (A & A) är sann. Det enklaste sättet att representera P-program är helt enkelt att ta den konkreta programtexten och översätta den till ett naturligt tal. Vi såg i föregående avsnitt, att vi kan implementera datatypen Char, och vi såg också att bara vi hade en implementering av en datatypen A, så kunde vi också implementera List(A). Men då är vi åtminstone till hälften klara, ett program skrivs ju som en sekvens av tecken och därför får vi ett unikt naturligt tal som representant för varje program. Visserligen kommer de flesta tal inte att representera något program alls, men det spelar ingen roll. Inte heller spelar det någon roll att talen blir astronomiskt stora för de flesta program. Ta till exempel programmet: Det skulle ju representeras av talet: PROGRAM(X0) X0:=SUCC(X0) RESULT(X0) P R O G R A M ( X Det här sättet att representera ett program fungerar, men är inte speciellt trevligt att använda. Bara att ta reda på om ett givet tal representerar ett syntaktiskt korrekt P-program är ett ganska komplicerat problem (en syntaxanalysator). Vi skall därför välja en annan representation, en representation som mer bygger på den logiska strukturen av ett program. För att till slut kunna implementera datatypen Prog så behöver vi först implementera datatyperna Var, Test,

105 98 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER Expr och Stmt motsvarande de tidigare introducerade mängderna Var, Villkor, Uttryck och Sats. Vi kommer hela tiden att använda den abstrakta syntaxen för att implementera dessa datatyper och antar därför att det finns en syntaxanalysator som kan översätta den konkreta representationen till den abstrakta (se avsnitt 3.5, Abstrakt Syntax, sid 49). Genom att göra på detta sätt kan vi lämna de rent syntaktiska problemen till andra kurser. Vi börjar med att definiera en datatyp för variabler MODULE TYPE: Var type OPERATIONS: mk var(n) Var, om n N dest var(x) N, om x Var EQUATIONS: dest var(mk var(n)) = n END Och implementerar den genom de två uttrycksförkortningarna: och EXPR MK-VAR(Y1) MACRO: -- Y1 : Uttryck Y2 := Y1 RESULT(Y2) EXPR DEST-VAR(Y1) MACRO: -- Y1 : Uttryck -- Y1 repr Var Y2 := Y1 RESULT(Y2) En variabel kommer alltså att representeras av sitt index, till exempel så kommer variabeln X23 av representeras av talet 23. Vid första anblicken, så tycks det enda syftet med de här definitionerna vara att förbruka papper! Men riktigt så illa är det inte, förkortningarna uttrycker typkonverteringar. Förkortningen MK-VAR konverterar ett tal till en variabel och DEST-VAR gör den inversa konverteringen. Nu är det så att vi har valt att representera en variabel just med sitt index och därför blir beräkningen av representationen helt trivial. Många programmerare tycker det är under deras värdighet att skriva sådana här funktioner (förkortningar) och gör istället konverteringarna i huvudet, ett beslut som i många fall kan få ödesdigra konsekvenser. Att byta representationen kan bli mer eller mindre omöjligt och den

106 3.10. P-PROGRAM SOM DATAOBJEKT 99 självklara typkonverteringen kan efter en tid, och för en annan person, verka allt annat än självklar. Det är därför god programmerarstil att alltid vara så explicit som möjligt då man implementerar nya datatyper. Med hjälp av de primitiva operationerna i datatypen kan vi nu implementera andra operationer, till exempel en förkortning som avgör om två variabler är lika EXPR EQVAR(Y1,Y2) MACRO -- Y1 : Uttryck, Y2 : Uttryck -- Y1 repr Var, Y2 repr Var Y3 := DEST-VAR(Y1); Y4 := DEST-VAR(Y2); Y3 := (Y3 = Y4) RESULT(Y3) Vi fortsätter med datatypen för villkor; den definieras på följande sätt: MODULE TYPE: Test type OPERATIONS: mk test(x) Test, om x Var dest test(v) Var, om v Test EQUATIONS: dest test(mk test(x)) = x Var END och vi implementerar den genom förkortningarna MK-TEST och DEST-TEST. EXPR MK-TEST(Y1) MACRO: -- Y1 : Uttryck -- Y1 repr Var Y2 := Y1 RESULT(Y2) Vi förutsätter att argumentet till förkortningen representerar en variabel. Ett villkor representeras således av det naturliga tal som är index till variabeln i villkoret. Vi lämnar implementeringen av DEST-TEST som övning, det skall förhoppningsvis inte bereda några problem. Varje naturligt tal kommer att representera en variabel och ett villkor; talet 59, till exempel, representerar variabeln X59 och villkoret X59 0. I grammatiken för P definieras ett uttryck med hjälp av följande grammatik: <uttryck> ::= 0 SUCC(<variabel>) PRED(<variabel>)

107 100 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER Datatypen Expr definieras därför på följande sätt, jämför med den abstrakta syntaxen och definitionen av mängden Uttryck. MODULE TYPE: Expr type OPERATIONS: mk 0 Expr mk succ(x) Expr, om x Var mk pred(x) Expr, om x Var is 0 (e) Bool, om e Expr is succ(e) Bool, om e Expr is pred(e) Bool, om e Expr dest succ(e) Var, om e Expr dest pred(e) Var, om e Expr EQUATIONS: is 0 (mk 0) = true is 0 (mk succ(x)) = false is 0 (mk pred(x)) = false is succ(mk 0) = false is succ(mk succ(x)) = true is succ(mk pred(x)) = false is pred(mk 0) = false is pred(mk succ(x)) = false is pred(mk pred(x)) = true dest succ(mk succ(x)) = x dest pred(mk pred(x)) = x END För att implementera datatypen definierar vi följande förkortningar: EXPR MK-0 MACRO: Y1 := 2 RESULT(Y1) EXPR MK-SUCC(Y1) MACRO: -- Y1 : Uttryck -- Y1 repr Var Y2 := 3**(Y1+1) RESULT(Y2)

108 3.10. P-PROGRAM SOM DATAOBJEKT 101 EXPR MK-PRED(Y1) MACRO: -- Y1 : Uttryck -- Y1 repr Var Y2 := 5**(Y1+1) RESULT(Y2) Dessa tre förkortningar är tillräckliga för att beskriva hur alla uttryck representeras. Dessutom definierar vi följande primitiva operationer för att avgöra vilken uttrycksform ett givet naturligt tal representerar. Här förutsätter vi att argumentet verkligen representerar ett syntaktiskt korrekt uttryck. EXPR IS-0(Y1) MACRO: -- Y1 : Uttryck -- Y1 repr Expr Y2 := (Y1=2) RESULT(Y2) Observera hur Y1 används! Y1 är en makrovariabel som står för ett godtyckligt P- uttryck; värdet av detta P-uttryck är ett naturligt tal som representerar ett annat P-uttryck. Det krävs att man håller tungan rätt i munnen när man håller på med att representera fraserna i ett språk i termer av uttryck i språket självt. Tänk noga igenom de olika nivåerna!! Vi fortsätter med ytterligare förkortningar för att implementera datatypen Expr. EXPR IS-SUCC(Y1) MACRO: -- Y1 : Uttryck -- Y1 repr Expr Y2 := PRIMEXP(2,Y1); Y3 := 3**Y2; IF (Y2>0) & (Y1=Y3) THEN Y3 := TRUE ELSE Y3 := FALSE RESULT(Y3) -- exp för 2:a primtalet(3) EXPR IS-PRED(Y1) MACRO: -- Y1 : Uttryck -- Y1 repr Expr Y2 := PRIMEXP(3,Y1); Y3 := 5**Y2; IF (Y2>0) & (Y1=Y3) THEN Y3 := TRUE ELSE Y3 := FALSE RESULT(Y3) -- exp för 3:e primtalet(5)

109 102 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER Vi fortsätter med att definiera förkortningar för att selektera ut representationerna för delarna av ett uttryck, givet representationen för hela uttrycket. EXPR DEST-SUCC(Y1) MACRO: -- Y1 : Uttryck -- Y1 repr Expr (SUCC) Y2 := PRIMEXP(2,Y1); Y2 := PRED(Y2) RESULT(Y2) EXPR DEST-PRED(Y1) MACRO: -- Y1 : Uttryck -- Y1 repr Expr (PRED) Y2 := PRIMEXP(3,Y1); Y2 := PRED(Y2) RESULT(Y2) Observera att DEST SUCC(n) bara ger ett meningsfullt resultat om n verkligen är representationen för ett uttryck SUCC(x), och på samma sätt så ger DEST PRED(n) bara ett meningsfullt resultat om n representerar ett uttryck PRED(x). Att implementeringen är korrekt, ser vi av att förkortningarna uppfyller de likheter som definitionen av datatypen kräver. Vi kan också definiera en förkortning som avgör om ett tal överhuvudtaget representerar ett uttryck. EXPR ISEXPR(Y1) MACRO: -- Y1 : Uttryck Y2 := IS-0(Y1) or (ISSUCC(Y1) or ISPRED(Y1)) RESULT(Y2) Den här förkortningen är endast meningsfull som en sorts begränsad typkontroll under exekveringen. Om vi har använt förkortningarna på ett typriktigt sätt, så skall det aldrig hända att vi försöker använda ett tal som inte representerar ett uttryck när ett sådant förväntas. Om vi studerar grammatiken för P, så kan vi se att satserna genereras rekursivt enligt följande grammatik: <sats> ::= <variabel> := <uttryck> WHILE <villkor> DO <sats> END <sats> ; <sats>

110 3.10. P-PROGRAM SOM DATAOBJEKT 103 Om c 1 och c 2 är satser, så är till exempel c 1 ; c 2 en sats. För att representera satserna så definierar vi datatypen Stmt. MODULE TYPE: Stmt type OPERATIONS: mk assign(x, e) Stmt, om x Var och e Expr mk while(v, c) Stmt, om v Test och c Stmt mk seq(c 1, c 2 ) Stmt, om c 1, c 2 Stmt is assign(c) Bool, om c Stmt is while(c) Bool, om c Stmt is seq(c) Bool, om c Stmt dest assign var(c) Var, om c Stmt dest assign expr(c) Expr, om c Stmt dest while test(c) Test, om c Stmt dest while stmt(c) Stmt, om c Stmt dest seq 1 (c) Stmt, om c Stmt dest seq 2 (c) Stmt, om c Stmt EQUATIONS: is assign(mk assign(x, e)) = true is assign(mk while(v, c)) = false is assign(mk seq(c 1, c 2 )) = false END. dest assign var(mk assign(x, e)) = x dest assign expr(mk assign(x, e)) = e. För att implementera datatypen och representera satser som naturliga tal, så definierar vi följande förkortningar: EXPR MK-ASSIGN(Y1,Y2) MACRO: -- Y1 : Uttryck, Y2 : Uttryck -- Y1 repr Var, Y2 repr Expr Y3 := 2**Y1; Y4 := 3**Y2; Y3 := Y3*Y4 RESULT(Y3)

111 104 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER EXPR MK-WHILE(Y1,Y2) MACRO: -- Y1 : Uttryck, Y2 : Uttryck -- Y1 repr Test, Y2 repr Stmt Y3 := 5**Y1; Y4 := 7**Y2; Y3 := Y3*Y4 RESULT(Y3) Sedan kan vi definiera förkortningen som ger representationen av en sekvens. EXPR MK-SEQ(Y1,Y2) MACRO: -- Y1 : Uttryck, Y2 : Uttryck -- Y1 repr Stmt, Y2 repr Stmt Y3 := 11**Y1; Y4 := 13**Y2; Y3 := Y3*Y4 RESULT(Y3) Notera hur vi i definitionerna av förkortningarna förutsätter att argumenten redan representerar satser. Vi definierar också en operation som avgör om ett tal representerar en tilldelningssats: EXPR IS-ASSIGN(Y1) MACRO: -- Y1 : Var, Y1 repr Stmt Y2 := PRIMEXP(1,Y1); Y3 := PRIMEXP(2,Y1); Y2 := (2**Y2)*(3**Y3); Y4 := (Y2 = Y1) RESULT(Y4) Vi lämnar implementeringen av övriga operationer som övning. Slutligen kan vi definera och implementera datatypen för program. Definitionen har följande utseende: MODULE TYPE: Prog type OPERATIONS: mk prog(x 1, c, x 2 ) Prog, om x 1, x 2 Var och c Stmt dest prog var 1 (p) Var, om p Prog dest prog stmt(p) Stmt, om p Prog dest prog var 2 (p) Var, om p Prog

112 3.10. P-PROGRAM SOM DATAOBJEKT 105 END EQUATIONS: dest prog var 1 (mk prog(x 1, c, x 2 )) = x 1 dest prog stmt(mk prog(x 1, c, x 2 )) = c dest prog var 2 (mk prog(x 1, c, x 2 )) = x 2 Och implementeringen består bl.a. av följande förkortning, som ger representationen av program. EXPR MK-PROG(Y1,Y2,Y3) MACRO: -- Y1 : Var, Y2 : Sats, Y3 : Var -- Y1 repr Var, Y2 repr Stmt, Y3 repr Var Y4 := 2**Y1; Y5 := 3**Y2; Y6 := 5**Y3; Y4 := Y4*Y5*Y6 RESULT(Y4) De övriga förkortningarna lämnar vi som övning. De förkortningar som vi har introducerat ger implicit en beräkningsbar kodningsfunktion för program och vi får också en avkodningsfunktion: e Prog Prog N d Prog N Prog Avkodningsfunktionen är partiell, eftersom det finns massor av naturliga tal som inte representerar något korrekt P-program alls. Eftersom d Prog inte är total, ger den inte någon uppräkning av P-programmen, och vi definierar därför den totala funktionen d Prog enligt: d Prog (n) = { d Prog (n), PROGRAM(X0) X0 := 0 RESULT(X0), om n dom(d Prog) annars. d Prog N Prog ger nu en uppräkning av alla P-program: d Prog (0), d Prog (1),.... Vi kommer ibland att använda beteckningen Ix istället för d Prog. Ix(q) kan läsas som P-programmet med index (eller koden) q. Observera att d Prog (och Ix) är en beräkningsbar funktion, vi kan ju mekaniskt avgöra om ett tal representerar ett P-program eller ej, det är ju precis det syntaxanalysatorn i en kompilator gör. Givet ett tal kan vi också mekaniskt erhålla det program som talet representerar; en början till detta kan man se i förkortningarna DEST-...

113 106 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER Eftersom varje P-program representerar en P-beräkningsbar funktion i N N, får vi dessutom en uppräkning av de P-beräkningsbara, partiella funktionerna i mängden N N. Den partiella funktion som associeras med talet n betecknar vi φ n (en annan beteckning som också används är {n}). Så φ n N N och vi kallar n för funktionens index. Om vi har ett tal n, så är Ix(n) det program som har koden n och φ n = P[Ix(n)] den partiella funktion som beräknas av programmet. Därför gäller det att φ n (m) = k Ix(n), m k Låt oss sammanfatta beteckningarna: q är koden till P-programmet Q; q = e Prog (Q) q är index till den funktion som beräknas av Q Q beräknar funktionen P[Q] Ix(q) är P-programmet med index(kod) q φ q är funktionen med index q; P[Q] = φ q Så till varje P-beräkningsbar funktion ψ, finns det ett index q, sådant att ψ = φ q. I själva verket finns det ett oändligt antal index till funktionen, eftersom vi kan ta ett P-program Q, som beräknar ψ, och modifiera detta för att erhålla en oändlig sekvens av program: Q 1, Q 2, Q 3,... som alla är syntaktiskt olika och därför har olika kod (index), men som alla beräknar funktionen ψ. Programmet Q k konstrueras från Q genom att sätta in k SKIP-satser i början av programmet. Q = PROGRAM(Xi) c RESULT(Xj) Q k = PROGRAM(Xi) SKIP; SKIP;... ; SKIP; }{{} k stycken c RESULT(Xj) Övning 3.32 Om man tycker det är konstigt att ett tal kan representera både ett villkor och en variabel, kan man då ändra representationen så att detta inte blir fallet? Övning 3.33 Hur representeras satsen X1:=0; X1:=SUCC(X1)? Övning 3.34 Definiera några av följande förkortningar:

114 3.10. P-PROGRAM SOM DATAOBJEKT 107 a. IS-WHILE(Y1) b. IS-SEQ(Y1) c. DEST-ASSIGN-VAR(Y1) (ger en representation av variabeln i tilldelningssatsen som Y1 förutsätts representera) d. DEST-ASSIGN-EXPR(Y1) e. DEST-WHILE-TEST(Y1) f. DEST-WHILE-STMT(Y1) g. DEST-SEQ-1(Y1) h. DEST-SEQ-2(Y1) Övning 3.35 Definiera några av följande förkortningar: a. IS-PROG(Y1) b. DEST-PROG-VAR-1(Y1) c. DEST-PROG-STMT(Y1) d. DEST-PROG-VAR-2(Y1) Övning 3.36 Om vi tänker oss att varje naturligt tal representerar ett P-program, såsom funktionen d Prog indikerar, hur kommer då förkortningarna IS-PROG, DEST-PROG-VAR-1, osv. att förändras.

115 108 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER 3.11 En interpretator för P-program skriven i P I förra avsnittet beskrev vi ett sätt att representera P-program som naturliga tal. Vi har tidigare också definierat en representation av ordnade par i termer av naturliga tal. Det är därför möjligt att tänka sig att indata till ett P-program representerar ett par n = n Q, n in där vi tolkar första komponenten som en representation av P-programmet Q = d Prog (n Q ) och andra komponenten som ett vanligt naturligt tal. Vårt mål i detta avsnitt är nu att skriva ett P-program U, som för denna tolkning av indata beter sig exakt som programmet Q med indata n in skulle göra. Dvs. om Q, n in m, så skall U, n m, och om Q, n in inte konvergerar, så skall inte heller U, n konvergera. Programmet U skall alltså vara en interpretator för P-program. Vi börjar med att definiera och implementera en datatyp State, som ska motsvara mängden av tillstånd som vi introducerade på sidan 59. Datatypen State består av följande primitiva operationer: omega State update(x, n, r) State om x Var, n N och r State valueof (x, r) N om x Var och r State motsvarar tillståndet Ω motsvarar att tillståndet appliceras på variabeln. Denna datatyp motsvarar inte helt mängden av tillstånd, eftersom vi inte kan erhålla alla tillstånd med hjälp av dessa operationer. Tillståndet som tillordnar talet 1 till alla variabler kan till exempel inte erhållas genom att använda operationerna ett ändligt antal gånger. Detta är dock inget problem i interpretatorn då alla tillstånd som vi behöver i denna kan fås med de tillgängliga operationerna. Om vi skriver ut datatypen, så får vi: MODULE TYPE: State type OPERATIONS: omega State update(x, n, r) State, om x Var, n N och r State valueof (x, r) N, om x Var och r State EQUATIONS: valueof (x, omega) = 0 valueof (x, update(y, n, r)) = if eqvar 3 (x, y) then n else valueof (x, r) END

116 3.11. EN INTERPRETATOR FÖR P-PROGRAM SKRIVEN I P 109 För att implementera denna datatyp behöver vi en representation av tillstånd. Vi väljer att representera ett tillstånd som en lista av variabelindex-värde par, dvs. som objekt i datatypen List(N N). Tillståndet Ω representeras av den tomma listan och tillståndet Ω[X1 1, X2 4, X9 9] representeras av listan [ 1, 1 ; 2, 4 ; 9, 9 ] De primitiva operationerna implementeras med hjälp av förkortningarna OMEGA, UPDATE och VALUEOF. UPDATE kan definieras på följande sätt: EXPR UPDATE(Y1,Y2,Y3) MACRO: -- Var N State -- Y1 : Uttryck, Y2 : Uttryck, -- Y3 : Uttryck -- Y1 repr Var, Y3 repr S Y4 := 0; -- Y4=1 om uppdaterad Y5 := NIL; WHILE NOT(NULL(Y3)) DO Y6 := HD(Y3); Y7 := FST(Y6); -- variabel IF Y1 = Y7 THEN Y4 := 1; IF Y2 0 THEN Y5 := Y1,Y2.Y5; FI ELSE Y5 := Y6.Y5 FI; Y3 := TL(Y3) END; IF Y4=0 THEN IF Y2 0 THEN Y5 := Y1,Y2.Y5 FI FI RESULT(Y5) Observera att ordningen i listan ändras av UPDATE, men eftersom det i listan bara kan finnas ett par x, n för varje variabel x så gör detta inget. Implementeringen av de två övriga förkortningarna lämnas som övning. Interpretatorn har följande utseende: 3 eqvar Var Var Bool är likhetsfunktionen för variabler; eqvar(x, y) = true omm x = y Var.

117 110 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER PROGRAM (X0) -- INTERPRETATOR FÖR P X1 := FST(X0); -- program X2 := SND(X0); -- indata X4 := DEST-PROG-VAR-1(X1); -- invariabel X5 := OMEGA; -- tillstånd X5 := UPDATE(X4,X2,X5); X6 := DEST-PROG-STMT(X1); -- satser WHILE X6 0 DO -- Låt X7 innehålla nästa sats. IF IS-SEQ(X6) THEN X7 := DEST-SEQ-1(X6); X6 := DEST-SEQ-2(X6); -- X7 fär inte vara en sekvens WHILE IS-SEQ(X7) DO X6 := MK-SEQ(DEST-SEQ-2(X7),X6); X7 := DEST-SEQ-1(X7) END ELSE X7 := X6; X6 := 0 FI; IF IS-ASSIGN(X7) THEN X5 := EVAL-ASSIGN(X7,X5); ELSE -- IS-WHILE(X7) X8 := DEST-WHILE-TEST(X7); X8 := EVAL-TEST(X8,X5); IF X8 0 THEN X9 := DEST-WHILE-STMT(X7); IF X6 0 THEN X6 := MK-SEQ(X7,X6); X6 := MK-SEQ(X9,X6) ELSE X6 := MK-SEQ(X9,X7) FI FI FI END; X10 := DEST-PROG-VAR-2(X1); -- resultatvariabel X10 := VALUEOF(X10,X5) RESULT(X10) Vi kommer i fortsättningen att använda en enkel form av mönster istället för

118 3.11. EN INTERPRETATOR FÖR P-PROGRAM SKRIVEN I P 111 indata-variabel. Vi kommer till exempel att skriva: istället för: PROGRAM( X1,X2 )... PROGRAM(X0) X1 := FST(X0); X2 := SND(X1);... I definitionen av interpretatorn har vi använt förkortningarna EVAL-ASSIGN och EVAL-TEST. Den första av dessa förkortningar kan definieras på följande sätt: EXPR EVAL-ASSIGN(Y1,Y2) MACRO: -- Y1 : Uttryck, Y2 : Uttryck -- Y1 repr Stmt, Y2 repr State Y3 := DEST-ASSIGN-VAR(Y1); Y4 := DEST-ASSIGN-EXPR(Y1); IF IS-0(Y4) THEN Y4 := UPDATE(Y3,0,Y2) ELSE IF IS-SUCC(Y4) THEN Y4 := DEST-SUCC(Y4); Y4 := VALUEOF(Y4,Y2); Y4 := SUCC(Y4); Y4 := UPDATE(Y3,Y4,Y2) ELSE Y4 := DEST-PRED(Y4); Y4 := VALUEOF(Y4,Y2); Y4 := PRED(Y4); Y4 := UPDATE(Y3,Y4,Y2) FI RESULT(Y4) FI -- variabel -- värde Definitionen av EVAL-TEST lämnas som övning. Vi är nu färdiga med vår uppgift. Programmet ovan fungerar enligt specifikationen för U, det är en interpretator för P-program. Den uttrycks-förkortning som motsvarar detta program kallar vi för EVAL-PROG(Y1,Y2). Dess definition börjar: EXPR EVAL-PROG(Y1,Y2) MACRO: -- Y1 : Uttryck, Y2 : Uttryck -- Y1 repr Prog

119 112 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER Programmet U beräknar en partiell funktion Φ N N. Utgående från denna kan vi definiera en ny funktion Φ N N N enligt: Φ = Φ e N N där e N N är kodningsfunktionen för ordnade par av naturliga tal. Φ kallas för en universell funktion och det gäller att: Φ(i, n) = φ i (n) Vi ska nu gå vidare med att implementera en variant av interpretatorn som alltid terminerar. I många sammanhang kan man inte släppa lös interpreteringen av ett program helt utan förbehåll som man gör om man använder EVAL-PROG, utan man vill ha en möjlighet att prova om interpreteringen hinner konvergera på ett bestämt antal steg. Ta till exempel funktionen χ N N N {1} med definition: χ(q, r, n) = { 1, odefinierad, om φ q (n) = 1 eller om φ r (n) = 1 annars. Kan den beräknas? Ett första försök är programmet V med följande definition: PROGRAM( X1,X2,X3 ) -- X1,X2,X3 = X1, X2,X3 X4 := EVAL-PROG(X1,X3); IF X4 1 THEN X4 := EVAL-PROG(X2,X3); IF X4 1 THEN WHILE X4 1 DO SKIP END FI FI RESULT(X4) Men V beräknar inte χ, ty om det första argumentet är index för ett program som inte terminerar, till exempel om det är lika med q = e prog (Q), där Q är programmet: PROGRAM(X0) X1 := 1; WHILE X1 0 DO SKIP END RESULT(X1) så terminerar V inte för indata q, x, y, oberoende av vad x och y är. Programmet V kan därför inte beräkna χ, eftersom χ(q, r, n) är lika med 1 om r är index för programmet:

120 3.11. EN INTERPRETATOR FÖR P-PROGRAM SKRIVEN I P 113 PROGRAM(X0) X0:=0; X0:=SUCC(X0) RESULT(X0) Om vi vill skriva ett program som ska beräkna χ(q, r, n), så kan vi inte starta beräkningen av φ q (n) utan kontroll. Det kan ju vara fallet att n dom(φ q ), vilket innebär att beräkningen aldrig terminerar och det finns då ingen möjlighet att testa om φ r (n) = 1. För att lösa problem som dessa, så kommer vi att definiera en variant av interpretatorn som alltid terminerar. Vi definierar en uttrycks-förkortning EVAL-PROG-STEP(Y1,Y2,Y3), där Y1 är en representation av ett program Q, Y2 står för indata till Q och Y3 bestämmer hur många steg som interpretatorn skall exekvera programmet. Resultatet av EVAL-PROG-STEP(Y1,Y2,Y3) är tänkt att representera ett par b, n, där b = 1 och n = m om interpreteringen av Q med Y2 som indata konvergerar med resultat m på mindre än Y3 steg. Om interpreteringen inte konvergerar på Y3 steg, så skall resultatet representera paret 0, 0. Vi definierar uttrycks-förkortningen enligt följande (jämför med definitionen av interpretatorn): EXPR EVAL-PROG-STEP(Y1,Y2,Y3) MACRO: -- Prog N Steg -- Y1 : Uttryck, Y2 : Uttryck, -- Y3 : Uttryck -- Y1 repr Prog Y4 := DEST-PROG-VAR-1(Y1);... Y6 := DEST-PROG-STMT(Y1); WHILE (Y3 0) & (Y6 0) DO -- OBS IF IS-SEQ(Y6)... FI; IF IS-ASSIGN(Y7)... FI; Y3 := PRED(Y3) -- OBS END; IF Y OBS THEN Y10 := DEST-PROG-VAR-2(Y1); -- OBS Y10 := VALUEOF(Y10,Y5); -- OBS Y10 := 1,Y10 -- OBS ELSE Y10 := 0,0 -- OBS FI RESULT(Y10) Nu kan vi skriva ett program som beräknar χ(q, r, n) genom att fläta samman

121 114 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER beräkningarna av φ q (n) och φ r (n). Vi definierar ett program V som beräknar χ enligt: PROGRAM( X1,X2,X3 ) X4 := 1; X5 := 1; -- Prog Prog N -- =0 om resultatet är funnet -- antal steg WHILE X4 0 DO X6 := EVAL-PROG-STEP(X1,X3,X5); X7 := SND(X6); -- X1 har terminerat med 1 IF X7=1 THEN X4 := 0 FI; X6 := EVAL-PROG-STEP(X2,X3,X5); X7 := SND(X6); -- X2 har terminerat med 1 END; X8 := 1 RESULT(X8) IF X7=1 THEN X4 := 0 FI; X5 := SUCC(X5) -- öka antalet steg Programmet V beräknar funktionen χ av följande skäl: Enligt definitionen är χ(q, r, n) = 1, om φ q (n) = 1 eller om φ r (n) = 1, och odefinierad annars. Men om Ix(q), n 1 eller om Ix(r), n 1, så får vi resultatet efter ett visst antal steg i interpretatorn, säg att vi får det efter k steg. Det vill säga, att om resultatet kommer från Ix(q), så kommer resultatet av EVAL PROG STEP(q, n, k) att bli 1,1, och då kommer V att terminera med resultatet 1. Om varken Ix(q), n eller Ix(r), n ger resultat 1, så kommer V aldrig att terminera, vilket motsvarar att funktionen χ är odefinierad. Vi ser att det är omöjligt att använda den vanliga interpretatorn för att beräkna funktionen χ. Den viktiga skillnaden mellan de två interpretatorerna är att EVAL-PROG-STEP alltid terminerar, medan EVAL-PROG ibland inte gör det. Övning 3.37 Implementera förkortningarna VALUEOF och OMEGA. Övning 3.38 Implementera EVAL-TEST. Övning 3.39 Vi får en alternativ interpretator genom att direkt koda beräkningshärledningar som naturliga tal. Vi har tidigare sett hur man kan koda program, satser, par, tillstånd, etc. som naturliga tal. Det går till och med att koda hela beräkningshärledningar som tal, och inte nog med det, det går dessutom att mekaniskt avgöra om ett tal kodar en beräkningshärledning samt att selektera

122 3.11. EN INTERPRETATOR FÖR P-PROGRAM SKRIVEN I P 115 ut komponenter ut härledningen. Vi säger att talet s kodar härledningen som slutar med [P]-regeln om r är en kod för en härledning av premissen och s = e N (N (N N)) (e Prog (PROGRAM(x) c RESULT(x )), n, σ(x ), r). Definiera koden för härledningar som slutar med de andra beräkningsreglerna! Definiera också uttrycksförkortningar som ger olika komponenter i koden för en härledning, t.ex. förkortningar IS-PPROG(Y1,Y2) som avgör om Y1 är koden för programmet i härledningskoden Y2; komponenter som behövs för att man skall kunna skriva interpretatorn för P på följande sätt: EXPR ALT-EVAL-PROG(Y1,Y2) MACRO: -- Y1 : Uttryck, Y2 : Uttryck -- Y1 repr Prog Y3 := 0; Y4 := 0; WHILE Y4=0 DO IF IS-PDERIVATION(Y3) & IS-PPROG(Y1,Y3) & IS-INPUT(Y2,Y3) THEN Y5:=DEST-RESULT(Y3); Y4:=1 ELSE Y3:=SUCC(Y3) FI END RESULT(Y5) I interpretatorn räknar Y3 upp alla möjliga koder för en härledning. För varje kod testas om Y3 är en härledning för det givna programmet och och det givna indata, och slutligen selekteras resultatet (Y5) ut från härledningen. Detta ger oss en mycket enkel, men tyvärr också en mycket ineffektiv, interpretator för P.

123 116 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER 3.12 Några icke-beräkningsbara funktioner Vi skall i detta avsnitt visa exempel på partiella funktioner som inte är beräkningsbara. I ett tidigare avsnitt visade vi hur man kunde representera P-program som naturliga tal med hjälp av kodningsfunktionen e Prog. Vi såg också hur man kunde gå bakvägen och från ett tal n, med hjälp av funktionen d Prog, bestämma vilket P-program detta tal representerade. Det är alltså möjligt för ett P-program att tolka indata som om det vore kod för ett P-program. De flesta (alla?) funktioner som vi ska visa att det inte går att beräkna kommer att ha åtminstone ett argument som tolkas som kod för ett program. Betrakta funktionen Θ N {0, 1} med följande definition: { 1, om Ix(n), n Θ(n) = 0, annars. Med andra ord, Θ(n) är 1, om det existerar ett naturligt tal m, som är sådant att programmet med index n terminerar med resultat m, om det beräknas med n som indata, och 0 om så inte är fallet. Den här definitionen tål att läsas igenom både en och två gånger! Så Θ är en funktion som tar ett programindex n som indata och ger 1 som resultat om programmet Ix(n) konvergerar för indata n och 0 annars. Dvs. Θ avgör om beräkningen Ix(n), n terminerar eller ej. Frågan är nu: Är Θ P-beräkningsbar? Svaret kommer i följande sats. Sats 3.21 Funktionen Θ är inte P-beräkningsbar. Bevis: Vi börjar med att antaga att Θ är beräkningsbar och härleder sedan en motsägelse från detta. Eftersom Θ, enligt antagandet, är beräkningsbar, så måste det finnas ett P-program som beräknar Θ, låt oss kalla detta program Q Θ. Från programmet kan vi enkelt få en uttrycks-förkortning QM Θ ; det räcker ju att byta ut PROGRAM(Xi) mot EXPR QM Θ (Yi) MACRO: och alla variabler Xi mot makrovariabler Yi. Med hjälp av denna förkortning kan vi skriva följande program: PROGRAM (X0) ( R) X1 := QM Θ (X0); WHILE X1 0 DO SKIP END RESULT(X1) Vi kallar det här programmet för R och ser att det har följande egenskaper: 1. Det konvergerar inte för indata n, om QM Θ (n) beräknas till ett värde som är skilt från 0. Men QM Θ beräknar funktionen Θ och Θ(n) är skild från 0 precis då n representerar ett program som är sådant att det konvergerar om det beräknas med n som indata. Dvs. R konvergerar ej om indata representerar ett program som konvergerar (för indata n). I symboler får vi

124 3.12. NÅGRA ICKE-BERÄKNINGSBARA FUNKTIONER 117 R, n om Θ(n) = 1, dvs. om Ix(n), n 2. Det konvergerar med resultatet 1 för indata n, om QM Θ ( n ) beräknas till 0, dvs. om Θ(n) = 0. Men Θ(n) är bara lika med 0 om n representerar ett program som inte konvergerar för indata n. Dvs. R konvergerar om indata representerar ett program som ej konvergerar (för indata n). I symboler R, n om Θ(n) = 0, dvs. om Ix(n), n Men R är ju ett P-program och det har därför ett index, e Prog (R), kalla detta för r. Vad händer nu om vi låter r vara indata till programmet R? Enligt egenskap (1) ovan konvergerar R ej för indata r, om r representerar ett program som konvergerar för indata r, men enligt våra förutsättningar så är r index för just programmet R! Programmet R har alltså egenskapen att det inte konvergerar för indata r precis då det konvergerar för indata r!! På samma sätt så får vi från egenskap (2) att R inte konvergerar för indata r precis då det konvergerar för indata r. 1. Om vi bara sätter in r istället för n i den första likheten ovan så får vi: Men R = Ix(r), så 2. På samma sätt får vi R, r om Θ(r) = 1, dvs. om Ix(r), r R, r om R, r R, r om R, r om vi sätter in r istället för n i den andra formeln. Dessa två villkor, tillsammans med villkoret att ett program antingen konvergerar eller divergerar ger oss vår motsägelse. Vårt antagande att funktionen Θ är beräkningsbar måste alltså vara felaktigt. Funktionen Θ är inte beräkningsbar. Vi har hittat ett exempel på en funktion som inte är beräkningsbar och som konsekvens kan vi se att det inte är någon idé att försöka skriva ett program som avgör om andra program terminerar då de ges sig själva som indata. I och för sig finns det ju inte så många program som är avsedda att ta sig själva som indata, så vid första anblicken kanske konsekvenserna av satsen inte syns vara så stora. Men vi skall snart se att det från den här satsen följer att vi kan visa att många

125 118 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER andra funktioner, och i flera fall kanske mer intressanta funktioner än Θ, inte är beräkningsbara. Vi har nu sett en metod att bevisa att en funktion f inte är beräkningsbar. I stort sett så gick den ut på att från antagandet att f är beräkningsbar härleda en motsägelse om f:s beteende, och därifrån sedan dra slutsatsen att f inte kan vara beräkningsbar. Vi ska nu visa att det finns andra funktioner som inte heller är beräkningsbara och vi skall då använda en något annorlunda teknik. För att visa att g inte är beräkningsbar, så antar vi först att g är beräkningsbar och visar sedan att då måste också funktionen Θ vara beräkningsbar. Men från sats 3.21 vet vi att så inte är fallet; vi har alltså återigen härlett en motsägelse och det måste då gälla att g inte är beräkningsbar. Vi fortsätter med att definiera funktionen stopp N N {0, 1} enligt: stopp(p, n) = { 1, 0, om Ix(p), n annars; och bevisar sedan satsen: Sats 3.22 Funktionen stopp N N {0, 1} är inte beräkningsbar. Bevis: Antag att stopp är beräkningsbar då finns ett program Q stopp som beräknar funktionen. Det finns då också en uttrycksförkortning, QM stopp, som precis motsvarar Q stopp. Med hjälp av denna förkortning kan vi definiera ett program enligt: PROGRAM(X0) X1 := QM stopp (X0,X0) RESULT(X1) Vi ser med en gång att detta program beräknar funktionen Θ, men denna är enligt sats 3.21 inte beräkningsbar. Vi har alltså härlett en motsägelse och vårt antagande måste vara felaktigt. Funktionen stopp är inte beräkningsbar. Det finns alltså ingen möjlighet att skriva ett program som avgör om ett godtyckligt program Q konvergerar för indata n, eller om det inte gör det. Men det blir genast annorlunda om vi modifierar funktionen lite grand, till exempel om vi definierar en funktion som bara ger ett resultat om indata-programmet terminerar. Då får vi en beräkningsbar funktion definiera funktionen stopp N N {1} enligt: stopp (p, n) = { 1, odefinierad, om Ix(p), n annars. Det är lätt att se att denna är beräkningsbar, den beräknas ju av följande program:

126 3.12. NÅGRA ICKE-BERÄKNINGSBARA FUNKTIONER 119 PROGRAM( X1,X2 ) X3 := EVAL-PROG(X1,X2); X3 := 1 RESULT(X3) Vi har sett att det inte går att skriva ett program som kan avgöra om ett godtyckligt program terminerar för ett givet indata. Man skulle kanske kunna tro att en orsak till detta är att det finns så många komplicerade program, och att det därför kanske skull gå att avgöra om ett program terminerade om man bara betraktade en klass av enkla program. Vi skall därför visa att stopp-funktionen inte ens går att beräkna om vi bara betraktar alla konstanta program. Låt oss kalla ett program K för ett konstant program om in-variabeln (dvs. variabeln som följer omedelbart efter ordet PROGRAM ) aldrig används på någon annan plats i programmet. Vi ser genast att antingen terminerar ett konstant program inte, dvs. det beräknar funktionen Λ, eller också beräknar det en konstant funktion, dvs. det finns ett naturligt tal l N så att: eller uttryckt annorlunda P[K ](n) = l, för alla n N. K, n l, för alla n N. Lägg märke till att det är ett syntaktiskt villkor om ett program är ett konstant program eller ej. Det är därför möjligt att mekaniskt kan avgöra om ett tal är index för ett konstant program. Funktionen k prog N {0, 1} med definition: k prog( q) = { 1, 0, om Ix(q) är ett konstant program annars är alltså beräkningsbar. Vi lämnar detta som övning. Att säga att ett program beräknar en konstant funktion, dvs. att programmet ger samma utdata oberoende av indata, är däremot ett semantiskt villkor som inte går att bestämma mekaniskt. Vi återkommer till detta längre fram. Innan vi kan bevisa att stopp funktionen för konstanta program inte går att beräkna, måste vi introducera en annan funktion. Vi har tidigare sett exempel på program som tagit (index till) program som indata. Det är inte svårt att tänka sig möjligheten att ett program kan ge ett (index till ett) program som resultat. Vi skall nu definiera ett program, eller snarare en beräkningsbar funktion, s 1 N N N, där både första argumentet och resultat är program (index). s 1 tar alltså ett program (index), q, och ett naturligt tal n som indata, och ger ett program(index) som resultat. Om vi antar att q är index för programmet Q:

127 120 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER PROGRAM(Xi) ( Q) c RESULT(Xj) så är s 1 (q, n) index för följande program: PROGRAM(Xmax +1) Xi := n; c RESULT(Xj) Vi kan se att programmet s 1 (q, n) är ett konstant program för vilket det gäller att: φ s1 (q,n)(m) = φ q (n) för alla m N Med andra ord, det konstanta programmet (med index) s 1 (q, n) har för alla indata samma uppträdande som programmet(med index) q har för indata n. Om Q, n konvergerar med resultatet n, så gör också Ix(s 1 (q, n)) det, och om Q, n inte terminerar så terminerar inte heller Ix(s 1 (q, n)) för något indata. Eftersom funktionen s 1 är beräkningsbar kan vi visa följande sats: Sats 3.23 Stopp-funktionen för konstanta program, dvs. stopp k (p) = { 1, 0, om Ix(p) är ett konstant program och Ix(p) annars är inte beräkningsbar. Bevis: Antag att stopp k är beräkningsbar då finns ett program Q k och en motsvarande uttrycks-förkortning QM k som beräknar stopp k. Låt dessutom SM 1 vara en uttrycksförkortning som beräknar funktionen s 1. Vi kan då definiera ett program enligt: PROGRAM( X1,X2 ) X3 := SM 1 (X1,X2); X4 := QM k (X3) RESULT(X4) -- Prog N Programmet fungerar på följande sätt: Indata tolkas som ett par, där första komponenten är ett programindex och där andra komponenten är detta programs indata; Först beräknas det motsvarande konstanta programmet och sedan avgörs om detta program konvergerar eller ej. Programmet beräknar alltså den allmänna stopp-funktionen, vilken vi tidigare har visat vara icke-beräkningsbar vi har alltså en motsägelse, och stopp k kan inte vara beräkningsbar.

128 3.12. NÅGRA ICKE-BERÄKNINGSBARA FUNKTIONER 121 Vi lämnar beviset av att s 1 är beräkningsbar som övning. Ett intressant problem i datalogi är att upptäcka död kod, dvs. satser i ett program som aldrig kommer att exekveras under någon beräkning, oberoende av vad indata till programmet är. Ett exempel på död kod är satserna c 1 ;... ; c n i programmet: PROGRAM(Xi)... X1 := 0;... RESULT(Xj) IF X1 0 THEN c 1 ;... ; c n FI; Eftersom dessa satser bara är till besvär, de tar upp minnesutrymme, tar tid att kompilera, etc., så vore det bra att ha ett program som automatiskt kunde märka ut all död kod i ett program. För att kunna göra detta måste vi kunna avgöra om en given sats är död. Detta förutsätter en numrering av satserna i ett program samt en närmare precisering av vad vi menar med att en while-sats exekveras. Låt oss numrera satserna på enklast möjliga vis uppifrån och låt oss säga att en while-sats exekveras om dess villkor exekveras. Här följer ett exempel på hur satserna numreras i ett program: PROGRAM(X0) X0 := 0; 1 X1 := SUCC(X0); 2 WHILE X1 0 DO 3 X0 := SUCC(X0); 4 X1 := PRED(X1) 5 END; X2 := 0 6 RESULT(X0) Tyvärr är det inte möjligt att skriva ett program som märker ut död kod, vilket vi nu skall visa. Men först definierar vi påståendet Exec sats nr(p, n, m) att vara sant om sats nummer m exekveras i beräkningen av Ix(p), n. Målet är alltså att visa att det är omöjligt att skriva ett program som givet ett programindex och ett satsnummer avgör om satsen kan bli exekverad, dvs. att funktionen ρ N N {0, 1} med definition: ρ(p, m) = { 1, 0, om det finns n sådant att Exec sats nr(p, n, m) annars inte är beräkningsbar.

129 122 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER Vi börjar med att visa att inte ens den enklare funktionen som avgör om en sats exekveras i ett program för ett givet indata är beräkningsbar. Definiera funktionen ρ N N N {0, 1} enligt: ρ (p, n, m) = { 1, 0, om Exec sats nr(p, n, m) annars och visa följande sats: Sats 3.24 Funktionen ρ är inte beräkningsbar. Bevis: Antag att Q är ett program med index q, med max som sitt högsta variabelindex och med följande utseende: PROGRAM(Xi) c RESULT(Xj) Vi definierar en uttrycks-förkortning RM som givet ett program-index, p, ger ett par p, n som resultat, där p är index till programmet: PROGRAM(Xi) c; Xmax +1 := 0 RESULT(Xj) och n är numret på satsen Xmax +1 := 0. Uttrycksförkortningen RM tar alltså ett program-index som argument och ger ett par, bestående av ett nytt programindex och ett naturligt tal, som resultat. Om vi nu antar att ρ är beräkningsbar, så finns det ett program, P ρ, och en uttrycks-förkortning, PM ρ, som beräknar funktionen. Med hjälp av denna uttrycksförkortning och RM, som beskrevs ovan, kan vi definiera programmet: PROGRAM( X1,X2 ) X3 := RM(X1); X4 := FST(X3); X5 := SND(X3); X6 := PM ρ (X4,X2,X5) RESULT(X6) Detta program beräknar funktionen stopp eftersom den sista satsen i X4 exekveras precis om Ix(X1), X2 konvergerar!! Men vi vet att funktionen stopp inte är beräkningsbar och därför kan inte heller ρ vara det.

130 3.12. NÅGRA ICKE-BERÄKNINGSBARA FUNKTIONER 123 Beviset går alltså ut på att om vi kunde avgöra om en sats var död för ett givet indata, då kunde vi speciellt kunna avgöra om den sista satsen i ett program var död. Men den satsen exekveras ju endast då programmet terminerar, och det måste därför vara minst lika svårt att avgöra om den satsen kommer att exekveras som att avgöra om programmet terminerar. Vi har nu visat att man inte kan skriva ett program som givet ett programindex q, ett satsnummer v och ett naturligt tal n kan avgöra om sats nummer v används under exekveringen av Ix(q), n. Vi skall gå vidare och visa att då är det inte heller möjligt att skriva ett program som märker ut död kod. Intuitivt verkar det naturligt att det är så. Att märka ut död kod, eller annorlunda uttryckt, att avgöra om sats nummer v används under exekveringen av Ix(q), n för något n, verkar vara ett svårare problem än att för ett givet n avgöra samma sak. Sats 3.25 Funktionen ρ N N {0, 1} är inte beräkningsbar. Bevis: Vi visar satsen på följande sätt: Konstruera en uttrycksförkortning som transformerar ett program (index) q för ett godtyckligt program till ett program (index) r för ett konstant program som beter sig sig likadant som Ix(q), n. En sats i det erhållna programmet exekveras för något indata precis om den motsvarande satsen i Ix(q) exekveras för indata n. Men detta leder till en motsägelse, eftersom om vi hade ett program som kunde avgöra om en godtycklig sats i Ix(q) exekveras för något indata, så skulle vi kunna avgöra om satsen exekverades för indata n, och från föregående sats vet vi att detta är omöjligt. Nu till detaljerna. Antag att ρ är beräkningsbar och att förkortningen RM beräknar den. Vi använder också uttrycksförkortningen SM 1, som beräknar funktionen s 1 (se sid 119), för att transformera ett programindex p och ett naturligt tal n till index för ett konstant program som beter sig likadant som Ix(q), n. Notera att SM 1 (q, n) är ett konstant program, dvs. exekveringen är oberoende av indata, och att programmet exekveras precis som Ix(q), n förutom den inledande sekvensen av tilldelningar. Lägg också märke till att satsen med nummer v + (n + 1) precis motsvarar satsen med nummer v i Ix(q). (expansionen av Xi := n ger ju n + 1 stycken satser: Xi := 0; Xi := SUCC(Xi) Xi := SUCC(Xi) ) }{{} n+1 stycken Vi definierar nu följande program PROGRAM ( X0,X1,X2 ) X0 := SM 1 (X0,X2); X3 := RM(X0,X1+X2+1) RESULT (X3) -- PGM SATSNR N Det här programmet beräknar funktionen ρ. Tänk noga igenom varför! Men ρ bevisade vi ju tidigare vara icke beräkningsbar. Vi har erhållit en motsägelse,

131 124 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER och antagandet att ρ skulle vara beräkningsbar måste vara felaktigt. ρ är inte beräkningsbar! Vi ska slutligen visa två satser som handlar om svårigheten att mekaniskt avgöra vilken partiell funktion ett givet index hör ihop med. Vi börjar med att visa att det inte går att mekaniskt avgöra om en beräkningsbar funktion med index q är lika med identitetsfunktionen, id N. Definiera först funktionen: eq id( x) = { 1, 0, om φ x = id N annars. Vi kan sedan visa satsen: Sats 3.26 eq id är inte beräkningsbar Bevis: Givet ett programindex q och ett naturligt tal n, så kan vi mekaniskt räkna ut index för följande program: PROGRAM(X0) X1 := EVAL-PROG(q,n) RESULT(X0) Låt oss säga att detta görs av den beräkningsbara funktionen g N N N. Men g(q, n) blir index till ett program som beräknar identitetsfunktionen precis då beräkningen av q, n konvergerar. Så om vi kan avgöra om ett naturligt tal är index för identitetsfunktionen, så kan vi speciellt avgöra om g(q, n) är ett sådant index, dvs. om q, n konvergerar. Men vi vet att detta inte är möjligt alltså är det inte heller möjligt att eq id är beräkningsbar. Som en direkt följd av denna sats, kan vi visa att det för två godtyckliga programindex inte är möjligt att avgöra om de kodar samma funktion. Sats 3.27 Det finns inget program som kan avgöra om två godtyckliga program (-index) beräknar samma funktion. Bevis: Det är enkelt att bestämma index för programmet: PROGRAM(X0) SKIP RESULT(X0) som beräknar identitetsfunktionen. Kalla detta index q id. Men om vi kunde avgöra om två index beräknade samma funktion, så skulle vi speciellt kunna avgöra om ett godtyckligt index beräknade samma funktion som q id. Men från föregående sats vet vi att detta inte är möjligt det kan då inte heller vara möjligt att mekaniskt avgöra om två godtyckliga program(index) beräknar samma funktion.

132 3.12. NÅGRA ICKE-BERÄKNINGSBARA FUNKTIONER 125 Eftersom vi i ett programmeringsspråk alltid representerar funktioner med något slags kod, kan vi från den här satsen dra slutsatsen att vi aldrig kan ha en riktig likhetsoperator för funktioner i våra programmeringsspråk. Övning 3.40 I beviset av att funktionen Θ inte är beräkningsbar härledde vi en motsägelse från villkoren att 1. R, r om R, r 2. R, r om R, r 3. varje beräkning konvergerar eller divergerar Kan man inte få en motsägelse direkt från 1, dvs. kan det finnas program som uppfyller villkor 1? Övning 3.41 Visa att funktionen max index N N är beräkningsbar. Resultatet av max index(q) är det högsta variabelindex i programmet Q = Ix(q). Övning 3.42 Visa att k prog är beräkningsbar. Övning 3.43 Visa i detalj varför s 1, som definierades på sidan 119, är beräkningsbar. Övning 3.44 Definiera funktionen ζ 1 N N N enligt: ζ 1 (x, y) = { y, odefinierad, om Ix(x), x annars. Är ζ 1 beräkningsbar? Övning 3.45 Är funktionen: ζ 2 (x, y) = { y, 0, om Ix(x), x annars beräkningsbar? Övning 3.46 Fermats stora sats lyder: Det finns inget tal n>2 sådant att det existerar naturliga tal x>0, y>0 och z>0, för vilka det gäller att: x n + y n = z n

133 126 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER Ingen vet idag om den är sann eller falsk. Betrakta den konstanta funktionen: ζ 3 (x) = { 1, 0, om Fermats stora sats är sann annars. Är ζ 3 beräkningsbar? Om den är beräkningsbar, kan du då räkna ut ζ 3 (0)? Beskriv hur, eller motivera varför det inte går. Övning 3.47 Är funktionen: ζ 4 (x) = { odefinierad, 0, om Ix(x), x annars. beräkningsbar? Övning 3.48 För att från sats 3.21 dra slutsatsen att det finns icke beräkningsbara funktioner, så måste vi veta att Θ verkligen är en funktion. Beskriv utförligt hur man visar detta. Övning 3.49 Funktionen s 1, som introducerades på sidan 119, är välkänd från en sats i rekursionsteori som brukar kallas s-m-n-satsen: För alla naturliga tal m och n, finns en beräkningsbar, total funktion s n m N m+1 N som är sådan att om q är kod för ett program som (via kodningsfunktionen för n + m-tuppler) beräknar funktionen f N n+m N så beräknar programmet med koden s n m(q, k 1,..., k m ) (via en kodningsfunktion för n-tuppler) funktionen g N n N där g(x 1,..., x n ) = f(x 1,..., x n, k 1,... k m ) Bevisa s-m-n-satsen. Övning 3.50 I rekursionsteori finns en berömd sats som kallas rekursionssatsen. En formulering av denna ser ut på följande sätt:

134 3.12. NÅGRA ICKE-BERÄKNINGSBARA FUNKTIONER 127 Om f N N N är en beräkningsbar funktion, så finns ett tal e sådant att f(x, e) = φ e (x) a. Om vi nu väljer f enligt: f(x, y) = y så är f beräkningsbar. Rekursionssatsen säger då att det finns ett konstant program med index e sådant att Ix(e), n e, dvs. ett program som ger sin egen kod som utdata! Kan detta vara möjligt? Det betyder ju att det skall gå att skriva ett Pascal-program som ger sig själv som utdata. Visa att det går att skriva ett sådant program. b. Vad betyder rekursionssatsen om vi väljer f(x, y) = eq N (x, y)

135 128 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER 3.13 Rekursion Vi har nu visat hur man i P skall implementera de flesta olika konstruktioner som finns i våra vanliga programmeringsspråk. Det är egentligen bara en konstruktion som återstår och det är rekursion. I ml kan vi definiera funktioner med hjälp av rekursion som i: FUN fac n = IF n=0 THEN 1 ELSE n*fac(n-1); och på liknande sätt kan vi göra i Pascal, och Lisp FUNCTION fac (n : integer) : integer; BEGIN IF n=0 THEN fac := 1 ELSE fac := n*fac(n-1) END (defun fac (n) (cond ((zerop n) 1) (t (* n (fac (1- n)))))) Den här funktionsdefinitionen är enkel att översätta till iterativ form och skriva med hjälp av while-satsen i P: PROGRAM(X0) X1 := 1; WHILE X0 0 DO X1 := X0*X1; X0 := PRED(X0) END RESULT(X1) Men det finns andra rekursivt definierade Pascal-funktioner som det är mycket svårare att implementera utan rekursion. Ta till exempel quicksort eller ett program som genomlöper en trädstruktur. Men eftersom alla beräkningsbara funktioner skall kunna beräknas av P-program måste det finnas en metod att översätta rekursion till P-satser. Att det finnas en sådan metod är uppenbart för alla som har sett maskininstruktionerna till en vanlig dator. Bland dessa finns ju ingen rekursionsinstruktion, och en kompilatorkonstruktör måste därför ha en metod att översätta rekursionen i ett högnivåspråk till icke-rekursiva maskininstruktioner. Låt oss se lite närmare på problemet.

136 3.13. REKURSION 129 För att visa att alla funktionsdefinitioner som innehåller rekursion går att programmera i P, så gör vi på följande sätt: Först definierar vi ett nytt språk som ser ut precis som P förutom att det också innehåller en konstruktion för rekursion. Det är inte svårt att sedan representera programmen i det nya språket som naturliga tal, dvs. att implementera en datatyp för programmen. Det mesta arbetet går att överföra från implementeringen av datatypen Prog. Efter detta implementerar vi en interpretator i P för det nya språket och har därmed visat hur man översätter rekursionen i det nya språket till iteration i P. Antag att alla program i fortsättningen använder X0 både som indata- och resultat-variabel, dvs. att alla program har utseendet: PROGRAM(X0) c RESULT(X0) Vi adderar nu en sats, REK, till P och ger den följande semantik: Om satsen finns i ett program Q, så skall följande hända då vi exekverar satsen i omgivningen σ: Resultatet av att exekvera satsen, om satsen har något resultat, fås genom att exekvera programmet Q med indata σ(x0), dvs. ett rekursivt anrop på programmet med indata från variabeln X0. Resultatet av det rekursiva anropet returneras också i X0. Vi skapar på detta sätt ett nytt språk som vi kallar kp. Ett rekursivt program för fakultetsfunktionen kan vi skriva på följande sätt: PROGRAM(X0) X1 := X0; IF X0 0 THEN X0 := PRED(X0); REK; X0 := X1*X0 ELSE X0 := 1 FI RESULT(X0) För att definiera semantiken för REK-satsen, med samma metod som vi använde för att definiera semantiken för P, så måste vi på något sätt spara hela programmets utseende. Vi kan välja att spara det som en del av tillståndet, eller att göra det explicit som en extra komponent som behövs för att definiera värdet av en sats. Låt oss välja det senare sättet och översiktligt skissera en semantisk definition av kp enligt samma modell som vi tidigare använde för P självt.

137 130 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER Semantiken för kp-program får följande utseende (jämför med hur det såg ut för P-program): kp-program c, Ω[x 1 n], PROGRAM(x 1 ) c RESULT(x 2 ) σ PROGRAM(x 1 ) c RESULT(x 2 ), n σ(x 2 ) där c Sats, x 1 Var, x 2 Var, σ State och n N. [kp ] Observera att värdet av en sats är ett tillstånd och för att kunna bestämma detta så behövs information om tillståndet före satsen exekveras och även information om det program som satsen förekommer i. k-tilldelning e, σ n x := e, σ, p σ[x n] där e Uttryck, n N, σ State, p Prog och x Var [kt ] k-while v, σ false WHILE v DO c END, σ, p σ [kw 1] v, σ true c, σ, p σ WHILE v DO c END, σ, p σ WHILE v DO c END, σ, p σ [kw 2] där v Villkor, σ State, σ State, σ State, c Sats och p Prog k-sekventiering c 1, σ, p σ c 2, σ, p σ c 1 ; c 2, σ, p σ [ks] där c 1 Sats, c 2 Sats, σ State, σ State, σ State och p Prog Slutligen har vi det som är nytt i kp jämfört med P, REK-satsen: REK-SATSEN p, σ(x0) n REK, σ, p σ[x0 n] där σ State, n N och p Prog [kr]

138 3.13. REKURSION 131 Beräkningsreglerna för villkor och uttryck behöver inte förändras, utan de beräkningsregler som vi använde för P gäller också för kp-villkor och kp-uttryck. Vi har nu definierat ett språk som innehåller rekursion och vi skall nu visa hur man kan skriva en interpretator för kp i P. Först måste vi naturligtvis representera kp-program i P, dvs. implementera en datatyp för kp-program, men det är bara en enkel utvidgning av datatypen Prog, så det lämnar vi som övning. Nästa steg är att implementera interpretatorn för kp. Problemet är naturligtvis REK-satsen. För att kunna interpretera denna, måste vi kunna spara det aktuella tillståndet och de kvarvarande satserna. Efter att gjort detta, kan vi sedan låta interpretorn ta hand om det rekursiva anropet och när detta är klart återställa tillståndet och kvarvarande satserna. Eftersom ett nytt rekursivt anrop kan förekomma under interpreteringen av en REK-sats, måste vi använda en stack för att spara det gamla tillståndet och de gamla satserna. En stack är samma sak som en lista, förutom att man brukar använda andra namn på de primitiva operationerna PUSH i stället för CONS, POP i stället för TL, TOP i stället för HD och EMPTY i stället för NULL. När vi ska interpretera en REK-sats, sparar vi först undan det aktuella tillståndet och de kvarvarande satserna i variablerna X50 (stack för tillstånd) och X60 (stack för satser), och börjar sedan interpretera programmet från början. När satserna tar slut i X6, kontrollerar vi om det är slutet av ett rekursivt anrop, och i så fall återställer vi det undansparade tillståndet och satserna och fortsätter interpreteringen. I annat fall avslutar vi interpreteringen som tidigare. Interpretatorn får följande utseende: EXPR EVAL-PROG-kP(Y1,Y2) MACRO: -- INTERPRETATOR FÖR kp

139 132 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER -- Y1:Uttryck, Y2:Uttryck -- Y1 repr kp-program... X5 := OMEGA; X5 := UPDATE(X4,X2,X5); X50 := NIL; X6 := DEST-PROG-STMT(X1); X60 := NIL; WHILE (X6 0) OR (X60 NIL) DO IF X6=0 THEN -- RETURN från REK-anrop X6 := HD(X60); X60:= TL(X60); X8 := VALUEOF(MK-VAR(0),X5); X5 := UPDATE(MK-VAR(0),X8,HD(X50)); X50 := TL(X50) FI; IF IS-SEQ(X6) THEN... ELSE... FI IF IS-ASSIGN(X7) THEN... ELSE IF IS-WHILE(X7) THEN... ELSE -- IS-REK(X7) X60 := X6. X60; X6 := X1; X50 := X5. X50; X8 := VALUEOF(MK-VAR(0),X5); X5 := UPDATE(MK-VAR(0),X8,OMEGA) FI FI END; X10 := DEST-PROG-VAR-2(X1); X10 := VALUEOF(X10,X5) RESULT(X10) -- OBS -- OBS -- OBS -- OBS -- OBS -- OBS -- OBS -- OBS -- OBS -- OBS -- OBS -- OBS -- OBS -- OBS -- OBS -- OBS -- OBS -- OBS -- resultatvariabel Övning 3.51 Definiera och implementera en datatyp för kp-program. Övning 3.52 Hur skall man implementera två ömsesidigt rekursiva funktioner i kp? Till exempel odd och even som i ml kan definieras:

140 3.13. REKURSION 133 let fun even n = if n=0 then true else not(odd(n-1)) and odd n = if n=0 then false else not(even(n-1)) in even 5 end

141 134 KAPITEL 3. BERÄKNINGSBARA FUNKTIONER

142 Kapitel 4 BERÄKNINGSBARA MÄNGDER I föregående kapitel studerade vi de beräkningsbara funktionerna; i detta kapitel skall vi studera beräkningsbara mängder. Vi börjar med ett begrepp som kallas rekursivt enumerabla mängder och fortsätter sedan med avgörbara mängder. 4.1 Rekursivt enumerabla mängder Intuitivt sett är en mängd rekursivt enumerabel (r.e.) om vi mekaniskt kan räkna upp dess element. Vi kräver inte att elementen skall kunna räknas upp i någon viss bestämd ordning eller att varje element bara får förekomma en enda gång i uppräkningen. Om ef är en beräkningsbar, total funktion, så är A = {ef(0), ef(1), ef(2),..., ef(n),...} en rekursivt enumerabel mängd. Om vi till en början begränsar oss till delmängder av N, så har vi följande definition: Definition 4.1 En mängd A N är rekursivt enumerabel om A = eller om det finns en P-beräkningsbar, surjektiv, total funktion ef N A. Vi kallar funktionen som räknar upp A:s element för dess enumereringsfunktion. Observera att vi måste behandla fallet A = för sig, eftersom det inte finns någon total funktion från N till den tomma mängden. Det enklaste sättet att visa att en mängd A N är rekursivt enumerabel är helt enkelt att definiera ett P-program EF A som beräknar den surjektiva, totala funktionen ef A N A; programmet EF A skall alltså ha följande egenskaper: 1. Det skall terminera för alla indata. 135

143 136 KAPITEL 4. BERÄKNINGSBARA MÄNGDER 2. Varje utdata skall tillhöra A. 3. För ett godtyckligt element a A skall det finnas ett naturligt tal n a sådant att EF A, n a a. Vi säger att EF A genererar mängden A. Nu till några exempel på rekursivt enumerabla mängder: 1. N identitetsfunktionen är beräkningsbar, surjektiv och total. 2. {n N n är ett primtal} den genereras ju av programmet 1 PROGRAM(X0) X1 := PRIMTAL(X0) RESULT(X1) 3. Varje språk som genereras av en grammatik, dvs. varje typ-0, typ-1, typ-2 och typ-3 språk, är en rekursivt enumerabel mängd. 4. Teoremen i en formell logisk teori utgör en rekursivt enumerabel mängd. 5. Varje induktivt definierad mängd är rekursivt enumerabel. Vid första anblicken kan det vara svårt att överhuvudtaget hitta mängder som inte är rekursivt enumerabla, men vi kommer snart att också ge exempel på sådana mängder. För att utvidga definitionen av rekursivt enumerabel till mängder som inte är delmängder av N, så måste vi använda den allmänna definitionen av beräkningsbar partiell funktion som vi introducerade i ett tidigare avsnitt (sid 88). Definition 4.2 En mängd A är rekursivt enumerabel om A = eller om det finns en beräkningsbar, total, avkodningsfunktion d A N A och en P-beräkningsbar, total funktion ef N N för vilka det gäller att kompositionen är surjektiv. d A ef N A Vi skall i fortsättningen, i första hand, betrakta delmängder till N, men genom att införa avkodningsfunktioner för andra mängder, till exempel sanningsvärden, tecken och listor, så kan satserna enkelt utvidgas till att gälla även dessa. Om vi jämför definitionen av en rekursiv enumerabel mängd med definitionen av en uppräkningsbar mängd (se avsnitt 3.1), så ser vi att den enda skillnaden är att man för en rekursivt enumerabel mängd kräver att funktionen som räknar upp elementen skall vara beräkningsbar. Som en direkt konsekvens av definitionerna får vi därför satsen: 1 För en definition av PRIMTAL se lösningen till övning 3.22, sid 85

144 4.1. REKURSIVT ENUMERABLA MÄNGDER 137 Sats 4.3 Varje rekursivt enumerabel mängd är uppräkningsbar. De två följande satserna visar att unionen och snittet av två rekursivt enumerabla mängder också är rekursivt enumerabel. Vi börjar med unionen. Sats 4.4 Om A N och B N är två rekursivt enumerabla mängder, så är också deras union A B rekursivt enumerabel. Bevis: Om A eller B är, så är A B trivialt rekursivt enumerabel. Antag därför att A och B båda är icke-tomma. Det finns då surjektiva, totala, beräkningsbara funktioner ef A N A ef B N B Antag att dessa beräknas av P-programmen EF A och EF B. Låt EFM A och EFM B vara de två uttrycks-förkortningar som motsvarar de båda P-programmen. Vi kan då definiera P-programmet: PROGRAM(X0) X1 := X0 MOD 2; X2 := X0 DIV 2; IF X1 0 THEN X3 := EFM B (X2) -- om X0 är udda ELSE X3 := EFM A (X2) -- om X0 är jämnt FI RESULT(X3) Detta program beräknar en total funktion: ef A B N A B för vilken det gäller att ef A B (2 n) = ef A (n), och att ef A B (2 n + 1) = ef B (n). Ett element c A B finns i A eller i B; om c A, så det finns ett tal n A N sådant att ef A (n A ) = c. Men då är ef A B (2 n A ) = c. På motsvarande sätt kan vi se att om c B så finns det ett tal n B N sådant att ef A B (2 n B + 1) = c. Funktionen ef A B är alltså surjektiv och därav följer det att A B är rekursivt enumerabel. Om vi formulerar detta i termer av de tre egenskaperna ett program skall ha för att beräkna en enumereringsfunktion, så får vi: 1. Programmet terminerar alltid eftersom både EFM A och EFM B terminerar. 2. Varje utdata tillhör A B eftersom det antingen fås från EFM A eller EFM B, dvs. antingen tillhör A eller tillhör B.

145 138 KAPITEL 4. BERÄKNINGSBARA MÄNGDER 3. Ett element i A B fås som resultat för något n, enligt beskrivningen ovan. De olika funktionerna räknar upp elementen på följande sätt: ef (0) ef (1) ef (2) ef (3) ef (4)... ef A a 0 a 1 a 2 a 3 a 4... ef B b 0 b 1 b 2 b 3 b 4... ef A B a 0 b 0 a 1 b 1 a 2... För att visa att snittet av två r.e. delmängder till N är rekursivt enumerabel, behöver vi använda en något krångligare algoritm. Svårigheten ligger i att om vi vet att en mängd A är r.e. så kan vi mekaniskt få reda på om ett givet element a tillhör mängden a tillhör ju A om ef A (n) = a för något n men vi kan aldrig mekaniskt få reda på om ett element inte tillhör A. Sats 4.5 Om A N och B N är r.e. mängder, så är också A B rekursivt enumerabel. Bevis: Om A B = så är A B rekursivt enumerabel. Antag att A B. Då finns det ett element a A B och eftersom A och B är rekursivt enumerabla, finns det två surjektiva funktioner ef A N A och ef B N B som beräknas av P-programmen EF A och EF B. Låt EFM A och EFM B vara två uttrycks-förkortningar som motsvarar dessa P-program. Vi kan då definiera ett nytt P-program enligt följande: PROGRAM(X0)

146 4.1. REKURSIVT ENUMERABLA MÄNGDER 139 X1 := X0 MOD 2; X2 := X0 DIV 2; X3 := a; -- det element som vi vet finns i A B IF X1 0 THEN X4 := EFM B (X2); FOR X5 := 0 TO X2 DO X6 := EFM A (X5); IF X4=X6 THEN X3 := X6 FI END ELSE X4 := EFM A (X2); FOR X5 := 0 TO X2 DO X6 := EFM B (X5); IF X4=X6 THEN X3 := X6 FI END FI RESULT(X3) Detta program beräknar en funktion: ef A B N A B för vilken det gäller, att ef A B (2 n) är lika med ef A (n) om detta element finns med bland ef B (0),..., ef B (n) och lika med a annars. Det gäller också att ef A B (2 n+1) är lika med ef B (n) om detta element finns med bland ef A (0),..., ef A (n) och lika med a annars. Om ett element c tillhör A B, så tillhör det både A och B, och det måste därför finnas två tal n A och n B sådana att ef A (n A ) = ef B (n B ) = c. Då följer det, att om n A n B så blir ef A B (2 n A ) = c, och om n B >n A så blir ef A B (2 n B + 1) = c. Det vill säga, alla element i A B kommer med i den uppräkning som ges av ef A B och A B är därför rekursivt enumerabel. Vi kan se att programmet genererar A B eftersom: 1. Det alltid terminerar; FOR-satser terminerar alltid. 2. Varje utdata från programmet tillhör A B. 3. Om c A B så är c utdata för något indata enligt beskrivingen ovan.

147 140 KAPITEL 4. BERÄKNINGSBARA MÄNGDER Förutom dessa två satser, som visar att unionen respektive snittet av två rekursivt enumerabla mängder också är rekursivt enumerabel, så kan vi se att de flesta mängder som vi i ett tidigare avsnitt visade vara uppräkningsbara också är rekursivt enumerabla. Funktionerna som räknar upp dessa mängder är ju beräkningsbara. Vi ska nu fortsätta med en sats som visar att det finns uppräkningsbara mängder som inte är rekursivt enumerabla. Den mängd som vi ska ta som exempel på detta är mängden av index för alla totala, beräkningsbara funktioner. T = {i N φ i är total} Att T är uppräkningsbar följer av att ett P-programindex är ett naturligt tal, så T N; vidare är de naturliga talen uppräkningsbara och en delmängd av en uppräkningsbar mängd är alltid uppräkningsbar (se avsnitt 3.1). Sats 4.6 Mängden T är inte rekursivt enumerabel. Bevis: Vi börjar med en översikt av beviset. Från antagandet att T är rekursivt enumerabel, så vet vi att det finns en (total, surjektiv, beräkningsbar) enumereringsfunktion ef T N T (T N), som räknar upp alla index till beräkningsbara funktioner. Så T = {ef T (0), ef T (1),...} är en mängd av index och den är naturligt relaterad till mängden {φ eft (0), φ eft (1),...} som innehåller totala funktioner. För att få en motsägelse så konstruerar vi en total, beräkningsbar funktion g N N som skiljer sig från alla φ i där i T. Konstruera g så att den skiljer sig från φ eft (0) för argumentet 0, φ eft (1) för argumentet 1, Ett lämpligt val av g som uppfyller dessa krav är. g(x) = φ eft (x)(x) + 1 g är total och beräkningsbar och dess index borde därför finnas med i T. Men g φ ft (0) eftersom g(0) = φ ft (0)(0) + 1 φ ft (0)(0) g φ ft (1) eftersom g(1) = φ ft (1)(1) + 1 φ ft (1)(1).

148 4.1. REKURSIVT ENUMERABLA MÄNGDER 141 (jämför med beviset av att N N inte är uppräkningsbar sats 3.4, sid 31 ) Nu till en mer noggrann genomgång. Vi börjar alltså med att antaga att T är rekursivt enumerabel. Det finns då ett program som räknar upp alla index av totala, beräkningsbara funktioner kalla detta program EF T och motsvarande uttrycksförkortning EFM T. Låt oss kalla funktionen som EF T beräknar för ef T. Vi skall nu definiera ett program som beräknar en total funktion, men en funktion som skiljer sig från alla funktioner vars index finns i uppräkningen som ges av ef T. Vi har då fått en motsägelse och T kan därför inte vara rekursivt enumerabel. Definiera ett program R enligt: PROGRAM(X0) ( R) X1 := EFM T (X0); -- X1 = ef T (X0) X2 := EVAL-PROG(X1,X0); -- X2 = φ X1 (X0) X2 := SUCC(X2) RESULT(X2) Detta program beräknar en funktion g, för vilken det gäller att: g(n) = φ eft (n)(n) + 1 (4.1) Eftersom resultatet av EFM T (X0) är ett index till en total funktion, kommer EVAL-PROG(X1,X0) alltid att terminera och följaktligen måste g vara en total funktion; att den är beräkningsbar är väl helt klart, den beräknas ju av programmet R. Då följer att index för programmet R, som vi kallar r, måste finnas med någonstans i den uppräkning som ges av EF T, säg att det finns med som det n:te elementet, dvs. EF T, n r, eller, med andra ord, Eftersom r är index för ett program som beräknar g är Vidare får vi, från 4.2 att ef T (n) = r (4.2) g = φ r dvs. g(n) = φ r (n) φ r = φ eft (n) dvs. φ r (n) = φ eft (n)(n) Men från definitionen av g (se 4.1) vet vi också att g(n) = φ eft (n)(n) + 1 Om vi kombinerar ihop dessa likheter så får vi att: g(n) = φ r (n) = φ eft (n)(n) φ eft (n)(n) + 1 = g(n) Detta ger vår motsägelse mängden T kan alltså inte vara rekursivt enumerabel. Beviset är ännu ett exempel på Cantors diagonalmetod.

149 142 KAPITEL 4. BERÄKNINGSBARA MÄNGDER Vi har tidigare visat hur man kodar (indexerar) de beräkningsbara, partiella funktionerna för att kunna skriva program som behandlar dem. Vi skall nu visa att det är möjligt att på ungefär samma sätt koda de rekursivt enumerabla mängderna. En r.e. mängd A är enligt definitionen värdemängd till en total, surjektiv, beräkningsbar funktion φ i och man skulle därför kunna tänka sig att välja att koda en rekursivt enumerabel mängd som index till den funktion som räknar upp mängden. Nackdelen med att välja denna kodningen är att en del tal, i det här fallet de tal som inte står för totala funktioner, inte motsvarar någon r.e. mängd. Dessutom är det ju inte möjligt att mekaniskt avgöra om ett index står för en total funktion. En bättre metod bygger på att varje r.e. mängd är domän till en beräkningsbar, partiell funktion. Sats 4.7 En mängd A N är rekursivt enumerabel om och endast om det finns en beräkningsbar, partiell funktion ef = φ i N N sådan att A = dom(ef ). Bevis: ( ) Antag att A är rekursivt enumerabel. Om A = så kan vi ta ef = Λ, den totalt odefinierade funktionen, eftersom dom(λ) =. Om A så finns det en uttrycksförkortning EFM A som genererar A. Då definierar vi programmet: PROGRAM(X0) X1 := 1; -- X1 -- stoppvillkor X2 := 0; WHILE X1 0 DO X3 := EFM A (X2); IF X3=X0 THEN X1 := 0 FI; X2 := SUCC(X2) END RESULT(X2) Detta program beräknar en partiell funktion ef A för vilken det gäller att dom(ef A ) = A. Tänk igenom precis varför detta gäller! ( ) Låt g A vara en beräkningsbar, partiell funktion. Visa att då är dom(g A ) en rekursivt enumerabel mängd. Eftersom g A är beräkningsbar, finns det ett index q sådant att g = φ q. Om g A = Λ så är dom(g A ) = och därför rekursivt enumerabel. Om g A Λ så är dom(g A ) och det finns ett element a dom(g A ). Med hjälp av detta element och förkortningen EVAL-PROG-STEP definierar vi följande program:

150 4.1. REKURSIVT ENUMERABLA MÄNGDER 143 PROGRAM(X0) ( R) X1 := FST(X0); -- indata repr. ett par X2 := SND(X0); X3 := EVAL-PROG-STEP(q,X1,X2); X4 := FST(X3); IF X4 0 THEN X5 := X1 ELSE X5 := a FI RESULT(X5) Det här programmet, som vi kallar R, beräknar en surjektiv, total funktion P[R] N A. Varje element n dom(g A ) kommer att bli resultat av R för något indata i. Ett element n tillhör dom(g A ) om och endast om Ix(q), n. Antag att beräkningen tar k steg att utföra. Då blir resultatet av EVAL-PROG-STEP(q, n, k) lika med paret 1, m för något m, och programmet R med indata i = e N N ( n, k ), dvs. koden för paret n, k, kommer att ge resultatet n. Vi kan också se att programmet alltid terminerar och att varje resultat tillhör dom(g A ) = A. Programmet har alltså de tre egenskaperna som gör att det beräknar en enumereringsfunktion. Den motsvarande funktionen P[R] är alltså en surjektiv, beräkningsbar, total funktion, och A en rekursivt enumerabel mängd. Genom att vi från föregående sats vet att varje r.e. mängd är domän till någon beräkningsbar, partiell funktion och att domänen till en beräkningsbar, partiell funktion alltid är rekursivt enumerabel, kan vi använda kodningen av de beräkningsbara, partiella funktionerna också för de rekursivt enumerabla mängderna. Ett naturligt tal i blir alltså kod för den rekursivt enumerabla mängden W i = dom(φ i ). Då vi nu har en kod för de r.e. mängderna, så kan vi skriva program som handlar om r.e. mängder. Vi skall här inte gå närmare in på hur man kan göra detta, utan bara formulera en sats som kan ge en indikation på vad man kan göra. Sats 4.8 Låt i och j vara index för två r.e. mängder. Då finns det beräkningsbara, totala funktioner ef 1 N N N och ef 2 N N N som är sådana att ef 1 (i, j) är index till W i W j och ef 2 (i, j) är index till W i W j. Vi lämnar beviset som övning. Vi skall avsluta detta avsnitt om rekursivt enumerabla mängder med en sats som ger en alternativ karakterisering av de r.e. mängderna.

151 144 KAPITEL 4. BERÄKNINGSBARA MÄNGDER Sats 4.9 En mängd A N är rekursivt enumerabel om och endast om det finns en P-beräkningsbar funktion f A N N med definition: f A (x) = { 1, odefinierad, om x A annars. Bevis: Enkel följd av sats 4.7. ( ) Antag att A är r.e. Då finns enligt sats 4.7 en beräkningsbar, funktion φ q, sådan att A = dom(φ q ). Men då gäller att programmet PROGRAM (X)) X1 := EVAL-PROG(q,X0); X2 := 1 RESULT(X2) har samma domän, men alltid ger resultatet 1. Programmet beräknar därför funktionen f A. ( ) Antag att det finns en P-beräkningsbar funktion f A satsen. Men då säger sats 4.7 att dom(f A ) = A. N N enligt Övning 4.1 Visa att N N är rekursivt enumerabel (det är nödvändigt att använda en avkodningsfunktion). Övning 4.2 Kan man med samma metod som i beviset av sats 4.6 visa att mängden av index för partiella, beräkningsbara funktioner inte är rekursivt enumerabel. Motivera varför, eller varför inte, det går. Övning 4.3 Är resultatmängden till den partiella funktionen med index i, dvs. ran(φ i ) = {y N φ i (x) = y för något x}, rekursivt enumerabel? Övning 4.4 Bevisa sats 4.8. Övning 4.5 Vilka av följande mängder är rekursivt enumerabla a. {x N Ix(x), x } b. {x N Ix(x), x } c. {x N φ x (x) = 1} d. {x N φ x id N } e. {x N φ x = id N }

152 4.2. AVGÖRBARA MÄNGDER Avgörbara mängder Om vi vill konstruera en delmängd A till en mängd B, så kan vi göra det genom att definiera den karakteristiska funktionen, c A B {0, 1}, för A där: { 1, om x A c A (x) = 0, annars. Notera att den karakteristiska funktionen är total. Vi definierar nu begreppet avgörbar mängd (eng. decidable set) för att beskriva de delmängder av B som är beräkningsbara. Definition 4.10 En mängd A är avgörbar (relativt B) om den karakteristiska funktionen c A B {0, 1} är beräkningsbar. Termen rekursiv mängd används ofta som synonym till avgörbar mängd, detta i analogi med att rekursiv funktion ofta används som synonym till beräkningsbar funktion. Ibland används namnet semiavgörbar mängd (eng. semi-decidable) istället för rekursivt enumerabel mängd. Man kan för en r.e. mängd avgöra om ett element finns i mängden, men inte om elementet inte finns där. Att en mängd A är avgörbar är ett starkt villkor; om man vill veta om ett element a tillhör, eller inte tillhör, en avgörbar mängd, så kan man mekaniskt få ett svar. Det är bara att beräkna den karakteristiska funktionen med a som indata. Om resultatet blir 0 så är a inte element i mängden, om resultatet blir 1 så tillhör a mängden. Vi skall i fortsättningen i huvudsak syssla med mängder som är avgörbara relativt N, dvs. delmängder till N. Det finns många enkla exempel på sådana mängder, till exempel: 1. N själv är naturligtvis avgörbar. Den karakteristiska funktionen till N är ju den konstanta funktionen som alltid är Mängden av primtal är avgörbar. Det går mekaniskt att avgöra om ett tal är primtal eller ej. Det finns också exempel på intressanta mängder som är avgörbara relativt andra mängder, till exempel: 1. Alla programmeringsspråk är syntaktiskt sett avgörbara mängder relativt List(Char). Alla typ-1, typ-2 och typ-3 språk är avgörbara. En syntaxanalysator är i detta fall en karakteristisk funktion. 2. I enkla formella teorier, till exempel satslogik, är mängden av teorem avgörbar relativt mängden av formler i teorin. Sanningstabellsmetoden går att exekvera mekaniskt och den ger i princip en karakteristisk funktion till mängden av teorem i satslogik.

153 146 KAPITEL 4. BERÄKNINGSBARA MÄNGDER Vi får andra avgörbara mängder från följande sats: Sats 4.11 Om A och B är avgörbara, så är också C(A) [komplementet till A], A B och A B avgörbara. Vi visar följande sats för att relatera de rekursivt enumerabla och de avgörbara mängderna. Sats 4.12 Varje avgörbar mängd är också rekursivt enumerabel. Bevis: Låt A vara en avgörbar mängd. Om A = så är A naturligtvis rekursivt enumerabel. Annars är A och det finns ett element a A. Låt CM A vara en uttrycksförkortning som beräknar den karakteristiska funktionen till A. Följande program genererar då A: PROGRAM(X0) ( EF A ) X1 := CM A (X0); IF X1 0 THEN X2 := X0 ELSE X2 := a FI RESULT(X2) Det här programmet terminerar alltid CM A är en karakteristisk funktion och terminerar därför alltid. Varje utdata tillhör A och varje element i A är utdata till programmet för något indata. Så A är en rekursivt enumerabel mängd eftersom a A medför att EF A, a a. Alla avgörbara mängder är tydligen rekursivt enumerabla men nästa sats visar att det finns rekursivt enumerabla mängder som inte är avgörbara. Vi definierar mängden K på följande sätt: K = {x N Ix(x), x } Detta är alltså den mängd av naturliga tal, som när talen betraktas som koder för program terminerar när de exekveras med sig själva som indata. Sats 4.13 Mängden K är rekursivt enumerabel men inte avgörbar. Bevis: Vi visade redan i sats 3.21 att funktionen Θ N N med definition Θ(x) = { 1, om Ix(x), x 0, annars

154 4.2. AVGÖRBARA MÄNGDER 147 inte är beräkningsbar. Men Θ är den karakteristiska funktionen för K. K kan därför inte vara avgörbar. För att visa att K är rekursivt enumerabel ska vi definiera ett program som alltid terminerar och som genererar alla index q för vilka det gäller att Ix(q), q. Följande program, som vi kallar R, uppfyller denna specifikation: 2 PROGRAM(X0) ( R) X1 := FST(X0); X2 := SND(X0); X3 := EVAL-PROG-STEP(X1,X1,X2); X4 := FST(X3); IF X4 0 THEN X5 := X1 ELSE X5 := MK-PROG(MK-VAR(0), MK-ASSIGN(MK-VAR(1),MK-0), MK-VAR(0)); FI RESULT(X5) Indata (X0) till R tolkas som ett par bestående av X1 och X2. Om programmet med index X1 konvergerar för indata X1 på X2 steg, så blir resultatet av R, X0 lika med X1; om det inte konvergerar på X2 steg, så blir resultatet lika med ett index för identitetsfunktionen (idx). Denna är total, så varje program som beräknar den konvergerar för alla indata; därför gäller det att idx K om idx är ett index för identitetsfunktionen. Eftersom en program-indata kombination Q, n konvergerar om och endast om det sker på ett ändligt antal steg, så finns det alltid ett indata till R som representerar det par där första komponenten q är index för Q och där andra komponenten n är lika med det antal steg som behövs för att Q, q skall konvergera. Vi kan se att R har följande egenskaper: 1. Det terminerar alltid. 2. Utdata tillhör alltid K idx K och X1 K om Ix(X1), X1 på X2 steg. 3. Om c K så gäller att Ix(c), c. Antag att det sker på n steg. Om i är koden för paret c, n, dvs. k = e N N ( c, n ), så blir c utdata från R för indata i, dvs. R, k c (eller P[R](k) = c). Programmet R genererar mängden K, som därför är rekursivt enumerabel. Mängden K är alltså rekursivt enumerabel. 2 Uttrycket MK-PROG(MK-VAR(0),MK-ASSIGN(MK-VAR(1),MK-0),MK-VAR(0)) ger index för programmet PROGRAM(X0) X1:=0 RESULT(X0) som beräknar identitetsfunktionen.

155 148 KAPITEL 4. BERÄKNINGSBARA MÄNGDER Vi skall nu formulera två satser som relaterar de r.e. mängderna till de avgörbara och tvärtom. Den första satsen är: Sats 4.14 Om en mängd A N och dess komplement [relativt N] C(A) båda är rekursivt enumerabla, så är både A och C(A) avgörbara. Bevis: Om A är tom eller lika med N, så är både A och dess komplement trivialt avgörbara, de är ju lika med N och. Så antag att A är icke-tom och skild från N. Antag vidare att uttrycksförkortningarna QM A och QM C(A) genererar A respektive C(A). Följande program beräknar då den karakteristiska funktionen till A : PROGRAM(X0) X1 := 0; X2 := 0; WHILE X1=0 DO X3 := QM A (X2); IF X3=X0 THEN X1 := 1 FI; X4 := QM C(A) (X2); IF X4=X0 THEN X1 := 1 FI; X2 := SUCC(X2) END; IF X3=X0 THEN X5 := 1 ELSE X5 := 0 FI RESULT(X5) -- STOPP -- villkor -- räknare Om vi bara växlar THEN-delen och ELSE-delen i den sista IF-satsen så får vi ett program som beräknar den karakteristiska funktionen till C(A). Programmen terminerar alltid eftersom varje tal (indata) antingen tillhör A eller C(A) och därför, förr eller senare, blir resultat av antingen QM A (X1) eller QM C(A) (X1). Den här satsen har en intressant tillämpning för formella logiska teorier, till exempel en formell teori för aritmetik. Antag att vi kan dela upp alla formler (påståenden) i en teori i två disjunkta mängder, de sanna (TRUE) och de falska (FALSE). Vi har tidigare nämnt att mängden av teorem i en formel teori är rekursivt enumerabel, så om vi inkluderar alla påståenden A som är sådana att A är ett teorem i mängden FALSE och alla A som är teorem i TRUE, så blir naturligtvis FALSE och TRUE också rekursivt enumerabla. Om nu TRUE = C(FALSE), dvs. alla påståenden är antingen falska eller sanna (teorin är fullständig), så är mängderna FALSE och TRUE avgörbara enligt ovanstående sats! Givet en formel, kan vi mekaniskt räkna ut om den är sann eller falsk! (För logik-kunniga, jämför

156 4.2. AVGÖRBARA MÄNGDER 149 Gödels ofullständighetssats det finns en formel i aritmetik (A) sådan att varken den eller dess negation ( A) går att bevisa.) Den andra satsen beskriver en egenskap som är tillräcklig för att en r.e. mängd skall vara avgörbar. Sats 4.15 Om ef A N A är en enumereringsfunktion för A som är strängt växande, dvs. om x>y så ef A (x)>ef A (y), så är A avgörbar. Bevis: Eftersom ef A är P-beräkningsbar, finns en uttrycksförkortning EFM A som beräknar ef A. Följande program beräknar då den karakteristiska funktionen för A : PROGRAM(X0) X1 := 0; REPEAT X2 := EFM A (X1); X1 := SUCC(X1) UNTIL X2 X0; X3 := (X2=X0) RESULT(X3) Eftersom X2 för varje varv i REPEAT-satsen får allt större värden (eftersom ef A är strängt växande), kommer värdet av X2 förr eller senare att bli större än eller lika med värdet av X0. Om X2 = X0 så finns X0 i uppräkningen som ges av ef A ; om X2 X0 så finns X0 inte med i uppräkningen. Programmet beräknar alltså den karakteristiska funktionen till A, och A är därför avgörbar. Vi skall som slutpunkt för vår behandling av avgörbara mängder formulera en sats om mängder av funktionsindex. Vi säger att en mängd I av index respekterar funktioner om i I och φ i = φ j medför att också j I. Med andra ord, om det i en mängd I, som respekterar funktioner, finns ett index till ett program Q, så finns i I också index till alla andra program som beräknar samma funktion som Q. Satsen som vi ska formulera kallas för Rices sats och ser ut på följande sätt: Sats 4.16 Om I är en indexmängd som respekterar funktioner, så är I avgörbar endast om I = eller om I = N. Bevis: Vi börjar med att antaga att I är avgörbar och att förkortningen CM I beräknar den karakteristiska funktionen för I. Låt oss också antaga att index för den helt odefinierade funktionen Λ finns i C(I). (Om så inte är fallet växla bara I och C(I) nedan.) Eftersom I finns index till åtminstone en funktion i I. Säg att q är ett sådant index. Definiera nu funktionen h N N så att h(n) är index till programmet:

157 150 KAPITEL 4. BERÄKNINGSBARA MÄNGDER PROGRAM(X0) X1 := EVAL-PROG(n,n); X0 := EVAL-PROG(q,X0); RESULT(X0) h är en beräkningsbar funktion, så låt oss antaga att uttrycksförkortningen HM beräknar den. Observera att h(n) är index till ett program som beräknar funktionen Λ om Ix(n), n och funktionen φ q annars. Så programmet R PROGRAM(X0) ( R) X1 := HM(X0); X2 := CM I (X1) RESULT(X2) beräknar först index till ett program som 1. beräknar Λ om Ix(X0), X0. 2. beräknar φ q om Ix(X0), X0. Men (alla) index för Λ funktionen tillhör C(I) och (alla) index för φ q funktionen tillhör I. Och eftersom CM I implementerar den karakteristiska funktionen till I, så kan vi avgöra om det för ett godtyckligt indata q gäller att Ix(q), q eller Ix(q), q. Programmet R beräknar alltså Θ funktionen, som vi vet är ickeberäkningsbar. Antagandet att I är avgörbar måste därför vara felaktigt; I är inte avgörbar. Vi får en viktig datalogisk tillämpning av Rices sats om vi betraktar problemet att verifiera att ett program är korrekt relativt en specifikation. Ett vanligt sätt att skriva en specifikation är i form av ett påstående som varje korrekt program skall uppfylla. Om vi låter R(Q) vara ett sådant påstående som inte refererar till några inre egenskaper hos programmet Q utan bara till dess indata/utdata relation, till exempel Sortspec(Q) ( in List(N)) ( ut List(N)) ( Q, in ut & Permutation(in, ut) & Ordnad(ut)) så kan vi bilda mängden av alla korrekta program Kprog {Q Prog Sortspec(Q)} Men det här är en indexmängd. Om två program beräknar samma funktion, så gäller antingen att bådas index tillhör mängden Kprog eller att inget av indexen tillhör mängden. Mängden Kprog är alltså enligt Rices sats inte avgörbar och vi kan inte mekaniskt avgöra om ett program uppfyller specifikationen Sortspec(Q)!

158 4.2. AVGÖRBARA MÄNGDER 151 Övning 4.6 Låt A B N vara mängder och A vara avgörbar relativt B. Visa att om A inte är avgörbar relativt N, så är inte heller B avgörbar relativt N. Övning 4.7 Är mängderna a. {q N φ q är total}, b. {q N φ q = Λ} dvs. mängden av index till program som aldrig terminerar, c. {q N φ q = id N } d. {q N φ q (0) = 1} avgörbara? Använd Rices sats, men försök också ge intuitiva argument. Övning 4.8 Bevisa sats 4.11.

159 152 KAPITEL 4. BERÄKNINGSBARA MÄNGDER 4.3 Predikatlogik är oavgörbar För er som kan lite om första ordningens predikatlogik skall vi nu visa en sats som bland annat inspirerade Alan Turing till att försöka definiera vad man menade med en mekanisk process. Bakgrunden till satsen är tre påståenden som den tyske matematikern David Hilbert formulerade under en kongress 1928: 1. Matematik är fullständig. Det går att bevisa eller motbevisa varje matematiskt påstående. 2. Matematik är konsistent. Det finns inte något matematiskt påstående som både kan bevisas och motbevisas. 3. Matematik är avgörbar. Det finns en mekanisk process som kan avgöra om varje påstående är sant eller falskt. Inget av dessa påståenden hade bevisats 1928, men Hilbert trodde att alla tre var sanna. Gödel visade ganska snart att ett formellt språk för aritmetik inte kunde vara fullständigt och att man inte kunde bevisa dess konsistens med hjälp av elementära metoder. Det tredje påståendet, avgörbarhetsproblemet (ty. das Entscheidungsproblem), kunde Church och Turing senare visa vara falskt och det är detta problem som vi nu ska intressera oss för. Vi börjar med en precis formulering av satsen: Sats 4.17 Mängden av teorem i första ordningens predikatlogik med likhet är inte avgörbar relativt mängden av formler. Stmten säger att den karakteristiska funktionen till mängden av teorem inte är beräkningsbar, eller med andra ord, att det inte finns någon beräkningsbar funktion (inget program) som ger resultatet 1 om indata är en formel som går att bevisa och 0 om den inte går att bevisa. Beviset för satsen är inte speciellt svårt, i synnerhet inte när vi redan har introducerat nästan allt maskineri som behövs. Idén är mycket enkel: Man definierar en uppsättning formler som handlar om ett P-program p och ett naturligt tal n. Formlerna definieras på ett sådant sätt att en formel är bevisbar om och endast om beräkningen av programmet p terminerar för indata n. Eftersom det inte går att mekaniskt avgöra detta, så går det inte heller att mekaniskt avgöra om formeln är bevisbar. Det återstår bara att konstruera dessa formler och uttrycka dem i predikatlogik. Vi börjar med att definiera ett predikatlogiskt språk med likhet. För enkelhets skull definierar vi ett typat språk, dvs. alla individkonstanter, funktioner och predikat ges en typ, och alla variabler tänkes sträcka sig över elementen i en given typ. Vårt språk måste först och främst innehålla typer och konstanter för sanningsvärden och naturliga tal:

160 4.3. PREDIKATLOGIK ÄR OAVGÖRBAR 153 Bool false Bool true Bool N 0 N s N N pred N N iszero N Bool Sedan behöver vi en typ för att beskriva tillstånd: State Ω State valof Var State N update Var N State State och slutligen typer för att beskriva språket P. Vi definierar en typ för varje syntaktisk kategori (jämför med den abstrakta syntaxen för P och med representationen av P-program i P): Var VAR N Var Uttryck 0 Uttryck SUCC Var Uttryck PRED Var Uttryck Villkor VILLKOR Var Villkor Stmt TILLD Var Uttryck Stmt WHILE Villkor Stmt Stmt SEKV Stmt Stmt Stmt Prog PROG Var Stmt Var Stmt Observera att termen s(0) står för det naturliga talet 1, medan SUCC(VAR(s(0)) betecknar P-uttrycket SUCC(X1). Vi introducerar en likhet för de olika typerna samt ett predikat för beräkningsrelationen för att kunna uttrycka de formler vi vill formulera. För att göra det hela lite klarare splittrar vi upp beräkningsrelationen och definierar ett beräkningspredikat för varje syntaktisk kategori i P. x = A y är ett påstående om A är en typ samt x A och y A p, m P n är ett påstående om p Prog och m, n N c, σ S σ är ett påstående om c Stmt och σ, σ State e, σ E n är ett påstående om e Uttryck,σ State och n N v, σ T b är ett påstående om v Villkor, σ State och b Bool x, σ X n är ett påstående om x Var, σ State och n N

161 154 KAPITEL 4. BERÄKNINGSBARA MÄNGDER Vi har nu definierat ett predikatlogiskt språk med likhet och det återstår bara att visa att det går att formalisera alla egenskaper som definierar P:s beräkningsrelation i detta språk. Först definierar vi ett antal predikatlogiska formler som formaliserar de fundamentala egenskaperna för de olika konstanterna. För de naturliga talen introducerar vi följande definitioner: N 1 pred(0) = N 0 N 2 ( n N) (pred(s(n)) = N n) N 3 iszero(0) = Bool true N 4 ( n N) (iszero(s(n)) = N false) N N 1 & N 2 & N 3 & N 4 Vi behöver inte några speciella egenskaper för sanningsvärdena true och false, men för datatypen State gör vi följande definitioner: S 1 ( x Var) (valof (x, Ω) = N 0) S 2 ( x Var) ( n N) ( σ State) (valof (x, update(x, n, σ)) = N n) S 3 ( x Var) ( x Var) ( n N) ( σ State) (x Var x valof (x, update(x, n, σ)) = N valof (x, σ)) S 4 ( x Var) ( σ State) ( σ State) ((valof (x, σ) = N valof (x, σ )) (σ = State σ )) S S 1 & S 2 & S 3 & S 4 Detta är formaliseringen av de olika typerna. Det återstår nu att definiera beräkningsreglerna i P som formler i predikatlogik. Beräkningsregeln för program får följande utseende om den skrivs som en predikatlogisk formel: P ( c Stmt) ( x Var) ( x Var) ( n N) ( σ State) ( c, update(x, n, Ω) S σ PROG(x, c, x ), n P valof (x, σ)) Observera hur strecket i ber kningsregeln har blivit ett implikatonstecken och hur alla fria variabler har blivit allkvantifierade. Beräkningsreglerna för satser uttryckes i predikatlogik på följande sätt: C 1 ( e Uttryck) ( x Var) ( n N) ( σ State) ( e, σ E n TILLD(x, e), σ S update(x, n, σ))

162 4.3. PREDIKATLOGIK ÄR OAVGÖRBAR 155 C 2 ( v Villkor) ( c Sats) ( σ State) ( v, σ V false WHILE(v, c), σ S σ) C 3 ( v Villkor) ( c Sats) ( σ State) ( σ State) ( σ State) (( v, σ V true & c, σ S σ & WHILE(v, c), σ S σ ) WHILE(v, c), σ S σ ) C 4 ( c 1 Sats) ( c 2 Sats) ( σ State) ( σ State) ( σ State) (( c 1, σ S σ & c 2, σ S σ ) SEKV(c 1, c 2 ), σ S σ ) C C 1 & C 2 & C 3 & C 4 Beräkningsreglerna för uttryck blir: E 1 ( σ State) ( 0, σ U 0) E 2 ( x Var) ( σ State) ( SUCC(x), σ U s(valof (x, σ))) E 3 ( x Var) ( σ State) ( PRED(x), σ U pred(valof (x, σ))) E E 1 & E 2 & E 3 Beräkningsreglerna för villkor och variabler blir: V ( x Var) ( σ State) ( VILLKOR(x), σ V iszero(valof (x, σ))) X ( x Var) ( σ State) ( x, σ X valof (x, σ)) Efter detta definierande kan vi nu till slut uttrycka den uppsättning formler som det inte går att mekaniskt avgöra om de är bevisbara eller ej. F(p, n) N & S & P & C & E & V & X & ( m N) ( p, n P m) Om predikatlogik vore avgörbar skulle vi, givet ett program p och ett naturligt tal n, bland annat kunna avgöra om F(p, n) vore bevisbar eller ej. Men formeln är bevisbar precis då programmet p med indata n terminerar, och detta är, som vi vet från sats 3.21, inte möjligt att mekaniskt avgöra. Eftersom inte den mängd bevisbara formler som beskrivs av F(p, n) är avgörbar, så kan inte heller mängden av alla bevisbara formler vara avgörbar. Vi har använt en typad variant av predikatlogik med likhet för att formalisera beräkningsreglerna i språket P och påståendet F(p, n). Detta är en smaksak och vi skulle lika gärna kunna använt otypad predikatlogik.

163 156 KAPITEL 4. BERÄKNINGSBARA MÄNGDER

164 Kapitel 5 λ-kalkyl 5.1 Inledning λ-kalkyl är en formell teori som är tänkt att handla om funktioner, inte funktioner i den mängdteoretiska mening som vi har använt hittills, utan funktioner betraktade som tillordningsregler eller algoritmer. Två personer som förknippas med λ-kalkyl är Alonzo Church, som är upphovsmannen, och Haskell Curry, som arbetat med en variant av λ-kalkyl som kallas kombinatorisk logik (combinatory logic). Det fanns från början två målsättningar med λ-kalkyl och kombinatorisk logik: 1. att utveckla en teori för funktioner, 2. att utöka denna med en logisk del, för att erhålla en grundläggande teori för logik och (delar av) matematik. Vi har redan tidigare kommit i kontakt med λ-kalkyl i samband med att vi redogjorde för olika sätt att försöka definiera de beräkningsbara funktionerna ett sätt att göra detta, faktiskt det första som föreslogs, är att säga att en funktion är beräkningsbar om och endast om den är definierbar i λ-kalkyl. Det är precis detta som är Churchs tes, även om man numera oftast formulerar den i termer av rekursiva funktioner. Vad har då λ-kalkyl för relevans för datalogi. Ett svar är att vi kan se λ-kalkyl som en modell för funktionella programmeringsspråk, ungefär på samma sätt som vi kan se språket P som en modell för imperativa programmeringsspråk. Den stora skillnaden mellan dessa två modeller är synen på minnet; på hur variabler får värden eller, som man också säger, hur värden binds till variabler. I imperativa språk, och speciellt då P, finns ett statiskt minne där antalet minnesceller är konstant, men där en minnescells innehåll kan ändras. I λ-kalkyl har vi en helt annan typ av minne ett dynamiskt minne. Man kan här skapa nya minnesplatser allt 157

165 158 KAPITEL 5. λ-kalkyl eftersom evalueringen fortskrider, men innehållet i en minnescell förändras aldrig. Den imperativa minnesmodellen är naturligtvis baserad på faktiskt existerande maskiner medan λ-kalkylmodellen är anpassad till ett matematiskt synsätt på beräkningar. λ-kalkyl används dessutom ofta för att beskriva olika programmeringsspråks semantik. I mitten av 60-talet försökte Peter Landin beskriva semantiken för algol- 60 genom att översätta algol-program till en slags λ-uttryck och sedan definiera en maskin för att evaluera dessa (den imperativa secd-maskinen). 1 I det här fallet använde Landin alltså λ-uttryck som en universell maskinkod! En annan metod att beskriva ett programmeringsspråks semantik är denotationssemantik (matematisk semantik); i denna metod, och i många andra sammanhang, använder man λ-notation för att beteckna matematiska (mängdteoretiska) funktioner. λ-kalkyl anses också vara grunden till programmeringsspråket Lisp, men det verkar vara en överdrift, åtminstone om man får tro J. McCarthy, upphovsmannen till Lisp, som skriver följande i boken: History of Programming Languages (ed. R Wexelblat, Academic-Press, 1981): To use functions as arguments, one needs a notation for functions, and it seemed natural to use the λ-notation of Church. I didn t understand the rest of the book so I wasn t tempted to try to implement his more general mechanism for defining functions λ-kalkyl betraktades länge med en viss skepsis av matematiker och logiker, man visste ju inte riktigt vilka funktioner som teorin handlade om. Det finns bara ett slags objekt i teorin och eftersom det är tillåtet att applicera ett objekt på ett godtyckligt objekt, så kan man alltid applicera en funktion på sig själv. Detta ger matematiska problem; teorin kan uppenbarligen inte handla om alla de partiella (mängdteoretiska) funktionerna. Om teorin skulle handla om dessa, så skulle det ju gälla att mängden av funktioner från D till D, dvs. D D, skulle innehålla lika många element som D självt. De enda mängder som uppfyller detta villkor är mängder med ett element, och dessa är det inte speciellt intressant att göra en teori för. Dana Scott visade i slutet av 60-talet vilka funktioner man skall betrakta för att få en lämplig (mängdteoretisk) modell till λ-kalkyl. Han visade att om man bara betraktar kontinuerliga funktioner mellan mängder med en speciell struktur, så kan man bilda en modell till λ-kalkyl. 1 P. Landin: A Correspondence Between Algol 60 and Church s Lambda-Notation, CACM, Vol 8 (1965), sid och

166 5.2. λ-notation λ-notation Abstraktion Låt oss börja med att introducera λ-notation som ett beteckningssätt för funktioner av en variabel. Konstigt nog finns det inom matematik inget allmänt accepterat sätt för att beteckna en funktion, utan man måste introducera ett namn om man vill ha en beteckning på en funktion. Om man till exempel vill ha en beteckning på funktionen som dubblar sitt argument, så måste man först införa definitionen f(x) = 2x och sedan kan man använda f som beteckning på själva funktionen. Det här är ett ganska otympligt sätt eftersom man hela tiden måste introducera nya namn på alla funktioner som man vill beskriva. I andra sammanhang, till exempel när man arbetar med funktioner i samband med derivering och integrering, så använder man en speciell notation. I ekvationerna d dx (x2 + 1) = 2x 2 0 z dz = 2 finns ett flertal funktioner gömda! Den första likheten säger att derivatan av funktionen f, där f(x) = x är lika med funktionen g, där g(x) = 2x Den andra ekvationen säger att integralen från 0 till 2 av identitetsfunktionen är lika med 2. Derivering är alltså en operation som avbildar en funktion på en funktion och integrering avbildar två tal och en funktion på ett tal. Med de beteckningssätt som normalt används är detta inte speciellt lätt att se. För att slippa introducera ett namn när man vill beteckna en funktion låter man ibland funktionen betecknas av det uttryck som definierar den. Tyvärr blir detta inte entydigt eftersom man inte vet vilken variabel som är oberoende. Ta följande definitioner av f 1, f 2 och f 3 i ML: - val x = 3 and y = 2 ; val x = 3 : int val y = 2 : int

167 160 KAPITEL 5. λ-kalkyl - val f1 (x) = x + y = and f2 (y) = x + y = and f3 (z) = x + y ; val f1 = fn : int -> int val f2 = fn : int -> int val f3 = fn : a -> int Funktionerna definieras av samma uttryck, men är alla olika. De är olika eftersom de har olika värde för argumentet 1. - (f1(1),f2(1),f3(1)); val it = (3,4,5) : int * int * int För att få en entydig beteckning måste man förutom uttrycket också veta vilken variabel som ska betraktas som oberoende. Church införde beteckningen 2 (λx.e) där λ-tecknet betyder att det som följer är beteckning för en funktion, x är den oberoende variabeln och E är uttrycket som definierar funktionen. Man säger att man fått funktionen (λx.e) genom att abstrahera ut x från E. Uttryck på den här formen kallas därför ofta för abstraktioner. Om vi låter D stå för derivering och I för integrering, så skulle ekvationerna ovan kunna skrivas D(λx.x 2 + 1) = λx.2x I(0, 2, λz.z) = 2 vilket är ett beteckningsätt som bättre uttrycker vad det hela handlar om (tycker åtminstone jag). När Church valde λ-tecknet för att inleda en funktionsbeteckning lyckades han välja ett tecken som inte finns på vanliga terminaler. 3 Man kan därför se olika varianter på hans beteckningssätt i olika programmeringsspråk. Funktionen (λx.x+ 1950) skrivs till exempel fn x x egentligen använde Church beteckningen (λxe) 3 Den enda förklaring till varifrån λ-tecknet kommer, som jag har sett, är från J.B. Rosser, Highlights of the History of the Lambda-Calculus: Church was struck with certain similarities between his new concept and that used in Whitehead and Russell (1925) for the class of all x s such that f(x); to wit, xf(x). Because the new concept differed quite appreciably from class membership, Church moved the caret from over the x down to the line just to the left of the x; specifically, Later, for reasons of typography, an appendage was added to the caret toˆxf(x). produce a lambda; the result was λxf(x).

168 5.2. λ-notation 161 i ml, i Lisp, och (lambda(x) (plus x 1950)) (int n)int : n i algol-68. I de flesta andra programmeringsspråk kan man faktiskt inte definiera en funktion utan att ge den ett namn, och det är väl kanske inte ens helt rätt att säga att (lambda(x) (plus x 1950)) är en funktion i Lisp. För att kunna beteckna funktioner med två eller flera argument, till exempel funktionen plus som adderar sina två argument, så skulle vi kunna införa en ny beteckning. Men detta är onödigt eftersom vi istället kan definiera dessa funktioner med hjälp av stegvisa (eng. curried) λ-uttryck på följande sätt: (λx 1.(... (λx n.e)...)) Så istället för att se plus som en funktion med två argument så ser vi den som en funktion med funktionaliteten: plus N (N N) dvs. en funktion tar sina argument stegvis. För att slippa många av parenteserna i λ-uttrycken, så brukar man använda följande parenteskonventioner: (λx.e) skrivs λx.e (λx 1.(... (λx n.e)...)) skrivs λx 1... x n.e Dessutom så räcker ett λ-tecken så långt som möjligt: Till exempel så skriver vi: λx.x y z betyder (λx.x y z) λx y z.x i stället för (λx.(λy.(λz.x))) (λx y z.z x y) w i stället för ((λx.(λy.(λz.((z x) y))))) w) Applikation När vi nu har ett beteckningssätt för funktioner, så skall vi naturligtvis också införa en beteckning för en funktions värde för ett givet argument. Vi använder beteckningen (f a) för att beteckna funktionen f:s värde för argumentet a. Vi säger att vi applicerar f på a. Uttrycket (f a) kallas därför för en applikation. Vi inför även här parenteskonventioner:

169 162 KAPITEL 5. λ-kalkyl (f a) skrivs f a (... ((f a 1 ) a 2 )... a n ) skrivs f a 1 a 2... a n dvs. vi utelämnar ofta de yttersta parenteserna samt tänker oss att applikation associerar till vänster (är vänsterassociativ). Om f är en abstraktion λx.e och om det inte finns några variabler i a, så är resultatet av applikationen (f a) helt enkelt E med a substituerat för x, vilket vi skriver E[x:=a]. Alla förekomster av den abstraherade variabeln i E ersätts med argumentet. Det blir problem om argumentet innehåller variabler som binds vid substitutionen, till exempel så får vi problem med y i applikationen (λx.λy.x) y. Vi kommer senare att behandla detta fall mycket utförligt. Betrakta den stegvisa funktionen plus = λx y.x + y N (N N) Om vi applicerar den på 2, så får vi som resultat en ny funktion plus 2 = λy.2 + y N N dvs. funktionen som adderar två till sitt argument. Vi kan nu applicera den på ett nytt tal, till exempel 3, (plus 2) 3 = = 5 Istället för applicera en funktion på flera argument samtidigt, applicerar vi argumenten stegvis.

170 5.3. λ-kalkyl λ-kalkyl Hittills har vi bara infört λ-notation som ett beteckningssätt, dvs. vad som har betecknats har redan varit intuitivt förstått. Vi hade definierat begreppet funktion innan vi införde beteckningssättet för funktioner. Vi skall nu gå vidare och introducera λ-kalkyl som ett formellt system, dvs. vi skall introducera ett antal syntaktiska regler som vi använder för att bevisa påståenden om λ-uttryck. Att reglerna är syntaktiska innebär att de endast beror på de ingående uttryckens form (syntax), inte på deras betydelse. Naturligtvis väljer man inte regler hur som helst, men vad som är viktigt är att en korrekt tillämpning av reglerna inte bygger på någon kunskap om vad de ingående delarna betyder. Det går därför att mekaniskt, till exempel med ett Pascal-program, kontrollera om reglerna har tillämpats korrekt. Vad är det då för saker som man vill visa? Två saker är av speciellt intresse: För det första vill vi kunna beräkna λ-uttryck och därför finns det i λ-kalkyl formler: M N (M reduceras till N) med betydelsen att λ-uttrycket M kan beräknas till N i 0 eller flera steg. Observera att det inte är säkert att N är färdigberäknat. För det andra finns det formler: M = N som intuitivt säger att λ-uttrycken M och N betecknar samma funktion. Vi kommer snart att ge regler för att bevisa formler i λ-kalkyl, men först måste vi definiera en del allmänna begrepp. Vi börjar med att exakt definiera de objekt (termer) som kalkylen innehåller. Vi kommer att låta x, y, z,... beteckna variabler och olika bokstäver betecknar olika variabler om vi inte uttryckligen säger annat. K, L, M och N kommer att stå för godtyckliga λ-uttryck och står för syntaktisk likhet. Två λ-termer är syntaktiskt lika om de är lika som uttryck, dvs. om de är lika symbol för symbol. Så λx.xy λx.xy men λw.w λy.x och λx.x λy.y. Definition 5.1 I λ-kalkyl (λ-termerna) definieras termerna induktivt genom följande klausuler: 1. Varje variabel är en λ-term. 2. Om M och N är λ-termer, så är (M N) en λ-term (applikation). Vi kallar M för applikationens operator och N för dess operand. 3. Om M är en λ-term och x en variabel, så är (λx.m) en λ-term (abstraktion). M är abstraktionens kropp (eng. body).

171 164 KAPITEL 5. λ-kalkyl λx apply z x apply y Figur 5.1: Trädrepresentation av (λx.x y) z Vi introducerar slutligen beteckningen Λ för mängden av λ-termer. Ibland lägger man till en klausul som säger att en konstant också är en λ-term, men det behövs inte. I praktiska tillämpningar av λ-kalkyl, t.ex. i form av funktionella språk, så inför man naturligtvis konstanter för tal, sanningsvärden, addition, multiplikation, etc. Utan konstanter kommer alla termer att stå för funktioner, det finns inga termer som direkt motsvarar till exempel naturliga tal eller sanningsvärden. För att få med dessa i teorin kodar man dem som funktioner, ungefär som vi kodade listor, sanningsvärden och ordnade par i form av naturliga tal tidigare. Mer om detta senare. Några olika exempel på λ-termer är: (λx.(x y)) (x (λx.(x y))) (x x) ((λx.x) (λx.x)) Vi fortsätter att använda de parenteskonventioner som vi tidigare införde. Så λx y.x y (λx.(λy.(x y))) x (λy z.y) z ((x (λy.(λz.y))) z) Lägg märke till att x y z betyder ((x y) z) inte (x (y z))!! Ibland kan det vara svårt att se hur ett λ-uttryck är uppbyggt. Det kan då hjälpa att rita det som ett träd, se figur 5.1 och 5.2. En liknande representation använder man dessutom ofta då man representerar λ-uttryck i datorprogram, till exempel när man vill skriva ett program som beräknar ett λ-uttrycks värde. För att kunna gå vidare och formulera regler för likheten mellan λ-termer så måste vi först definiera några andra begrepp som vi behöver. Först definierar vi vad som menas med att en variabel är fri respektive bunden i ett uttryck. Vi gör

172 5.3. λ-kalkyl 165 x apply apply λx λy x λz y Figur 5.2: Trädrepresentation av x (λy z.y) (λx.x) detta genom induktion över uppbyggnaden av ett λ-uttryck (se definitionen ovan). Tanken är naturligtvis att en variabel är bunden i ett uttryck om den abstraherats ut i detta uttryck. Definition 5.2 En variabel är fri i ett uttryck i följande fall: 1. Variabeln x är fri i uttrycket x. 2. x är fri i (M N) om den är fri i någon av M eller N. 3. x är fri i (λy.m) om x och y är olika variabler och x är fri i M. Nu till definitionen av vad som menas med en bunden variabel. Definition 5.3 En variabel är bunden i ett uttryck i följande fall: 1. x är bunden i (M N) om x är bunden i M eller bunden i N. 2. x är bunden i (λy.m) om x och y är samma variabel eller om x är bunden i M. Lägg märke till att en variabel kan förekomma både bunden och fri i ett uttryck, ta till exempel x i uttrycket: (λx.x) x Ibland talar man om fria respektive bundna förekomster (eng. occurrences) av en variabel istället för fria respektive bundna variabler. Man kan lägga märke till att variabeln y är bunden i λy.x men att där inte finns några bundna förekomster av y.

173 166 KAPITEL 5. λ-kalkyl Vi kan nu formellt definiera vad vi menar med att substituera M för de fria förekomsterna av x i N, vilket vi betecknar N [x:=m]. Observera att substitution inte är en operation i λ-kalkyl som till exempel applikation, utan en syntaktisk operation på λ-termer som vi endast definierar därför att vi med dess hjälp enklare kan beskriva reglerna i λ-kalkyl. Skillnaden kan kanske bäst förstås via skillnaden mellan metaspråk och objektspråk. Objektspråket är det språk som vi beskriver det språk som vi talar om. Metaspråket, däremot, är det språk som vi använder för att beskriva objektspråket. I vårt fall är λ-kalkyl objektspråk och svenska metaspråk. Skillnaden mellan applikation och substitution är nu att applikation är en operation i objektspråket medan substitution är en operation i metaspråket. Vi är nu redo för definitionen av substitution. Kom ihåg att står för syntaktisk likhet. Definition 5.4 Substitution av λ-uttryck definieras enligt följande. Om M och N är λ-termer och x en variabel, så definieras N [x:=m] (substitutionen av M för x i N) 4 : 1. Om N är en variabel y, så är (a) y [x:=m] M om x och y är samma variabel, dvs. x y. (b) y [x:=m] y om x och y är olika variabler. 2. Om N är en applikation (K L), så är (K L) [x:=m] (K [x:=m] L [x:=m]) 3. Om N är en abstraktion (λy.k), så är (a) (λy.k) [x:=m] (λy.k) om x och y är samma variabel. (b) (λy.k) [x:=m] (λy.k [x:=m]) om x och y är olika variabler och om y inte förekommer fri i M eller om x inte förekommer fri i K. (c) (λy.k) [x:=m] (λz.k [y:=z] [x:=m]) annars. z är en ny variabel; en variabel som inte förekommer fri i K eller M. 4 Beteckningen som används för substitution varierar kraftigt mellan olika författare. Här är några olika varianter för att beteckna uttrycket som erhålls genom att substituera N för x i M: N[x]M G. Peano: The Principles of Arithmetic (1889). [N/x]M H. Curry and R. Feys: Combinatory Logic (1958). MN x D. Prawitz: Natural Deduction (1965). M[N/x] P. Martin-Löf: Constructive Mathematics and Computer Programming (1979). M[x:=N] H. Barendregt: The Lambda Calculus (1981). M[x : N] E. Hehner: The Logic of Programming (1984).

174 5.3. λ-kalkyl 167 De två första klausulerna är det inga problem med, men den tredje kräver kanske en kommentar. Det besvärliga fallet är när vi skall substituera M för x i en abstraktion λy.k. Om x och y är samma variabel, så kan det ju inte finnas några fria förekomster av x i λx.k och resultatet blir därför helt enkelt λx.k, dvs. abstraktionen oförändrad. Om x och y är olika variabler så måste vi skilja på två fall: 1. Om y inte förekommer fri i M, dvs. ingen fri variabel i M kan bindas av λy, eller om x inte förekommer fri i K, dvs. ingen substitution kommer att ske. 2. Annars, dvs. om y förekommer fri i M och x förekommer fri i K. Det är det andra fallet som är besvärligt, eftersom en naiv substitution skulle medföra att en fri variabel i M skulle bindas efter substitutionen. Till exempel: skulle, om man bara naivt ersatte x med y, bli (λy.x) [x:=y] (5.1) λy.y vilket inte kan vara korrekt. Vilket variabelnamn som vi väljer i en λ-bindning skall inte spela någon roll; om vi hade valt en annan variabel, så skulle vi kunna skriva (5.1) som (λz.x) [x:=y] men då skulle ju resultatet av substitutionen helt plötsligt bli λz.y vilket är en helt annan funktion än λy.y (identitetsfunktionen). Om en fri variabel i argumentet riskerar att bli bunden, så måste vi helt enkelt byta namn på den motsvarande λ-bundna variabeln. Namnet på den spelar ju ändå ingen roll. Precis samma problem förekommer överallt där vi använder bundna variabler, t.ex. inom programmering. Betrakta Pascal programmet: PROGRAM P (input, output); VAR x : integer; PROCEDURE f (y : integer); BEGIN writeln(x) END; BEGIN x := 25; f (1) END.

175 168 KAPITEL 5. λ-kalkyl Om vi exekverar det här programmet så får vi utskriften 25. Men om vi naivt byter ut alla förekomster av variabeln x mot y i programmet, så får vi helt plötsligt utskriften 1! Vi får precis samma problem som i λ-kalkyl om vi vill definiera en operation för att ändra variabelnamn i Pascal-program utan att programmens mening förändras. Vi inför också ett begrepp för att beskriva λ-termer som bara skiljer sig i valet av bundna variabler. Definition 5.5 Vi kan byta en bunden variabel x i λ-termen M genom att ersätta den del av M som har formen λx.n med (λy.n[x:=y]). Variabeln y får inte förekomma fri i N. Vi säger att M är kongruent med N om vi kan erhålla N från M genom att succesivt byta bundna variabler. Vi kan nu använda följande strategi för att utföra substitutioner: Om vi har en substitution M [y:=n], så byter vi först ut alla bundna variabler i M som kolliderar med de fria variablerna i N. Sedan kan vi naivt byta ut alla fria förekomster av y i M mot N. Övning 5.1 Vilka av följande λ-uttryck är syntaktiskt lika? a. λx.x y z b. (λx.x y) z c. (λx.x) y z d. (((λx.x) y) z) e. (λx.x)(y z) Övning 5.2 Vilka variabler är bundna respektive fria i följande λ-uttryck? a. λx.x y z b. (λx.x y z)(λy.x y z) c. (λx.y)(λx.z) Övning 5.3 Vilka variabler är fria respektive bundna i följande matematiska uttryck: a. ni=1 ij=1 i 2 j + y b. ( x)( y)(x = y & y = z) c. n n x2 + y dy + y

176 5.3. λ-kalkyl 169 Övning 5.4 Formulera informella regler som säger hur man skall gå till väga för att byta variabelnamn i ett Pascal-program utan att programmets semantik förändras. Övning 5.5 Vilket λ-uttryck står följande för? a. x [y:=z] b. (λx.x) [x:=z] c. (λy.x) [x:=z] d. ((λx.x) (λy.x)) [x:=z] e. ((λx.x y) (λy.x y)) [y:=x] [x:=y] Övning 5.6 Den stora svårigheten med att definiera substitution är att de fria variablerna inte får bindas vid substitutionen. Ett sätt att komma bort från denna svårighet är att ha två olika syntaktiska kategorier av variabler, bundna respektive fria variabler; ungefär som man normalt har två olika syntaktiska kategorier för konstanter och variabler. Låt t.ex. x, y, z,... vara fria variabler och 1, 2, 3... vara bundna variabler (!!). Skriv om det här avsnittet för denna variant av λ-kalkyl.

177 170 KAPITEL 5. λ-kalkyl Beräkningsregler Innan vi ger regler för likhet mellan λ-termer, så skall vi se lite på hur man kan evaluera λ-uttryck. Om vi tänker oss att λ-uttrycken står för funktioner och applikationerna för funktionsanrop, så är det naturligt att tänka sig att ett uttryck helt enkelt evalueras genom att applikationerna beräknas. Vi har alltså en beräkningsregel: (λx.m) N β M [x:=n] Denna regel säger att vi beräknar en applikation genom att substituera in argumentet istället för den bundna variabeln i funktionskroppen. Regeln skall tolkas så, att om det någonstans i en λ-term finns ett deluttryck av formen ((λx.m) N) så kan detta deluttryck ersättas med M [x:=n]. Förutom den ovan nämnda regeln, som kallas β-regeln, så finns det ytterligare en beräkningsregel (reduktionsregel), nämligen: (λx.(m x)) η där x inte får förekomma fri i M. η-regeln är lite kontroversiell och i många sammanhang formulerar man λ-kalkyl utan att ta med den. Om det gäller att M kan evalueras till N, så måste M = N för att reglerna skall bli naturliga. η-regeln ger därför upphov till likhetsregeln M (λx.(m x)) = M Denna regel medför att vi får en extensionell likhet mellan λ-termerna. När man arbetar med funktioner (algoritmer, tillordningsregler), så säger man att man har en extensionell likhet om man från kan dra slutsatsen att K L = M L (för alla L) K = M Intuitivt betyder detta att om två λ-termer ger lika resultat när de appliceras på ett godtyckligt argument, så är de två λ-termerna lika. Om man tänker sig att λ-termerna står för matematiska funktioner, så är detta naturligtvis korrekt. Men det finns tillfällen då man vill skilja på termer som reducerar argumenten i olika ordning. I det senare fallet talar man om en intensionell likhet, och om man vill formalisera en sådan likhet, så kan man naturligtvis inte ta med η-regeln. Den del i ett λ-uttryck som kan reduceras med hjälp av en beräkningsregel kallas för ett redex och om vi uttryckligen vill säga vilken regel som är applicerbar så kan vi kalla det reducerbara deluttrycket för ett β-redex eller η-redex. Vi säger att λ-termen M reduceras till N om vi kan erhålla N från M genom att 0 eller flera gånger reducera redex. Vi skriver M N, eller M λ N, för att beteckna att M kan reduceras till N. Vi har följande exempel:

178 5.3. λ-kalkyl (λx.x) (λx.x) (λx.x) (λx.x) 2. efter 0 reduktioner. (λx.x) (λx.x) (λx.x) eftersom (λx.x) (λx.x) β (λx.x) 3. (λx.(λy.y x) z) u z u eftersom (λx.(λy.y x) z) u β (λy.y u) z och (λy.y u) z β z u 4. λw.((λx.(λy.y) x) z) λw.z eftersom (λx.(λy.y) x) η λy.y och (λy.y) z β z Ett mer formellt sätt att definiera relationen M N är att definera den med hjälp av följande klausuler: 1. M M 2. Om L M och M N så L N (alternativt L M M N L N ). 3. Om M N så (L M) (L N) (alternativt M N (L M) (L N) ). 4. Om L M så (L N) (M N) (alternativt L M (L N) (M N) ). 5. Om M N så (λx.m) (λx.n) (alternativt M N λx.m λx.n ). 6. (λx.m) N M [x:=n] 7. (λx.(m x)) M Observera att dessa regler endast formaliserar det som vi ovan informellt beskrev genom att säga att vi kunde reducera delar av ett λ-uttryck och att vi kunde reducera redex 0 eller flera gånger. De 7 reglerna utgör en formalisering av reduktionsrelationen. Om ett λ-uttryck M reduceras till N kan vi visa det med dessa

179 172 KAPITEL 5. λ-kalkyl regler. Med hjälp av bevisnotationen från naturlig deduktion kan vi skriva beviset av exempel 4 ovan på följande sätt: λx.(λy.y)x (λy.y) (7) (λx.(λy.y)x)z (λy.y)z (3) (λy.y)z z (6) (λx.(λy.y)x)z z λw.(λx.(λy.y)x)z λw.z Vi kan nu definiera vad som i λ-kalkyl är motsvarigheten till ett värde, dvs. ett fullt evaluerat uttryck. Definition 5.6 Ett λ-uttryck är på normalform om det inte innehåller några redex. Om vi vill skilja på reduktionsreglerna så kan vi tala om β-normalform respektive η-normalform. Vi säger dessutom att ett λ-uttryck har en normalform om det kan reduceras till ett λ-uttryck på normalform. Exempel på λ-uttryck på normalform är: λx.x λx y z.z y z x(λx.x) (λx.x x) Vi kan nu formulera en mycket viktig sats. Sats 5.7 Det finns λ-uttryck som inte har någon normalform. (2) (5) Bevis: Tag till exempel λ-uttrycket (λx.x x) (λx.x x) Hela uttrycket är ett β-redex och det finns inga andra redex i uttrycket. Om vi använder β-regeln och substituerar in (λx.x x) istället för x i x x så får vi (λx.x x) (λx.x x). Men det är ju precis samma uttryck som det som vi började med. Vi kan alltså fortsätta att reducera uttrycket hur länge vi vill utan att nå ett uttryck utan redex! De λ-uttryck som inte har någon normalform motsvarar de beräkningar i språket P som inte terminerar och det är därför inte möjligt att mekaniskt avgöra om ett godtyckligt λ-uttryck har en normalform. En stor skillnad mellan P och λ-kalkyl är att i λ-kalkyl är program och data-objekt precis samma slags objekt. I P skilde vi ju på programdelar och data-objekt (naturliga tal) och även om vi med hjälp av interpretatorn kunde förvandla naturliga tal till program, så kunde vi aldrig direkt ha ett P-program som indata till sig själv. Vi fick först koda programmet

180 5.3. λ-kalkyl 173 som ett naturligt tal. I λ-kalkyl kan verkligen en λ-term vara indata till sig själv, eftersom uttryck som (λx.x x) (λx.x x) är korrekta λ-termer. Om vi jämför beräkningsreglerna i P med motsvarande regler i λ-kalkyl, så kan vi se en mycket viktig skillnad. I en P-beräkning fanns det aldrig utrymme för val bland de olika reglerna i varje situation var bara en enda regel tillämpbar. I λ-kalkyl är det annorlunda; ett λ-uttryck kan innehålla flera redex och vi kan välja vilket av dem som vi ska reducera först. Betrakta följande λ-uttryck: (λx.λy.y) }{{} A D E {}}{{}}{ ((λx.x x) (λx.x x)) }{{} B z }{{} C Det här uttrycket har strukturen ((A (D E)) C) och innehåller två olika redex. För det första applikationen (A B) och för det andra applikationen (D E). Båda dessa kan reduceras. Låt oss se vad som händer om vi genomför de två beräkningarna (reduktionerna): (λx.λy.y) ((λx.x x) (λx.x x)) z (λy.y) z (λx.λy.y) ((λx.x x) (λx.x x)) z (λx.λy.y) ((λx.x x) (λx.x x)) z. z (λx.λy.y) ((λx.x x) (λx.x x)) z Om vi väljer att reducera det första redexet, så kommer vi snart fram till en normalform; om vi väljer det andra, så terminerar inte reduktionen, dvs. vi kommer aldrig fram till någon normalform. Det är alltså viktigt i vilken ordning vi väljer att reducera redex; fel ordning kan som vi sett leda till att vi aldrig kommer fram till någon normalform. En ändå värre situation vore om vi skulle kunna komma fram till olika normalformer genom att välja olika strategier för att reducera redex. Att detta inte är möjligt framgår av följande sats som vi ger utan bevis: Sats 5.8 Om M N, M K och N och K båda är på normalform, så är N och K kongruenta (lika så när som på namnen av de bundna variablerna)

181 174 KAPITEL 5. λ-kalkyl Den här satsen följer av en känd sats i λ-kalkyl som kallas Church-Rosser satsen. Låt oss nu se lite närmare på de två olika beräkningarna ovan och framförallt se hur vi valde vilket redex vi skulle reducera. I de två beräkningarna valdes redex enligt följande strategier: I båda fallen väljer vi redex utifrån, och om det yttersta redexet är ett η-redex så reducerar vi det direkt, men om det är ett β-redex (λx.m) N så skiljer sig strategierna. 1. Den första strategin att välja redex kallas den normala ordningen och går till på följande sätt: Reducera β-redexet direkt enligt β-reduktionsregeln, dvs. substituera in N (precis som det står) istället för x i M. 2. Den andra strategin kallas den applikativa ordningen: Reducera först argumentet N till normalform och substituera sedan in resultatet istället för x i M. Evaluering av λ-uttryck enligt normal ordningen motsvarar i programmeringsspråk parameteröverföring med namnanrop (eng. call-by-name), eller lat evaluering (eng. lazy evaluation) som man ibland säger. Den applikativa ordningen motsvarar värdeanrop (eng. call-by-value), eller ivrig evaluering (eng. eager evaluation). Det heter lat evaluering eftersom man i en funktionsapplikation inte evaluerar argumentet innan det ska användas. De två strategierna har som vi sett olika egenskaper. Man kan visa att om man väljer redex enligt normal ordningen, så kommer man alltid till en normalform om det finns en sådan. Det är därför en säker beräkningsstrategi. Den applikativa ordningen kan, som vi sett, däremot leda till icke-terminerande beräkningar. Om vi väljer redex enligt den applikativa ordningen kan vi således få en icke-terminerande beräkning trots att λ-uttrycket har en normalform (har ett värde). Man kan då fråga sig varför den applikativa ordningen överhuvudtaget är intressant, då den inte säkert leder fram till ett resultat. Anledningen är helt enkelt effektivitet; den leder för det mesta(!) fram till normalform mycket fortare är normal ordningen. Ta till exempel λ-uttrycket (λx.x x x) ((λx.x x)(λx.x)) Med normal ordning får vi följande reduktion (det redex som reduceras i varje steg

182 5.3. λ-kalkyl 175 är understruket): (λx.x x x) ((λx.x x)(λx.x)) (((λx.x x)(λx.x))((λx.x x)(λx.x)))((λx.x x)(λx.x)) (((λx.x)(λx.x))((λx.x x)(λx.x)))((λx.x x)(λx.x)) ((λx.x)((λx.x x)(λx.x)))((λx.x x)(λx.x)) ((λx.x x)(λx.x))((λx.x x)(λx.x)) ((λx.x)(λx.x))((λx.x x)(λx.x)) (λx.x)((λx.x x)(λx.x)) (λx.x x)(λx.x) ((λx.x)(λx.x)) (λx.x) Med applikativ ordning får vi den kortare reduktionen: (λx.x x x) ((λx.x x)(λx.x)) (λx.x x x) ((λx.x)(λx.x)) (λx.x x x) (λx.x) ((λx.x)(λx.x))(λx.x) (λx.x)(λx.x) (λx.x) Med normalordnings-strategin evalueras aldrig ett argument innan det substitueras in i λ-uttryckets kropp. Anledningen till att metoden ofta blir mindre effektiv är att om den bundna variabeln förekommer flera gånger i λ-uttryckets kropp, så måste argumentet evalueras lika många gånger som det används (3 gånger i exemplet ovan). Men detta är också nyckeln till varför normalordnings-strategin är säker. Lika många gånger som det används kan ju betyda 0 gånger om argumentet aldrig används! I det första exemplet hade vi en applikation där argumentet inte hade någon normalform men där det å andra sidan aldrig användes i λ-uttryckets kropp. Kontrollera detta! Låt oss avsluta detta avsnitt med en liten avstickare till verkligheten och praktiska implementeringar av funktionella språk. Interpretatorer för funktionella språk implementerar helt enkelt beräkningsrelationen M N, som vi beskrivit ovan, samt beräkningsregler för naturliga tal, listor, par etc. När vi talar om funktionella språk, så skiljer vi på lata språk som sasl, krc och lml, och ivriga språk som ml och Hope. Skillnaden är att de lata språken använder normalordningen för att välja nästa redex att reducera, medan de ivriga språken använder den applikativa ordningen. I praktiska implementeringar gör man dessutom oftast följande inskränkningar: 1. Man implementerar inte η-regeln.

183 176 KAPITEL 5. λ-kalkyl 2. Man evaluerar aldrig innanför ett λ-tecken. Dessa två inskränkningar innebär att man börjar evalueringen utifrån och reducerar λ-uttrycket tills man kommer fram till något på formen λx.m eller tills man når en konstant. β-regeln implementeras inte heller med hjälp av direkt substitution som vi gjort ovan. Det skulle bli alltför ineffektivt eftersom man i princip måste gå igenom hela funktionskroppen för att substituera in argumentet. Om kroppen är stor kan detta bli mycket kostsamt. Istället för direkt substitution så använder man ofta något som brukar kallas för omgivningar (eng. environments). En omgivning kan ses som en lista av variabel-värde par och varje λ-uttryck förses helt enkelt med en omgivning som associerar värden med de fria variablerna i uttrycket. Med hjälp av omgivningar kan man förenkla evalueringen av applikationer högst betydligt. Antag att vi vill evaluera λ-uttrycket (λx.m) N Istället för att substituera in N, eller N:s värde, i M, så reducerar vi det direkt till följande par bestående av ett λ-uttryck och en omgivning: M, [ x, N ] [ x, N ] är den omgivning där variabeln x är bunden till uttrycket N. I ett lat språk så är N = N medan i ett ivrigt språk så är N lika med värdet (normalformen) av N. Nackdelen med detta sätt att evaluera funktionella program är att omgivningarna kan växa obegränsat och att de därför kan kräva mycket stort minnesutrymme. Detta kan i sin tur innebära att det går åt tid för skräpsamling och annan minnesadministration vilket gör att programmet kan komma att exekveras långsamt. Tyvärr finns det här inte utrymme att gå närmare in på olika evalueringsmekanismer för funktionella program och vi avslutar vår utflykt med ett exempel som förhoppningsvis illustrerar hur ett λ-uttryck kan evalueras med hjälp av omgivningar. Exempel: Vi ser hur man evaluerar λ-uttrycket (λw.((λy.w + (y 2)) (λz.z w))) 6 med hjälp av omgivningar. Antag att vi har definierat de vanliga beräkningsreglerna för addition och multiplikation av naturliga tal. Vi börjar då evalueringen med att skapa en tom omgivning: (λw.((λy.w + (y 2)) (λz.z w))) 6, []

184 5.3. λ-kalkyl 177 Sedan evaluerar vi den yttersta applikationen genom att binda w till 6 i omgivningen. Vi får då: ((λy.w + (y 2)) (λz.z w)), [ w, 6 ] Vi fortsätter sedan med nästa applikation, vilken naivt sett binder y till λz.z w. Det är här viktigt att vi inte bara binder y till λ-uttrycket, utan även tar med den aktuella omgivningen som ger värden till de fria variablerna i λ-uttrycket. Vi binder därför y till (λz.z w)[<w, 6>], dvs. λ-uttrycket och den aktuella omgivningen. Ett sådant här par kallas ofta för ett funktionshölje (eng. closure) och kan lite grovt ses som ett λ-uttrycks värde. Resultatet blir: w + (y 2), [ y, (λz.z w)[<w, 6>], w, 6 ] För att kunna evaluera additionen så måste vi ha värden på de två operanderna; vi måste därför evaluera de två uttrycken w och (y 2) i den aktuella omgivningen. Evalueringen av den första fortgår enligt följande. Vi startar med: w, [ y, (λz.z w)[<w, 6>], w, 6 ] En variabel evalueras genom att den ersätts med det värde som den är bunden till i omgivningen. Vi får då: 6, [ y, (λz.z w)[<w, 6>], w, 6 ] Längre kommer vi inte med den första operanden, värdet av den blir 6. Nu till den andra operanden som är lite besvärligare; vi börjar med: (y 2), [ y, (λz.z w)[<w, 6>], w, 6 ] För att komma vidare så måste vi evaluera y, dvs. ersätta den med sitt värde från omgivningen; lägg märke till att värdet är ett funktionshölje. För att evaluera en applikation där operatorn är ett funktionshölje utvidgar vi dess omgivning med den bundna variabeln bunden till (värdet av) operanden. Resultatet av detta blir: z w, [ z, 2, w, 6 ] Efter att ha evaluerat operanderna till multiplikationen, dvs. tagit reda på deras värden i omgivningen, så får vi: 2 6, [ z, 2, w, 6 ] dvs. den andra operanden till additionen är 12. Vi kan nu evaluera additionen och få värdet av hela uttrycket: Så Detta avslutar exemplet. 18, [ y, (λz.z w)[<w, 6>], w, 6 ] (λw.((λy.w + (y 2)) (λz.z w))) 6 18

185 178 KAPITEL 5. λ-kalkyl Övning 5.7 Skriv en BNF-grammatik för λ-uttryck. Övning 5.8 Vilka redex innehåller följande λ-uttryck? a. (λx.y) (λx.x) z b. x (λx.x x) (λx.x) c. (λx.y x) ((λz.x) (λy.y)) d. λx.λy.x(x(y)) e. ((λx.x) x) ((λy.z y) (λy.z y)) Övning 5.9 Reducera följande λ-uttryck till normalform: a. (λx.x) (y z) b. (λx.x y) (λz.z) c. λx y.x y d. λx y.y x e. (λx y z.z (z y)) (λw.z w) z Övning 5.10 Man brukar ibland förklara LET och WHERE konstruktionerna i funktionella språk genom att säga att de bara är alternativa sätt att skriva λ- uttryck. Man tänker sig att man har definitionerna: (LET x = e IN e ) (e WHERE x = e) ((λx.e ) e) Använd dessa definitioner för att översätta: (LET z = 4 IN (LET f = (LET z = 1 IN (λx.z)) IN (f z))) till ett vanligt λ-uttryck. Övning 5.11 Evaluera uttrycket i föregående övning med hjälp av omgivningar. Se också vad som händer under evalueringen om man binder en variabel till en λ-abstraktion istället för att binda den till ett helt funktionshölje (λ-abstraktion och aktuell omgivning).

186 5.3. λ-kalkyl Likhetsregler Vilka regler skall nu finnas för att formellt beskriva när två λ-uttryck betecknar samma funktion? För det första måste vi ha regler som säger att = är en ekvivalensrelation. M = M (REFL ρ) M = N N = M (SYM σ) L = M M = N (TRANS τ) L = N Dessutom har vi regler som säger att man kan substituera lika för lika i applikationer och abstraktioner. M = N (µ) L M = L N L = M L N = M N M = N (ξ) λx.m = λx.n De här reglerna är nödvändiga för att vi skall få en likhetsrelation med naturliga egenskaper. Men det måste naturligtvis finnas fler regler; regler som är speciella för det vi vill att λ-uttrycken ska betyda. Om vi tänker på att det är meningen att λ-termerna skall stå för funktioner, så är det klart att det inte spelar någon roll vilken variabel som vi använder i en λ-bindning; λx.x och λy.y skall naturligtvis beteckna samma funktion. Vi får regeln: λx.m = λy.m[x:=y] (α) (där y inte får förekomma fri i M) Dessutom skall det finnas en regel som beskriver betydelsen av applikation. (ν) (λx.m)n = M[x:=N] (β) Observera att problemet med fria och bundna variabler togs om hand redan när vi definierade substitutions-operationen. Slutligen har vi den likhetsregel som hör ihop med η-reduktionsregeln: λx.(m x) = M (η) (där x inte får förekomma fri i M)

187 180 KAPITEL 5. λ-kalkyl Man kan notera att alla likhetsreglerna utom α och σ har en motsvarande beräkningsregel. Det går därför att presentera reglerna för likhet även på följande sätt: 1. Om M N så M = N (alternativt M N M=N ) 2. Om M = N så N = M (alternativt M=N N=M ) 3. Om L = M och M = N så L = N (alternativt 4. λx.m = λy.m[y:=x] (och y ej fri i M) L=M M=N L=N ) Som ett exempel på hur man kan använda de formella reglerna skall vi se hur man kan visa att om M x = N x så M = N (EXT ζ) följer från (ν) och (η): (där x inte får förekomma fri i M eller N) Exempel: Antag att Genom att använda (ν) får vi M x = N x λx.(m x) = λx.(n x) Nu kan vi använda (η), λx.(n x) = N, och transitivitet för att få λx.(m x) = N Om vi sedan än en gång använder (η), λx.(m x) = M, och fortsätter med reglerna för symmetri och transitivitet, så får vi M = N Dvs. (EXT): Om M x = N x så M = N. Vi kan också presentera detta bevis på följande sätt: λx.m x = M (η) M = λx.m x (σ) M x = N x λx.m x = λx.n x (ξ) λx.n x = N (η) λx.m x = N M = N (π) (τ) På motsvarande sätt kan vi från (EXT) och (β) visa (ν) och (η).

188 5.3. λ-kalkyl 181 Exempel: Från (β) får vi, om x inte förekommer fri i M, att (λx.(m x)) y = M y Men från detta följer direkt av (EXT) att λx.m x = M dvs. η. Exempel: Om vi antar att M = N så kan vi med följande två instanser av (β): (λx.m) x = M (λx.n) x = N och genom att använda symmetri och transitivitet visa att (λx.m) x = (λx.n) x Om vi nu använder (EXT), så får vi λx.m = λx.n Dvs (ν): Om M = N så λx.m = λx.n Kombinatorer Ett alternativt sätt att formulera λ-kalkyl är i termer av kombinatorer. Vi har sett att hanterandet av bundna och fria variabler har vållat en del problem. Kombinatorer är ett radikalt försök att komma till rätta med dessa problem genom att helt enkelt försöka klara sig utan bundna variabler. Vi börjar med följande definition: Definition 5.9 En λ-term utan fria variabler kallas en kombinator. Exempel på kombinatorer: I λx.x K λx y.x S λx y z.x z (y z)

189 182 KAPITEL 5. λ-kalkyl Ett förbluffande resultat, tycker jag, är att alla λ-termer utan fria variabler kan skrivas med hjälp av S och K samt applikation. Kombinatorn I, till exempel, kan skrivas som S K K eftersom S K K = ((λx y z.x z (y z)) K) K = λz.k z (K z) = λz.z = λx.x I Det pris som man får betala för de mycket få primitiverna är att en del λ-termer blir aningen komplicerade, till exempel, λx y.y x (λx.(λy.(y x))) = λx.(s I (K x)) = S (K (S I)) (S (K K) I) = S (K (S (S K K))) (S (K K) (S K K)) Men även om resultatet kan bli ohanterligt stort, så är det viktigt att veta att själva översättningen kan göras helt mekaniskt (se övning 5.13). Kombinatorer har under senaste tiden dragit till sig ett allt större intresse som maskinspråk för att exekvera funktionella språk. och en del beräkningsmekanismer kan enklast förklaras i termer av kombinatorer. För intresserade så finns det en referens till en artikel av David Turner bland litteraturhänvisningarna. Övning 5.12 Om det känns förbluffande att alla λ-termer utan fria variabler kan skrivas i termer av S, K och applikation, hur vore det om de kunde skrivas i termer av en kombinator och applikation. Definiera X på följande sätt: Vad är då a. X X X? b. X (X X)? X λt.t K S K Övning 5.13 Betrakta följande regler för att översätta λ-termer till kombinatorerna S, K och I. 1. λx.x = I 2. λx.m = K M, om x ej förekommer fri i M.

190 5.3. λ-kalkyl λx.m N = S (λx.m) (λx.n) Använd reglerna för att översätta några av följande λ-termer till kombinatorform: a. λx y.y b. λx y.x y c. λx y.x y y d. λx y z.x y z e. λx y z.x (y z) Representation av naturliga tal och andra dataobjekt Det är uppenbart att vi kan uppfatta en λ-term som en partiell funktion från λ-termer till λ-termer. Om vi applicerar en λ-term på en λ-term, så blir ju resultatet (av reduktionen), om det finns, också en λ-term. Eftersom vi mekaniskt kan beräkna (reducera) ett λ-uttryck utan fria variabler, kan vi undersöka vilken klass av funktioner som är λ-beräkningsbara, eller med andra ord, vilka funktioner som vi kan representera i λ-kalkyl. För att relatera λ-kalkyl till beräkningsbarhet och då framförallt till de beräkningsbara partiella funktionerna från N till N, måste vi på något sätt representera (implementera) datatypen N i λ-kalkyl. Vi gör detta genom att representera de naturliga talen och dess primitiva operationer som λ-termer. För att få en enkel implementering skall vi börja med att implementera en del andra datatyper. Vi börjar med Bool; denna datatyp kan implementeras genom följande definitioner: T λtrue.λfalse.true representerar true F λtrue.λfalse.false false Observera att de true och false som förekommer i λ-uttrycken är variabler och inte har något att göra med sanningsvärdena true och false. Vi skulle lika gärna kunna säga att λt.λf.t representerar true, men det finns en viss systematik i vårt sätt att representera objekt och den syns bäst om vi använder variabelnamn som liknar de konstanter som vi vill representera. if then else-uttrycket kan representeras på följande sätt IF λb.λe 1.λe 2.b e 1 e 2

191 184 KAPITEL 5. λ-kalkyl eftersom IF T e 1 e 2 (λb.λe 1.λe 2.b e 1 e 2 ) (λtrue.λfalse.true) e 1 e 2 = (λe 1.λe 2.((λtrue.λfalse.true)e 1 e 2 )) e 1 e 2 = (λtrue.λfalse.true) e 1 e 2 = (λfalse.e 1 ) e 2 = e 1 IF F e 1 e 2 = e 2 Vi kan också implementera datatypen Cartesisk produkt genom följande definitioner: M, N λpair.pair M N FST λx.x (λf s.f) SND λx.x (λf s.s) De här definitionerna är korrekta eftersom det gäller att FST M, N (λx.x (λf s.f)) (λpair.pair M N) = (λpair.pair M N) (λf s.f) = (λf s.f) M N = M SND M, N = N Vi är nu i en position där vi kan välja en enkel representation av de naturliga talen. Vi väljer följande definitioner: 0 T, T SUCC λx. F, x ISZERO FST PRED SND Beteckningen n används för den λ-term som representerar talet n. Vi lämnar det som övning att visa att dessa definitioner uppfyller de fundamentala lagarna för naturliga tal. Övning 5.14 Visa att a. SND M, N = N b. IF F e 1 e 2 = e 2

192 5.3. λ-kalkyl 185 Övning 5.15 Visa att följande likheter gäller a. PRED(SUCC x) = x b. ISZERO 0 = T c. ISZERO (SUCC x) = F Övning 5.16 Vad blir PRED 0? Övning 5.17 Ett alternativt sätt att implementera de naturliga talen (det sätt som Church använde) är att välja följande representation: 0 λsucc.λzero.zero 1 λsucc.λzero.succ zero 2 λsucc.λzero.succ(succ zero). n λsucc.λzero.succ n zero Efterföljarfunktionen kan med denna representation implementeras genom λ-uttrycket SUCC λx.λsucc.λzero.x succ (succ zero) Visa nu att: a. λ-uttrycket implementerar addition. b. λ-uttrycket λx.λy.λsucc.λzero.x succ (y succ zero) λx.λy.x y implementerar multiplikation, med M N λx.m(n(x)). c. λ-uttrycket implementerar exponentiering. λx.λy.y x Övning 5.18 Använd Churchs representation av de naturliga talen i de följande övningarna. Definiera ett λ-uttryck ITER som är sådant att ITER 0 f g = g ITER (SUCC n) f g = f(iter n f g) dvs. sådant att ITER n f g = f n (g)

193 186 KAPITEL 5. λ-kalkyl Övning 5.19 Definiera ett λ-uttryck MINUS som implementerar funktionen x. y (se övning 2.13) Övning 5.20 Använd tidigare definierade λ-uttryck för att definiera ett λ-uttryck eqn som är sådant att eqn 0 0 = T eqn (SUCC x) 0 = F eqn 0 (SUCC x) = F eqn (SUCC x) (SUCC y) = eqn x y Övning 5.21 (Relativt svår) Definiera ett λ-uttryck som implementerar pred funktionen Tips: Använd ITER och ordnade par Rekursion Vi skall nu se hur man kan representera rekursiva funktioner i λ-kalkyl. Låt oss som exempel ta fakultetsfunktionen och antag att vi har definierat λ-uttryck PLUS och MULT som representerar addition och multiplikation. För att representera fakultetsfunktionen skulle vi vilja införa definitionen: FAC λn. IF (ISZERO n) } (SUCC 0) } (MULT n (FAC (PRED n))) Men det här är ju ingen riktig definition, det som ska definieras används ju i högerledet. FAC förklaras i termer av sig själv. Det som vi skrivit är ingen definition, utan snarare en ekvation som vi vill lösa; vi vill hitta ett λ-uttryck FAC som uppfyller ekvationen. Detta är vårt mål. Låt oss införa följande definition H λf.λn.if ( ISZERO n) } (SUCC 0) } (MULT n (f (PRED n))) Om vi tittar närmare på den här definitionen, så kan vi se att den har stora likheter med högerledet i definitionen av FAC ovan. Det enda vi gjort är att vi skrivit f istället för FAC och sedan abstraherat ut f. Vi har nu en helt riktig definition, men det är ju inte den som vi ville ha! Fördelen med H är att vi i termer av den kan beskriva FAC; FAC är helt enkelt en fixpunkt till H. Det vill säga ett λ-uttryck som är sådant att om H appliceras på det, så blir resultatet likadant som argumentet. Vi söker alltså ett λ-uttryck FAC som är sådant att H FAC = FAC

194 5.3. λ-kalkyl 187 Nu är det så lyckligt att det finns λ-uttryck som när de appliceras på något ger en fixpunkt till det som det appliceras på. Ett sådant uttryck kallas för en fixpunktskombinator eller en paradoxal kombinator och brukar betecknas Y. En enkel fixpunktskombinator kan definieras: Y (λf.(λx.f(x x)) (λx.f(x x))) Det är enkelt att se att Y ger en fixpunkt om den appliceras på en term. Y g (λf.(λx.f(x x)) (λx.f(x x))) g = (λx.g(x x)) (λx.g(x x)) = g ((λx.g(x x))((λx.g(x x))) = g (Yg) Om vi vänder på denna likhet, så får vi g (Y g) = Yg vilken säger att Y g är en fixpunkt till g!! Om vi vill ha en fixpunkt till ett λ- uttryck M, så behöver vi bara applicera Y på M. Eftersom FAC skulle vara en fixpunkt till H, så kan vi därför definiera FAC enligt följande: FAC Y H Övning 5.22 Reducera FAC 2 till normalform. Använd likheterna: ISZERO 2 = F ISZERO 1 = F ISZERO 0 = T MULT 2 1 = 2 MULT 1 1 = 1 PRED 2 = 1 PRED 1 = 0 Övning 5.23 Definiera λ-uttryck för addition och multiplikation av naturliga tal, där de naturliga talen är representerade som par. Övning 5.24 Definiera också längdfunktionen för listor (antag att vi har implementerat en datatyp för listor med operationerna NIL, CONS, ISNULL, HD och TL) och likhetsfunktionen för naturliga tal med hjälp av Y-kombinatorn.

195 188 KAPITEL 5. λ-kalkyl Övning 5.25 Det är inte bara λ-uttrycket Y som ger en fixpunkt till sitt argument. Visa att också λ-termen Z λf.((λx.f(λz.x x z)) (λx.f(λz.x x z))) gör det. Visa också att man kan reducera (Z M) med applikativ ordning, men att detta inte går med (Y M)

196 5.4. λ-kalkyl OCH BERÄKNINGSBARHET λ-kalkyl och beräkningsbarhet Vi skall avsluta avsnittet om λ-kalkyl med att relatera de partiella funktioner som kan definieras som λ-termer till de P-beräkningsbara partiella funktionerna. Vi gör detta genom att först visa att alla P-beräkningsbara funktioner kan definieras som λ-termer och sedan indikerar vi hur en λ-term utan fria variabler kan beräknas av ett P-program. De partiella funktioner som kan definieras som λ-termer är alltså precis de P-beräkningsbara funktionerna! Om vi tänker oss λ-kalkyl som ett programmeringsspråk, så kan vi se att syntaxen ges av den induktiva definitionen av λ-termer på sidan 163 och semantiken av beräkningsrelationen λ, som vi definierar på följande sätt: Definition 5.10 Om M λ N och N är på normalform, så säger vi att M:s värde är N, vilket vi skriver M λ N. Via beräkningsrelationen kan vi också definiera den partiella funktion mellan λ- termer som ett λ-uttryck M beräknar Definition 5.11.Ett λ-uttryck M beräknar (eller definierar) den partiella funktionen λ[m ] Λ Λ för vilken det gäller att λ[m ] (N) = { L, odefinierad, om det finns ett L sådant att (M N) λ L annars. Vi får också en motsvarighet till P-beräkningsbar partiell funktion: Definition 5.12 Den partiella funktionen f Λ Λ är λ-beräkningsbar (λ-definierbar) om det finns ett λ-uttryck som beräknar f. Via implementeringen av naturliga tal, kan vi få en direkt jämförelse med de P- beräkningsbara funktionerna mellan naturliga tal. Definition 5.13 Funktionen f N N är λ-beräkningsbar (λ-definierbar) om det finns ett λ-uttryck M som är sådant att: 1. λ[m ] n = f(n), om n dom(f) 2. (M n) har ingen normalform om n dom(f) n betecknar det λ-uttryck som representerar talet n.

197 190 KAPITEL 5. λ-kalkyl Översättning från P-program till λ-termer Vi visar nu hur ett godtyckligt P-program kan översättas till en λ-term med samma semantik som P-programmet. Vi gör detta genom att helt enkelt presentera en översättningsfunktion (kompileringsalgoritm) från P-program till λ-termer. I själva verket följer översättningen P-programmets syntaktiska struktur, så vi definierar en översättningsfunktion för varje syntaktisk kategori i P: P Prog Λ C Sats Λ E Uttryck Λ X Var Λ V Villkor Λ Λ betecknar mängden (eller språket) av λ-termer. Vi börjar med att ge en översiktlig beskrivning av hur de λ-termer som vi översätter till är tänkta att fungera. Eftersom den λ-term som ska motsvara ett program Q skall ha samma semantik som Q så måste den ha formen λn.m och om m är det λ-uttryck som representerar det naturliga talet m, så skall det gälla att om Q, m P m så (λn.m) m λ m Vidare skall det gälla att om Q, m så skall inte (λn.m) m ha någon normalform. Om vi studerar beräkningsregeln för program, så ser vi att vi får en korrekt översättning av P-programmet PROGRAM(x 1 ) c RESULT(x 2 ) om vi översätter c till ett λ-uttryck λy.c som är sådant att det representerar samma tillstånds-funktion som c, dvs. om (λy.c) r λ r precis då c, r P r. 5 För att beskriva de λ-termer som ska motsvara satser och uttryck i P måste vi först implementera en datatyp för tillstånd. Man kan jämföra detta med hur vi tidigare implementerade datatypen State i P (se sid ). Vi använder en så enkel implementering som möjligt; observera att den är något olik den vi tidigare använde. Vi börjar med att implementera tripler på följande sätt: x 1, x 2, x 3 λtriple.triple x 1 x 2 x 3 FST 3 λx.x (λx 1 x 2 x 3.x 1 ) SND 3 λx.x (λx 1 x 2 x 3.x 2 ) TRD 3 λx.x (λx 1 x 2 x 3.x 3 ) 5 Vi kommer i fortsättningen att konsekvent använda beteckningen r för det λ-uttryck som implementerar r oavsett om r är ett tillstånd, ett naturligt tal eller något annat objekt.

198 5.4. λ-kalkyl OCH BERÄKNINGSBARHET 191 Det gäller då att FST 3 x 1, x 2, x 3 = x 1 SND 3 x 1, x 2, x 3 = x 2 TRD 3 x 1, x 2, x 3 = x 3 Med hjälp av dessa tripler kan vi nu implementera listor enligt följande: NIL T, T, T CONS λa.λl. F, a, l ISNULL FST 3 HD SND 3 TL TRD 3 Detta är en korrekt implementering eftersom ISNULL NIL = T ISNULL (CONS x y) = F HD (CONS x y) = x TL (CONS x y) = y Med hjälp av listor, par, naturliga tal och rekursion kan vi nu implementera datatypen för tillstånd. Vi gör detta på följande sätt: OMEGA NIL UPDATE λx n r.cons x, n r VALUEOF Y (λv x r. IF(ISNULL r) 0 (IF(eqN x (FST (HD r))) (SND (HD r)) (v x (TL r)))) Jämför implementeringen av VALUEOF med följande funktionsdefinition i ML: - fun valueof x r = if isnull r then 0 else if x = fst(hd r) then snd(hd r) else valueof x (tl r)

199 192 KAPITEL 5. λ-kalkyl Vi ser att om X översätter variabler till λ-termer på ett korrekt sätt, så gäller det för varje tillstånd σ = Ω[x 1 n 1,..., x m n m ] att VALUEOF(X [x]) } (UPDATE (X [x 1 ]) (n 1 ) } } (UPDATE... } } } (UPDATE (X [x 1 ]) (n 1 ) OMEGA)...)) = k σ(x) = k om och endast om dvs. implementeringen av tillstånd är korrekt. En sats i P skall översättas till ett λ-uttryck som beter sig precis som satsen. Vi översätter en sats c till λr.c Detta λ-uttryck skall vara sådant att om c, r P r så ska det gälla att (λr.c) r λ r Dessutom skall (λr.c) r inte ha någon normalform om evalueringen av c, r inte terminerar. Uttryck skall översättas till λ-termer på ett liknande sätt; uttrycket e översätts till λ-termen λr.e som är sådan att 1. om e, r P n så (λr.e) r λ n 2. om e, r så har (λr.e) r ingen normalform. En variabel översätts till det λ-uttryck som representerar variabelns index, och ett villkor till ett λ-uttryck som om det appliceras på representationen av ett tillstånd har normalformen T om villkoret är sant och F om villkoret är falskt. Vi får följande översättningsfunktioner från P-fraser till λ-termer (vi använder dubbla parenteser ([...]) kring syntaktiska argument i enlighet med traditionen från denotationssemantik). P [PROGRAM(x 1 ) c RESULT(x 2 )] = } λn.valueof (X [x 1 ])(C [c](update (X [x 2 ]) n OMEGA)) C [x := e] = λr.update(x [x]) (E [e] r) r

200 5.4. λ-kalkyl OCH BERÄKNINGSBARHET 193 C [WHILE v DO c END] = Y (λw.λr. IF (V [v ] r) r (w (C [c] r))) C [c 1 ; c 2 ] = C [c 1 ] C [c 2 ] E [0] = λr.0 E [SUCC(x)] = λr.succ (VALUEOF (X [x]) r) E [PRED(x)] = λr.pred (VALUEOF (X [x]) r) V [x 0] = λr.issucc(valueof(x [x])r) }} where ISSUCC λx. IF (ISZERO x) F T X [Xn] = n Till det yttre ser de här översättningsfunktionerna ut som en denotationssemantik för språket P. Skillnaden är bara hur man uppfattar λ-uttrycken. I en denotationell beskrivning uppfattas λ-uttrycken som namn på matematiska objekt som används för att förklara P-fraserna. Här är vi däremot intresserade av att visa att λ-kalkyl har samma styrka som P genom att översätta varje P-fras till ett λ-uttryck som har samma beteende som P-frasen. Observera hur översättningen av en sammansatt syntaktisk konstruktion sker i termer av dess delar. Se hur de λ-uttryck som delarna översätts till komponeras till det slutgiltliga resultatet. Låt oss slutligen som hastigast se att översättningen sker på det sätt som vi indikerade ovan. 1. Den första beräkningsregeln har utseendet: 0, σ P 0 Om översättningen är korrekt skall det gälla att (E [0] σ) λ 0 men E [0] = λr.0 så det följer direkt. 2. Den andra beräkningsregeln för uttryck har följande utseende: SUCC(x), σ P s(σ(x)) För att visa att översättningen är korrekt, måste vi visa att (E [SUCC (x)] σ) λ SUCC (VALUEOF (X [x]) σ)

201 194 KAPITEL 5. λ-kalkyl dvs. att ((λr.succ (VALUEOF (X [x]) r)) σ λ SUCC (VALUEOF (X [x]) σ) vilket vi genast ser är sant. De övriga två beräkningsreglerna för uttryck, samt beräkningsreglerna för satser och program, kan behandlas på precis samma sätt. Vi har alltså översiktligt visat att översättningen av P-program till λ-uttryck bevarar semantiken, dvs. att den är en korrekt kompileringsalgoritm från P till Λ. Övning 5.26 Vilket λ-uttryck kommer programmet att översättas till? PROGRAM(X0) X1:=SUCC(X0) RESULT(X1) Övning 5.27 Visa att varje primitivt rekursiv funktion, dvs. varje funktion f PRF, är λ-definierbar. Övning 5.28 Visa att översättningen av satser är korrekt.

202 5.4. λ-kalkyl OCH BERÄKNINGSBARHET En interpretator för λ-termer skriven i P I förra avsnittet visade vi att varje P-program kunde representeras som en λ- term. λ-kalkyl är alltså teoretiskt sett lika kraftfullt som språket P. Men det kan fortfarande vara mer uttrycksfullt. Tänk om det det finns funktioner som är λ-definierbara men som inte kan beräknas av ett P-program. Om det vore på det sättet vore uppläggningen av den här boken inte speciellt lyckad. Språket P är konstruerat för att vara en modell för mekanisk beräkningsbarhet och eftersom reduktionen i λ-kalkyl är helt mekanisk vore P en dålig modell om det fanns λ- definierbara funktioner som inte vore P-beräkningsbara. Vi skall därför visa att varje reduktion som kan beskrivas av ett λ-uttryck också kan beskrivas av ett P-program. Men istället för att ge en direkt översättning, som vi gjorde i förra kapitlet, skall vi använda en lite annorlunda metod. Vi skall definiera en interpretator för λ-uttryck i P. Om vi kan göra detta har vi visat att varje beräkning som kan beskrivas av ett λ-uttryck motsvaras av en beräkning i P. Nämligen den beräkning som utförs av interpretatorn på en representation av M. Om vi skulle göra detta helt korrekt, så skulle vi skriva ett P-program som med hjälp av normalordningsstrategin kunde reducera ett godtyckligt λ-uttryck till normalform. Men som vi tidigare påpekat brukar man i datalogi tänka sig att ett λ- uttryck är färdigevaluerat när man kommer till något på formen λx.m. Dessutom brukar man (eller brukade åtminstone förr i tiden) använda den applikativa reduktionsordningen. Peter Landin definierade på 60-talet en maskin, secd-maskinen 6 som använde den applikativa ordningen för att reducera λ-termer till formen λx.m, och vår interpretator för λ-uttryck är inget annat än en implementering av denna maskin. Med utgångspunkt från secd-maskinen kan man sedan relativt enkelt konstruera en interpretator som använder normalordningsstrategin och som reducerar även innanför λ-tecknet (se övningarna). För att kunna skriva interpretatorn, måste vi naturligtvis först ha en representation av λ-uttryck i P, dvs. vi måste ha en datatyp för λ-uttryck. Den induktiva definitionen av λ-uttryck som vi gav på sidan 163 kan vi enkelt översätta till följande modulspecifikation: 6 secd-maskinen finns beskriven i P. Landins artikel: The Mechanical Evaluation of Expressions, Computer Journal, vol 6 (1964), no 4, sid

203 196 KAPITEL 5. λ-kalkyl MODULE TYPE: L expr OPERATIONS: mk var Identifier L expr mk abs Identifier L expr L expr mk app L expr L expr L expr is var L expr Bool is abs L expr Bool is app L expr Bool dest var L expr Identifier dest abs bvar L expr Identifier dest abs body L expr L expr dest app rand L expr L expr dest app rator L expr L expr EQUATIONS: is var(mk var(x)) = true is var(mk abs(x, l)) = false is var(mk app(l 1, l 2 )) = false is abs(mk var(x)) = false is abs(mk abs(x, l)) = true is abs(mk app(l 1, l 2 )) = false is app(mk var(x)) = false is app(mk abs(x, l)) = false is app(mk app(l 1, l 2 )) = true dest var(mk var(x)) = x dest abs bvar(mk abs(x, l)) = x dest abs body(mk abs(x, l)) = l dest app rand(mk app(l 1, l 2 )) = l 1 dest app rator(mk app(l 1, l 2 )) = l 2 END Att implementera denna är inte svårt och vi lämnar det som övning. För att kunna utföra reduktioner, behöver vi ett internt tillstånd i maskinen. Detta tillstånd innehåller följande objekt (gissa varför Landin kallade maskinen secd-maskinen). s Stack e Environment c Control d Dump

204 5.4. λ-kalkyl OCH BERÄKNINGSBARHET 197 med följande användning: s Stacken, används för att spara de redan evaluerade deluttryckens värden. Den används ungefär på samma sätt som man normalt använder en stack när man evaluerar aritmetiska uttryck. Vid slutet av beräkningen kommer resultatet att ligga överst på stacken. e Omgivningen, används för att associera värden till de fria variablerna i ett uttryck. En omgivning representeras ofta i form av en associationslista, dvs. en lista av par där första komponenten är en identifierare och andra komponenten ett värde. Funktionen lookup Identifier Environment Value ger som resultat det värde som är associerat med en given identifierare i en omgivning. Man kan jämföra med hur vi använde omgivningar på sid 176 för att evaluera λ-uttryck. c innehåller det uttryck som håller på att evalueras. Man kan jämföra med koden, eller koden och programräknaren, i en vanlig maskin. d Dumpen används för att spara hela evalueringskontext, dvs. trippler av typen Stack Environment Control Innan vi kan beskriva secd-maskinens arbetssätt, måste vi introducera begreppet funktionshölje (eng. closure). Enkelt uttryckt så kan man se ett funktionshölje som värdet av ett λ-uttryck och det består av λ-uttrycket tillsammans med den omgivning i vilken uttrycket skall evalueras. Ibland brukar man även dela upp λ-uttrycket i två delar, den bundna variabeln och funktionskroppen. Om man gör detta kommer ett funktionshölje att ha typen Identifier L expr Environment Vi är nu redo att ge formella definitioner av de datatyper som vi informellt har introducerat. Observera att de olika typerna är ömsesidigt rekursiva: Closure Identifier L expr Environment Value Closure Stack List(Value) Environment Table(Identifier, Value) Control List(L expr + {APP}) Dump List(Stack Environment Control)

205 198 KAPITEL 5. λ-kalkyl APP är en instruktion som vi inför i Control-objekten för att beskriva att vi ska utföra en applikation av den funktion som ligger överst på stacken på det objekt som ligger näst överst. Funktionen kan i det allmänna fallet vara en konstant eller ett objekt av typ Closure. Vi kommer inte behandla några speciella konstanter i vår implementering; i en praktisk implementering måste man naturligtvis ha konstanter för addition och multiplikation av heltal, konstruktion och selektion av listor, etc. men vi gör det enkelt för oss och glömmer helt enkelt alla dessa konstanter. Det enklaste sättet att förstå hur secd maskinen fungerar är att studera hur ett beräkningssteg i maskinen definieras. Maskinens inre tillstånd beskrivs fullständigt av fyrtuppeln s, e, c, d och beroende på värdet av c kommer detta inre tillstånd att transformeras till ett nytt tillstånd enligt följande regler: σ.s, e, [ ], s, e, c.d σ.s, e, c, d s, e, x.c, d lookup(x, e).s, e, c, d s, e, (λx.m).c, d mk closure(x, M, e).s, e, c, d s, e, (M N).c, d s, e, N.M.APP.c, d mk closure(x, M, e ).N.s, e, APP.c, d [ ], x, N.e, M, s, e, c.d Vårt nästa steg är nu närmast trivialt, det gäller bara att skriva ett P-program som utför dessa transformationer och håller på med detta tills c = [ ] och d = [ ]. Förutom de P-förkortningar som vi tidigare definierat, så kommer vi att använda följande förkortningar: MK-CLOSURE(bvar, body, env) skapar ett funktionshölje av den bundna variabeln bvar, funktionskroppen body och omgivningen env. IS-CLOSURE(l) ger true om l är ett funktionshölje och false om l är ett vanligt λ-uttryck. CLOS-BVAR(c) ger som resultat den bundna variabeln i funktionshöljet c. CLOS-EXPR(c) ger funktionskroppen i höljet c. CLOS-ENV(c) ger omgivningen i höljet c. secd-implementeringen har följande utseende (vi tar oss friheten att använda identifierare med små bokstäver för makrovariabler): EXPR L-EVAL(lexp) MACRO: -- lexp : Expr; lexp repr L-expr; -- s repr Stack; e repr Environment; c repr Control; -- d repr Dump;

206 5.4. λ-kalkyl OCH BERÄKNINGSBARHET 199 s := NIL; e := EMPTY; c := CONS(INL(lexp),NIL); d := NIL; WHILE NOT((ISNULL(c)) and (ISNULL(d))) DO IF ISNULL(c) THEN -- deluttrycket färdigevaluerat; -- resultatet av deluttrycket finns överst på -- stacken; -- fortsätt med det som finns i "dumpen" d := HD(d); d := TL(d); s := CONS(FST(d ),s); e := SND(d ); c := TRD(d ); ELSE -- det finns mer att göra i c hdc := HD(c); c := TL(c); IF (ISL(hdc)) THEN -- ingen APP-instruktion lc := OUTL(hdc); -- om språket hade innehållit konstanter -- skulle vi tagit hand om dem här IF IS-VAR(lc) THEN -- värdet av en variabel fås genom att -- slå upp dess värde i omgivningen e x := DEST-VAR(lc); c := LOOKUP(x,e) END IF IS-ABS(lc) THEN -- abstraktioner översätts till -- funktionshöljen bv := DEST-ABS-VAR(hs); by := DEST-ABS-BODY(hs); s := MK-CLOSURE(bv,by,e); c := TL(c) END IF IS-APP(lc) THEN -- applikationer evalueras genom att -- först evaluera operanden(argumentet), -- sedan evaluera operatorn(funktionen) -- och till sist exekvera APP-instr. nd := DEST-APP-RAND(lc); or := DEST-APP-RATOR(lc); c := CONS(INL(nd), CONS(INL(or),CONS(INR(APP),c))) END

207 200 KAPITEL 5. λ-kalkyl ELSE -- ISR(hdc) and OUTR(hdc)=APP -- exekvering av APP-instruktionen: nd := HD(s); or := TL(s); IF IS-CLOSURE(nd) THEN -- om det som ligger överst på stacken -- är ett funktionshölje, så evalueras -- dess kropp i den omgivning som fås -- genom att den gamla omgivningen -- utökas med bindningen av den bundna -- variabeln till det som ligger näst -- överst på stacken (argumentet i -- applikationen) e := CONS({CLOS-BVAR(nd),or}, CLOS-ENV(nd)); c := CONS(CLOS-EXPR(nd),NIL); d := {TL(TL(s)),e,TL(c)} d := CONS(d,d); s := NIL ELSE -- här skulle vi tagit hand om appli- -- kationer av konstanta operationer -- som addition, cons, etc. END END END END; result := HD(s) RESULT(result) Gå igenom implementeringen för att övertyga dig om att den korrekt implementerar β-reduktion med applikativ ordning. Övning 5.29 Implementera datatypen L-expr. Övning 5.30 Genomför evalueringen av (λx.(λy.y) (λx.x x)) (λx.x) med secdmaskinen. Övning 5.31 Hur skall man förändra definitionen av ett beräkningssteg i secdmaskinen för att få en fullständig reduktion, dvs. så att maskinen reducerar redex även innanför λ-tecknet? Övning 5.32 Kan man ändra interpretatorn så att den använder normalordningen för att välja redex att reducera? Hur? När man använder omgivningar för

208 5.4. λ-kalkyl OCH BERÄKNINGSBARHET 201 att implementera reduktion enligt normalordningen, kan man införa följande förbättring: När man slagit upp värdet av en identifierare i omgivningen fortsätter man att evaluera det man slagit upp. Eftersom man använder lat evaluering, är det inte säkert att det man får från omgivningen är evaluerat. Det kan mycket väl vara ett uttryck som kräver många steg för att reduceras till normalform. För att inte behöva upprepa detta arbete när man nästa gång slår upp identifieraren, kan man helt enkelt ersätta det uttryck som identifieraren är bunden till i omgivningen med det värde som man fått i beräkningen. Nästa gång som man slår upp identifieraren i omgivningen, om det blir någon mer gång, behöver man då inte upprepa evalueringen. Värdet av uttrycket blir ju alltid detsamma. Det här sättet att utföra beräkningen kallas ibland för call-by-need eftersom man bara evaluerar ett argument när man behöver det. Man kan karakterisera de olika sätten att evaluera argumentet i en applikation på följande sätt: CALL-BY-VALUE (ivrig evaluering) evaluerer alltid argumentet en gång. CALL-BY-NAME (lat evaluering) evaluerar argumentet lika många gånger som det används i funktionskroppen. CALL-BY-NEED (lat evaluering) evaluerar argumentet maximalt en gång Observera att resultatet av en evaluering alltid blir lika för CALL-BY-NAME och CALL-BY-NEED, och om det blir ett resultat så blir värdet samma även för CALL-BY-VALUE. Men det är inte säkert att en reduktion med CALL-BY- VALUE terminerar även om det finns en normalform. Övning 5.33 PROGRAMMERINGSUPPGIFT: Implementera secd-maskinen i ett lämpligt programmeringsspråk, ml, Pascal eller något liknande. Låt naturliga tal och listor, med lämpliga operationer, finnas i språket.

209 202 KAPITEL 5. λ-kalkyl

210 Kapitel 6 BERÄKNINGSMODELLER Vi skall i detta avsnitt översiktligt beskriva några olika beräkningsmodeller som har föreslagits under årens lopp. I tidigare avsnitt har vi utförligt beskrivit två stycken beräkningsmodeller, programmeringsspråket P och λ-kalkyl. Vi fortsätter nu med ändliga tillståndsmaskiner, Turing-maskiner och rekursiva funktioner. 6.1 Ändliga tillståndsmaskiner Den ändliga tillståndsmaskinen (ätm) är en beräkningsmodell som inte är lika kraftfull som språket P, men som ofta används för att beskriva enklare uppgifter. Det främsta skillnaden mellan en ätm och språket P är att en ändlig tillståndsmaskin bara kräver ett konstant minne för varje exekvering. Givet en ätm, kan vi bestämma en fix övre gräns på det minnesutrymme som krävs för att genomföra en exekvering med godtyckligt indata. Detta går inte att göra varken för P-program eller λ-termer. Ändliga tillståndsmaskiner och teorin för dessa används för att beskriva och modellera många olika slags processer och system, till exempel: 1. mekaniska system som additionsmaskiner och myntautomater, 2. elektroniska kretsar, 3. lexikalanalys i kompilatorer, 4. strängmatchning vid sökning, till exempel i redigeringsprogram, 5. olika slags protokoll för datakommunikation. De ändliga tillståndsmaskinerna har som synes ett brett användningsområde. En ätm använder ett alfabet Σ; den läser ändliga strängar över Σ som indata och kan producera ändliga strängar över Σ som utdata. Exekveringen sker 203

211 204 KAPITEL 6. BERÄKNINGSMODELLER på följande sätt: Maskinen startas i ett starttillstånd, läser sedan en symbol från indata-strängen och går därefter över till ett nytt (eller samma) tillstånd samtidigt som den eventuellt producerar en symbol i utdatasträngen. Beräkningen fortsätter sedan på samma sätt med nästa symbol från indata-strängen, ända tills dess inga fler symboler återstår att bearbeta. Eftersom maskinen äter upp en symbol i indata-strängen i varje beräkningssteg, indata-strängen är ändlig och varje beräkningssteg terminerar, kommer alla ätm-beräkningar att terminera. Vi kan tänka oss att resultatet från en ätm ges på två olika sätt. Antingen producerar maskinen en utdata-sträng som resultat eller också utgör sluttillståndet hela resultatet av beräkningen. Vi skall se exempel på båda dessa slag av resultat i fortsättningen. Vi börjar med de maskiner vars resultat är strängar. För att definiera en sådan ätm så måste vi veta: 1. dess alfabet Σ, 2. mängden av möjliga tillstånd State, 3. starttillståndet s 0, 4. hur maskinen, givet en indatasymbol och ett tillstånd, producerar utdata och ett nytt tillstånd, dvs. en övergångsfunktion (eng. transition function) δ Σ State Σ State Vi har följande definition: Definition 6.1 En ändlig tillståndsmaskin är en fyrtuppel Σ, State, s 0, δ där Σ State s 0 δ är ett alfabet en ändlig mängd tillstånd starttillståndet, s 0 State en övergångsfunktion, δ Σ State Σ State En del skribenter skiljer på indata- (I) och utdata-alfabet (O) och ibland delar man upp övergångsfunktionen (δ) i två delar, en tillståndsövergångsfunktion (δ State Σ State State) och en utdata-funktion (δ O Σ State Σ). Exakt hur man gör är inte speciellt viktigt, eftersom det naturligtvis gäller att δ(a, s) = δ State (a, s), δ O (a, s) δ State (a, s) = fst(δ(a, s)) δ O (a, s) = snd(δ(a, s)) Σ = I O

212 6.1. ÄNDLIGA TILLSTÅNDSMASKINER 205 Det enklaste sättet att presentera en ätm är i form av en riktad graf, där noderna representerar tillstånden och bågarna representerar de möjliga tillståndsövergångarna. Varje båge är märkt med två symboler, den första står för ett indata och den andra ett för utdata. Låt oss se på ett exempel. Exempel: en ätm. Låt oss använda alfabetet Σ B {0, 1} och låt följande graf definiera Betraktad som en fyrtuppel, kan samma maskin skrivas: {0, 1}, {S}, S, { 0, S, 1, S, 1, S, 0, S } Maskinen har endast ett tillstånd, som då naturligtvis också måste vara starttillståndet. Om den aktuella indatasymbolen är en nolla, så produceras en etta som utdatasymbol. Om den aktuella indata-symbolen är en etta, så ger maskinen en nolla som utdata. I båda fallen blir det nya tillståndet samma som det gamla. Eftersom indata kan ses som en lista av nollor och ettor, kan vi säga att maskinen beräknar funktionen f List({0, 1}) List({0, 1}) för vilken det gäller att f(0.k) = 1.f(k) f(1.k) = 0.f(k) Vi skall ge ännu ett exempel på en ätm som ger en utdatasträng som resultat. Låt oss se hur man kan definiera en ätm för att utföra binär addition. Detta exempel illustrerar också hur man kan använda ändliga tillståndsmaskiner för att modellera elektroniska kretsar. Exempel: För att kunna ha par av binära tal som indata och binära tal som utdata, introducerar vi följande alfabet. Σ + {0, 1, 0, 0, 0, 1, 1, 0, 1, 1 }

213 206 KAPITEL 6. BERÄKNINGSMODELLER Vi tänker oss att indata-strängen börjar med de minst signifikanta siffrorna och att den alltid avslutas med symbolen 0, 0. Om vi ska addera de binära talen 1100 = 12 och 101 = 5, så har vi alltså indata-strängen: 0, 1 0, 0 1, 1 1, 0 0, 0 Följande ätm är tillräcklig för att klara av uppgiften. En beräkning med indatasträngen ovan som indata kommer att genomgå följande steg: steg indatasymbol tillstånd utdatasymbol nytt tillstånd 1 0,1 NC 1 NC 2 0,0 NC 0 NC 3 1,1 NC 0 C 4 1,0 C 0 C 5 0,0 C 1 NC Vi får alltså resultatet = 17 av denna exekvering. Observera att den första utdata-symbolen representerar den minst signifikanta siffran i resultatet. Om vi betraktar grafen som definierar den ändliga tillståndsmaskinen, så motsvaras en beräkning av ett spår genom grafen. Vilket spår motsvarar exekveringen ovan? Vi sade i inledningen att de ändliga tillståndsmaskinerna inte var lika kraftfulla som till exempel språket P. Vi skall nu studera ett problem som inte går att lösa med en ätm, men som vi ganska enkelt kunde lösa i P. Problemet är helt enkelt att multiplicera två binära tal på samma sätt som vi adderade dem ovan. Enkelt uttryckt så begränsas de ändliga tillståndsmaskinerna av att de inte kan minnas godtyckligt mycket information. En ätm har ett slags minne vi kunde ju minnas en minnessiffra i additionsmaskinen men maskinens minne utgörs av dess olika tillstånd, så antalet olika minnestillstånd är givet på förhand. Minneskapaciteten

214 6.1. ÄNDLIGA TILLSTÅNDSMASKINER 207 bestäms därför helt av antalet element i tillståndsmängden. Finns det två olika tillstånd, har maskinen ett minne som motsvarar en bit, har den 256 olika tillstånd motsvarar minnet 8 bitar. Låt oss nu se närmare på multiplikationsexemplet: Exempel: Vi börjar med att anta att det finns en maskin för multiplikation och att denna maskin har n olika tillstånd. Låt oss då se hur maskinen klarar av att multiplicera de två n-siffriga binära talen och I = i 1 i 2 J = j 1 j 2... i n... j n I resultatet av multiplikationen i j, gäller det för den n:te siffran att: (i j) n = i 1 j n + i 2 j n i n 1 j 2 + i n j 1 + minnessiffra När maskinen skall beräkna denna siffra, måste den ha tillgång till alla de 2n tidigare indata-symbolerna, i 1, j 1,..., i n, j n. Om varje indata-symbol är en binär siffra, så finns det 2 2n olika möjligheter för dessa siffror, men maskinen har bara n olika tillstånd, den kan bara minnas n olika möjligheter. Det är därför omöjligt att definiera en ätm som klarar av att multiplicera godtyckligt stora binära tal. De ändliga tillståndsmaskiner som vi sett exempel på hittills har alla producerat en utdatasträng givet en indatasträng. I många olika sammanhang är det bättre att låta sluttillståndet, dvs. det tillstånd som maskinen befinner sig i då indatasträngen tar slut, utgöra maskinens resultat. En maskin som skall avgöra om indatasträngen innehåller ett udda eller ett jämnt antal ettor är ett sådant exempel. Man kan gå ytterligare ett steg och istället för att låta sluttillståndet utgöra resultatet, kan man dela in sluttillstånden i acceptabla respektive oacceptabla tillstånd. Vi skall göra detta i fortsättningen. Om sluttillståndet för exekveringen av en viss indatasträng är acceptabelt, så säger vi att maskinen har accepterat indata-strängen. Vi kallar maskiner som fungerar på detta sätt för automater och motsvarigheten till ändliga tillståndsmaskiner för ändliga automater(äa), Istället för att en maskin motsvarar en funktion som avbildar indata på utdata, kan vi se att en automat motsvarar en mängd, mängden av de indatasträngar som maskinen accepterar. Låt oss se på ett exempel. Exempel: Antag att vi vill konstruera en automat som motsvarar mängden av alla strängar över {0, 1} som är sådana att de innehåller ett jämnt antal ettor. För att presentera en automat för denna uppgift, använder vi precis som förut en graf, men vi utesluter alla utdata och använder dubbla cirklar för att beteckna det acceptabla sluttillståndet. Vi får automaten:

215 208 KAPITEL 6. BERÄKNINGSMODELLER Vi har följande definition: Definition 6.2 Matematiskt sett är en ändlig automat en femtuppel (kvintett?): Σ, State, s 0, δ, F där Σ State s 0 δ F är ett alfabet är en mängd tillstånd är starttillståndet, s 0 State är övergångsfunktionen, δ Σ State State är mängden av acceptabla sluttillstånd, F State Eftersom en automat motsvarar en mängd av strängar över ett alfabet och ett språk, syntaktiskt sett, är just en sådan mängd, är det uppenbart att det finns ett direkt samband mellan automater och språk. Låt oss se på ett exempel som visar detta. Exempel: Låt oss försöka konstruera en ÄA som endast accepterar strängar i språket {11 n 01 m }. Alfabetet skall naturligtvis vara {0,1} och det är inte svårt att se att följande maskin löser uppgiften:

216 6.1. ÄNDLIGA TILLSTÅNDSMASKINER 209 Vi har redan sett ett exempel på klassificering av språk, Chomskys språkhierarki i avsnitt 3.3.2, och en fråga är: Vilka språk kan accepteras av en ÄA. Det är inte svårt att visa att de ändliga automaterna kan acceptera precis de reguljära (typ-3) språken. Vi har sett hur man kunde beskriva ett reguljärt språk via en typ-3 grammatik, ett något annorlunda sätt är att använda reguljära uttryck, Språk är, som vi sagt många gånger, bara vanliga mängder, och det är därför möjligt att använda mer typiska mängdoperationer för att definiera ett språk. Antag att vi har ett alfabet Σ = {a 1,..., a n }, då kan vi introducera följande språk: ɛ = {} a 1 = {a 1 }. a n = {a n } Så ɛ betecknar det tomma språket och varje element i alfabetet står för det språk som endast innehåller den sträng som består av elementet ur alfabetet. Lägg märke till beteckningsförvirringen, a i kan nu stå för 1. ett element i alfabetet, dvs. a 1 Σ, 2. en sträng över Σ, dvs. a 1 List(Σ) 3. ett språk över Σ, dvs. a 1 P(List(Σ)). Vad det betyder vid ett visst tillfälle framgår av sammanhanget som det så vackert heter. Vi har nu ett antal språk över Σ. Låt oss fortsätta med att införa ett antal operationer som givet språk över Σ producerar nya språk, också över Σ: Först definierar vi en operation för att konkatenera språk; om X och Y är språk över Σ, så är (X Y ), konkateneringen av X och Y, följande språk: (X Y ) = {append(x, y) x X, y Y } En fras i (X Y ) består helt enkelt av en första del som är en fras i X och en andra del som är en fras i Y. Som man ofta gör i matematisk notation, så glömmer vi och skriver X Y som X Y. Nästa operation är vanlig mängdunion, men vi använder beteckningen i detta sammanhang. Om X och Y är språk över Σ, så är (X Y ) språket som består av alla fraser i X tillsammans med alla fraser i Y. Slutligen har vi operationen repetition, som vi betecknar med X och definerar på följande sätt: X = {append(x 1, (append(x 2,... append(x n 1, x n )...)) x i X}

217 210 KAPITEL 6. BERÄKNINGSMODELLER Om vi kan erhålla en fras genom att konkatenera ett godtyckligt antal strängar (0 eller flera) från språket X, så tillhör frasen språket X. Några exempel kan vara på sin plats. Exempel: Låt oss hela tiden betrakta språk som är definierade över alfabetet {a, b, c, d, r}. Här är då några exempel: c = {c} c = {ɛ, c, cc, ccc, cccc, ccccc,...} (c d) = {c, d} ((c d) a) = {cda} ((c d) a) = {ɛ, cda, cdacda, cdacdacda,...} (((c (a d)) (a d) ) r) = {car, caar, cadr, caaar, caadr,..., cdr, cddr, cdar,...} Oftast brukar man ta bort de flesta parenteserna genom att införa konventionen att repetition binder starkare än konkatenering, som i sin tur binder starkare än union. De sista uttrycken ovan kan då skrivas: c d, cda, (cda) respektive c(a d)(a d) r. De reguljära uttrycken är inget annat än namnen på de språk som vi introducerade ovan. Definition 6.3 Ett reguljärt uttryck över ett alfabet Σ bildas genom följande regler: 1. ɛ är ett reguljärt uttryck över Σ 2. om a Σ så är a ett reguljärt uttryck över Σ 3. om X och Y är reguljära uttryck över Σ så är (a) (X Y ) ett reguljärt uttryck över Σ (b) (X Y ) ett reguljärt uttryck över Σ (c) X ett reguljärt uttryck över Σ Definition av språk med reguljära uttryck förekommer väldigt ofta inom datalogi, till exempel när man skall söka efter textsträngar. Många gånger vet man inte riktigt vad det är man söker efter, och då kan det vara bra att istället söka efter alla (eller den första) förekomsten av en sträng i ett språk. Om språket definieras med hjälp av ett reguljärt uttryck, så kan man göra sökproceduren mycket effektiv. Sökningar av det här slaget kan man ofta göra i olika redigeringsprogram, till exempel vi och emacs (i Unix). Det finns också program (t.ex. lex) som automatiskt genererar syntaxanalysatorer för språk som beskrivs av reguljära uttryck.

218 6.1. ÄNDLIGA TILLSTÅNDSMASKINER 211 Anledningen till att vi i detta sammanhang har tagit upp språk som definieras av reguljära uttryck är följande sats: Sats 6.4 De språk som kan definieras med hjälp av reguljära uttryck är precis de språk som kan accepteras av ändliga automater. Vi lämnar beviset som övning. Övning 6.1 Definiera en ändlig tillståndsmaskin med alfabetet {0, 1} som fungerar som en tvåstegs fördröjningskrets. Presentera maskinen både som en graf och som en fyrtuppel. Visa också stegen för en beräkning med som indata. Övning 6.2 Gör en ätm som kan översätta binära tal till oktala tal. Låt den tomma symbolen ɛ tillhöra alfabetet. Visa hur exekveringen för indata-strängen går till. Övning 6.3 Konstruera en ändlig automat som accepterar strängar av formen 0(10) n, dvs. 0, 010, 01010, ,... Definiera också språket som ett reguljärt uttryck och med hjälp av en reguljär (typ-3) grammatik. Övning 6.4 Konstruera en äa som känner igen identifierare och flyttal enligt Pascals syntax. Övning 6.5 Konstruera en äa med alfabetet {0, 1} som bara accepterar strängar som är sådana att antalet ettor är jämnt delbart med 3. Hur skulle automaten förändras om man också krävde att antalet nollor skulle vara delbart med 2? Övning 6.6 Antag att vi har ändliga automater som accepterar språken X och Y. Hur kan vi få en äa som accepterar språken: 1. (X Y ) 2. (X Y ) 3. X Övning 6.7 Använd föregående övning till att bevisa att det till varje språk som kan definieras av ett reguljärt uttryck finns en äa som accepterar språket. Övning 6.8 Alla äa som vi hittills har beskrivit har varit deterministiska, dvs. från en nod i grafpresentationen har det som mest funnits en utgående båge för varje indata-symbol. Om man tillåter flera olika bågar märkta med samma indatasymbol, så får man en icke-deterministisk ändlig automat. En indata-sträng accepteras om det finns ett sätt att acceptera strängen. Betrakta automaten:

219 212 KAPITEL 6. BERÄKNINGSMODELLER 0 1 A 0 B 1 C E F 0 1 D G Strängarna 00 och 01 accepteras eftersom tillståndsövergångarna A B E respektive A D G leder till accepterande sluttillstånd för de två indatasträngarna. Beskriv en icke-deterministisk äa matematiskt. Hur ska den definieras, vilka strängar accepterar den? Vilken kraftfullhet har den relativt vanliga äa, finns det mängder som kan accepteras av en icke-deterministisk äa som inte kan accepteras av en vanlig Äa?

220 6.2. TURING MASKINER Turing maskiner En Turingmaskin T M är en beräkningsmaskin som skriver och läser från en (pappers)remsa. Remsan är indelad i rutor och är (potentiellt) oändlig i båda riktningarna. En ruta på remsan är antingen tom (vilket vi skriver ) eller innehåller en symbol från ett alfabet Σ T M (dvs. från en icke tom ändlig mängd av primitiva symboler). För enkelhets skull betraktar vi som en vanlig symbol och tänker oss att Σ T M. Turingmaskinen har ett läs/skriv-huvud som är placerat över en viss ruta någonstans utefter remsan och den kan utföra följande primitiva operationer: 1. Ersätta den symbol som finns under läs/skriv-huvudet med en ny symbol ur alfabetet. Om den symbol som ska skrivas är a så använder vi helt enkelt a för att beteckna denna operation. 2. Flytta läs/skriv-huvudet ett steg till vänster. Vi skriver L för att beteckna den här operationen. 3. Flytta läs/skriv-huvudet ett steg till höger. Operationen betecknas R. Låt oss kalla mängden av operationer för Op, dvs. Op = Σ T M {L, R} och vi förutsätter naturligtvis att L och R inte finns med i Σ T M. I varje ögonblick under en beräkning är maskinen i ett av ett ändligt antal möjliga tillstånd, mängden av dessa tillstånd betecknar vi State T M. Maskinens beteende definieras av en uppsättning instruktioner av formen: s, a, o, s där s, s State T M, a Σ T M och o Op En instruktion har följande intuitiva mening: Om tillståndet är s och symbolen under läs/skriv-huvudet är a så skall operationen o utföras och maskinen skall sedan inta tillståndet s. Eftersom maskinen skall uppträda deterministiskt så får det för varje tillstånd s och symbol a som mest finnas en instruktion s, a, o, s Ett alternativt sätt att definiera en maskins beteende är i form av en partiell funktion (jfr. övergångsfunktionen för Ätm) δ State Σ Op State som är sådan att om s, a, o, s

221 214 KAPITEL 6. BERÄKNINGSMODELLER är en instruktion, så gäller att s, a dom(δ) och δ( s, a ) = o, s. Vi kommer i fortsättningen att använda båda sätten att definiera Turingmaskiner. En beräkning går till så att Turingmaskinen startas i ett speciellt starttillstånd och exekverar sedan instruktioner så länge som det finns tillämpbara sådana. Exempel: Låt M vara Turingmaskinen med alfabet Σ = {0, 1, } tillståndsmängden State = {P, Q} och följande instruktioner P 0 R P instruktion 1 P 1 0 Q 2 Q 0 R Q 3 Q 1 R P 4 Om starttillståndet är P och maskinen startas med följande konfiguration (remsa + läs/skriv-huvud position + tillstånd) (remsan) (läs/skrivhuvudet) P (maskinens tillstånd) så ser vi att villkoret för att exekvera den andra instruktionen är uppfyllt, och om vi exekverar instruktionen så kommer konfigurationen att ändras till: Q Efter ytterligare en instruktion (nummer 3) får vi: Q Så här fortsätter exekveringen ända tills vi kommer till konfigurationen P

222 6.2. TURING MASKINER 215 Nu är inte längre någon instruktion tillämpbar. Maskinen stannar då! Ett alternativt sätt att presentera en Turingmaskin är som en riktad graf, med noderna representerande de olika tillstånden och varje båge märkt med en symbol och en operation. Turingmaskinen M ovan, får i denna representation utseendet: Det är självklart att vi kan använda en Turingmaskin för att beskriva en beräkning. Givet en remsa och en placering av läs/skriv-huvudet (indata), vilket vi kallar en positionerad remsa, så bestämmer maskinens instruktioner entydigt hur beräkningen skall fortskrida. Om maskinen kommer till en konfiguration där ingen instruktion är tillämpbar, så stannar maskinen och vi har erhållit en ny positionerad remsa. Eftersom det är möjligt att maskinen aldrig stannar, så kan vi säga att en Turingmaskin beräknar en partiell funktion från positionerade remsor till positionerade remsor. Om vi väljer en kodning av naturliga tal i termer av dessa remsor, så kan vi definiera vad det betyder att en Turingmaskin beräknar en partiell funktion från naturliga tal till naturliga tal. Vi kan sedan studera vilken klass av funktioner som på detta sätt kan beräknas; svaret är, som vi tidigare nämnt, att klassen av Turing-beräkningsbara funktioner är lika med den tidigare definierade klassen av P-beräkningsbara funktioner. Om beräkningen för Turingmaskinen M, med den positionerade remsan α som indata, terminerar med den positionerade remsan β som resultat, så betecknar vi detta M, α T M β Och om beräkningen inte terminerar, så använder vi beteckningen M, α Låt oss i fortsättningen bara använda alfabetet {, 1}, motsvarande en dators alfabet {0,1}. Vi kan då använda följande kodning av naturliga tal i form av positionerade remsor. Låt ett tal n representeras av en blank följd av n stycken ettor och sedan åter en blank. Läs/skriv-huvudet skall vara placerat över den första ettan. Talet 5 skall till exempel representeras av

223 216 KAPITEL 6. BERÄKNINGSMODELLER Låt oss också för enkelhets skull antaga att remsan förövrigt bara består av blanktecken. Givet den här representationen av naturliga tal, så kan vi konstruera Turingmaskiner för att beräkna olika partiella funktioner från naturliga tal till naturliga tal. För att beräkna identitetsfunktionen, id N N N, krävs ingen komplicerad maskin. Det räcker med S 0 Det vill säga maskinen som helt enkelt inte innehåller några instruktioner alls! Om vi ska beräkna den konstanta funktionen zero N N med definition zero(x) = 0 så krävs det en lite mer avancerad maskin. Om maskinen startas med konfigurationen så skall den stanna med S S i Ett sätt att utföra detta är att helt enkelt ersätta alla ettor med blanktecken. Vi skall alltså ha en instruktion 1, S 0,, S 1 som i tillståndet S 0 ersätter en etta under läs/skriv-huvudet med ett blanktecken och sedan ändrar tillståndet till S 1. Efter att den här instruktionen har exekverats, så skall vi flytta läs/skriv-huvudet ett steg till höger. Detta gör vi med instruktionen, S 1, R, S 0 De här två instruktionerna är de enda som behövs, eftersom maskinen med de här två instruktionerna stannar då den i tillståndet S 0 finner ett blanktecken under läs/skriv-huvudet, dvs. maskinen stannar med konfigurationen S 0

224 6.2. TURING MASKINER 217 Om vi betraktar detta som ett naturligt tal, så är det representationen för talet 0, vilket ju alltid skulle bli maskinens resultat. Vi kan se att om maskinen startas med en godtycklig positionerad remsa som representerar ett naturligt tal, så kommer den till slut att nå detta sluttillstånd. Maskinen beräknar alltså funktionen zero. Om vi ritar den resulterade maskinen som en graf, så får vi: Vi kan också tänka oss att representera par av naturliga tal som positionerade remsor. Låt oss koda paret 3, 4 som den positionerade remsan Nu kan vi konstruera en maskin som beräknar funktionen + N N N. Det enklaste sättet att göra detta är helt enkelt att ersätta blanktecknet mellan de naturliga talen med en etta samt att ta bort en inledande etta. Kontrollera att följande maskin gör detta: Genom kodningen av naturliga tal kan vi definiera när en partiell funktion f N N är Turing-beräkningsbar. Om vi låter n beteckna den positionerade remsa som kodar talet n N, så kan vi göra följande definition: Definition 6.5 Turingmaskinen M beräknar den partiella funktionen TM[M ] N N med definition: TM[M ](m) = { n, odefinierad, om M, m T M n annars. Vi kan också göra följande definition:

BER AKNINGSBARHET F OR DATALOGER. Kent Petersson. Institutionen for Datavetenskap Goteborgs Universitet / Chalmers Goteborg, Sweden

BER AKNINGSBARHET F OR DATALOGER. Kent Petersson. Institutionen for Datavetenskap Goteborgs Universitet / Chalmers Goteborg, Sweden BER AKNINGSBARHET F OR DATALOGER Fran till P Kent Petersson Institutionen for Datavetenskap Goteborgs Universitet / Chalmers 412 96 Goteborg, Sweden ii Kent Petersson Department of Computer Science Goteborgs

Läs mer

Tentamen i kurserna Beräkningsmodeller (TDA181/INN110) och Grundläggande Datalogi (TDA180)

Tentamen i kurserna Beräkningsmodeller (TDA181/INN110) och Grundläggande Datalogi (TDA180) Göteborgs Universitet och Chalmers Tekniska Högskola 25 oktober 2005 Datavetenskap TDA180/TDA181/INN110 Tentamen i kurserna Beräkningsmodeller (TDA181/INN110) och Grundläggande Datalogi (TDA180) Onsdagen

Läs mer

Tentamen i kurserna Beräkningsmodeller (TDA181/INN110) och Grundläggande Datalogi (TDA180)

Tentamen i kurserna Beräkningsmodeller (TDA181/INN110) och Grundläggande Datalogi (TDA180) Göteborgs Universitet och Chalmers Tekniska Högskola 16 januari 2007 Datavetenskap TDA180/TDA181/INN110 Tentamen i kurserna Beräkningsmodeller (TDA181/INN110) och Grundläggande Datalogi (TDA180) Onsdagen

Läs mer

Tentamen i kurserna Beräkningsmodeller (TDA181/INN110) och Grundläggande Datalogi (TDA180)

Tentamen i kurserna Beräkningsmodeller (TDA181/INN110) och Grundläggande Datalogi (TDA180) Göteborgs Universitet och Chalmers Tekniska Högskola 19 januari 2005 Datavetenskap TDA180/TDA181/INN110 Tentamen i kurserna Beräkningsmodeller (TDA181/INN110) och Grundläggande Datalogi (TDA180) Onsdagen

Läs mer

Introduktion till formella metoder Programmeringsmetodik 1. Inledning

Introduktion till formella metoder Programmeringsmetodik 1. Inledning Introduktion till formella metoder Programmeringsmetodik 1. Inledning Fokus på imperativa program (ex. C, Java) program betyder härefter ett imperativt program Program bestäms i en abstrakt mening av hur

Läs mer

Primitivt rekursiva funktioner och den aritmetiska hierarkin

Primitivt rekursiva funktioner och den aritmetiska hierarkin Primitivt rekursiva funktioner och den aritmetiska hierarkin Rasmus Blanck 0 Inledning En rad frågor inom logiken, matematiken och datavetenskapen relaterar till begreppet beräkningsbarhet. En del i kursen

Läs mer

Övningshäfte 3: Funktioner och relationer

Övningshäfte 3: Funktioner och relationer GÖTEBORGS UNIVERSITET MATEMATIK 1, MAM100, HT2014 INLEDANDE ALGEBRA Övningshäfte 3: Funktioner och relationer Övning H Syftet är att utforska ett av matematikens viktigaste begrepp: funktionen. Du har

Läs mer

ÄNDLIGT OCH OÄNDLIGT AVSNITT 4

ÄNDLIGT OCH OÄNDLIGT AVSNITT 4 VSNITT ÄNDLIGT OCH OÄNDLIGT Är det möjligt att jämföra storleken av olika talmängder? Har det någon mening om man säger att det finns fler irrationella tal än rationella? Är det överhuvudtaget möjligt

Läs mer

Mängder och kardinalitet

Mängder och kardinalitet UPPSALA UNIVERSITET Matematiska institutionen Erik Melin Specialkursen HT07 28 september 2007 Mängder och kardinalitet Dessa blad utgör skissartade föreläsningsanteckningar kombinerat med övningar. Framställningen

Läs mer

Övningshäfte 2: Induktion och rekursion

Övningshäfte 2: Induktion och rekursion GÖTEBORGS UNIVERSITET MATEMATIK 1, MMG200, HT2017 INLEDANDE ALGEBRA Övningshäfte 2: Induktion och rekursion Övning D Syftet är att öva förmågan att utgående från enkla samband, aritmetiska och geometriska,

Läs mer

Introduktion till algoritmer - Lektion 4 Matematikgymnasiet, Läsåret 2014-2015. Lektion 4

Introduktion till algoritmer - Lektion 4 Matematikgymnasiet, Läsåret 2014-2015. Lektion 4 Introduktion till algoritmer - Lektion 4 Matematikgymnasiet, Läsåret 014-015 Denna lektion ska vi studera rekursion. Lektion 4 Principen om induktion Principen om induktion är ett vanligt sätt att bevisa

Läs mer

Kontinuitet och gränsvärden

Kontinuitet och gränsvärden Kapitel Kontinuitet och gränsvärden.1 Introduktion till kontinuerliga funktioner Kapitlet börjar med allmänna definitioner. Därefter utvidgar vi successivt familjen av kontinuerliga funktioner, genom specifika

Läs mer

Explorativ övning 4 ÄNDLIGT OCH OÄNDLIGT. Övning A

Explorativ övning 4 ÄNDLIGT OCH OÄNDLIGT. Övning A Explorativ övning 4 ÄNDLIGT OCH OÄNDLIGT Första delen av övningen handlar om begreppet funktion. Syftet är att bekanta sig med funktionsbegreppet som en parbildning. Vi koncentrerar oss på tre viktiga

Läs mer

Diskret matematik, lektion 2

Diskret matematik, lektion 2 Diskret matematik, lektion Uppgifter med (*) är överkurs, och potentiellt lite klurigare. Ni behöver inte kunna lösa dessa. 1 Uppgifter 1. Låt A = {1,, 3}, B = {a, b}. Vilka element finns med i... a) A

Läs mer

10. Mängder och språk

10. Mängder och språk Objektorienterad modellering och diskreta strukturer 10. Mängder och språk Sven Gestegård Robertz Institutionen för datavetenskap, LTH 2013 Rekaputilation Vi har talat om satslogik, predikatlogik och härledning

Läs mer

Om ordinaltal och kardinaltal

Om ordinaltal och kardinaltal Matematik, KTH Bengt Ek december 2017 Material till kursen SF1679, Diskret matematik: Om ordinaltal och kardinaltal (Ännu ofullständig version) Mängdteorin kan ses som grunden för all matematik Här skall

Läs mer

Definitionsmängd, urbild, domän

Definitionsmängd, urbild, domän 5B1493, lekt 5, HT06 Funktioner Definition av begreppet Definition: Låt X och Y vara två mängder. En funktion f av typ X Y är detsamma som en delmängd av X Y, sådan att 1. Om (x, y) och (x, z) f, så är

Läs mer

Föreläsning 5. Deduktion

Föreläsning 5. Deduktion Föreläsning 5 Deduktion Hur ett deduktivt system fungerar Komponenter - Vokabulär Ett deduktivt system använder ett visst slags språk som kan kallas för systemets vokabulär. I mindre formella fall är kanske

Läs mer

DD1350 Logik för dataloger. Fö 7 Predikatlogikens semantik

DD1350 Logik för dataloger. Fö 7 Predikatlogikens semantik DD1350 Logik för dataloger Fö 7 Predikatlogikens semantik 1 Kryssprodukt av mängder Om A och B är två mängder så är deras kryssprodukt A B mängden av alla par (a,b), där a A och b B. Ex: A={1,2}, B={3,4},

Läs mer

Objektorienterad modellering och diskreta strukturer. 13. Problem. Sven Gestegård Robertz. Datavetenskap, LTH

Objektorienterad modellering och diskreta strukturer. 13. Problem. Sven Gestegård Robertz. Datavetenskap, LTH Objektorienterad modellering och diskreta strukturer 13. Problem Sven Gestegård Robertz Datavetenskap, LTH 2014 Rekaputilation Vi har talat om satslogik och härledning predikatlogik och substitution mängder

Läs mer

Mängder, funktioner och naturliga tal

Mängder, funktioner och naturliga tal Lådprincipen Följande sats framstår som en fullständig självklarhet: Sats (Lådprincipen (pigeon hole principle)). Låt n > m vara naturliga tal. Fördelar man n föremål i m lådor, så kommer åtminstone en

Läs mer

Programmering II (ID1019)

Programmering II (ID1019) ID1019 Johan Montelius Instruktioner Betyg Programmering II (ID1019) 2019-03-08 Svaren skall lämnas på dessa sidor, använd det utrymme som nns under varje uppgift för att skriva ner ditt svar (inte på

Läs mer

Bakgrund och motivation. Definition av algoritmer Beskrivningssätt Algoritmanalys. Algoritmer. Lars Larsson VT 2007. Lars Larsson Algoritmer 1

Bakgrund och motivation. Definition av algoritmer Beskrivningssätt Algoritmanalys. Algoritmer. Lars Larsson VT 2007. Lars Larsson Algoritmer 1 Algoritmer Lars Larsson VT 2007 Lars Larsson Algoritmer 1 1 2 3 4 5 Lars Larsson Algoritmer 2 Ni som går denna kurs är framtidens projektledare inom mjukvaruutveckling. Som ledare måste ni göra svåra beslut

Läs mer

1.1. Fördjupning: Jämförelse av oändliga mängder

1.1. Fördjupning: Jämförelse av oändliga mängder Kapitel 1 Kardinalitet Den här texten är tagen från boken Diskret matematik av Asratian Björn Turesson (och delvis modifierad) Av den anledningen finns det visa hänvisningar på en del ställen som är ersatta

Läs mer

I kursen i endimensionell analys är mängden av reella tal (eng. real number), R, fundamental.

I kursen i endimensionell analys är mängden av reella tal (eng. real number), R, fundamental. Lunds tekniska högskola Datavetenskap Lennart ndersson Föreläsningsanteckningar EDF10 4 Mängder 4.1 Motivering Mängden är den mest grundläggande diskreta strukturen. Nästan alla matematiska begrepp går

Läs mer

Föreläsning 8 i kursen Ma III, #IX1305, HT 07. (Fjärde föreläsningen av Bo Åhlander)

Föreläsning 8 i kursen Ma III, #IX1305, HT 07. (Fjärde föreläsningen av Bo Åhlander) Föreläsning 8 i kursen Ma III, #IX1305, HT 07. (Fjärde föreläsningen av Bo Åhlander) Böiers 5.3 Relationer. Vi har definierat en funktion f: A B som en regel som kopplar ihop ett element a A, med ett element

Läs mer

Filosofisk Logik (FTEA21:4) föreläsningsanteckningar/kompendium. v. 2.0, den 29/ III. Metalogik 17-19

Filosofisk Logik (FTEA21:4) föreläsningsanteckningar/kompendium. v. 2.0, den 29/ III. Metalogik 17-19 Filosofisk Logik (FTEA21:4) föreläsningsanteckningar/kompendium IV v. 2.0, den 29/4 2013 III. Metalogik 17-19 Modeller för satslogiken 18.1 Vi har tidigare sagt att en modell är en tolkning av en teori

Läs mer

(N) och mängden av heltal (Z); objekten i en mängd behöver dock inte vara tal. De objekt som ingår i en mängd kallas för mängdens element.

(N) och mängden av heltal (Z); objekten i en mängd behöver dock inte vara tal. De objekt som ingår i en mängd kallas för mängdens element. Grunder i matematik och logik (2017) Mängdlära Marco Kuhlmann 1 Grundläggande begrepp Mängder och element 2.01 En mängd är en samling objekt. Två standardexempel är mängden av naturliga tal (N) och mängden

Läs mer

Universitetet i Linköping Institutionen för datavetenskap Anders Haraldsson 2

Universitetet i Linköping Institutionen för datavetenskap Anders Haraldsson 2 Anders Haraldsson 1 Anders Haraldsson 2 Dagens föreläsning Programmering i Lisp Fö 5 - Funktioner - lambda-uttryck (avs 7.1) - funcall och function (avs 7.2) - Högre ordningens funktioner (avs 7.) - Iteratorer

Läs mer

Sista delen av kursen

Sista delen av kursen Sista delen av kursen handlar om hur program, delprogram och datatyper deklareras och vad det man åstadkommit egentligen betyder. Innehåll Syntaktisk (hur ser det ut) och semantisk (vad betyder det) beskrivning

Läs mer

Programspråkslingvistik. Sista delen av kursen. Ett programspråk

Programspråkslingvistik. Sista delen av kursen. Ett programspråk Sista delen av kursen Programspråkslingvistik handlar om hur program, delprogram och datatyper deklareras och vad det man åstadkommit egentligen betyder. Innehåll Syntaktisk (hur ser det ut) och semantisk

Läs mer

Dagens föreläsning Programmering i Lisp Fö 5

Dagens föreläsning Programmering i Lisp Fö 5 Anders Haraldsson 1 Dagens föreläsning Programmering i Lisp Fö 5 - Funktioner - lambda-uttryck (avs 7.1) - funcall och function (avs 7.2) - Högre ordningens funktioner (avs 7.3) - Iteratorer - Egenskaper

Läs mer

Läsanvisning till Discrete matematics av Norman Biggs - 5B1118 Diskret matematik

Läsanvisning till Discrete matematics av Norman Biggs - 5B1118 Diskret matematik Läsanvisning till Discrete matematics av Norman Biggs - 5B1118 Diskret matematik Mats Boij 28 oktober 2001 1 Heltalen Det första kapitlet handlar om heltalen och deras aritmetik, dvs deras egenskaper som

Läs mer

Algebra I, 1MA004. Lektionsplanering

Algebra I, 1MA004. Lektionsplanering UPPSALA UNIVERSITET Matematiska Institutionen Dan Strängberg HT2016 Fristående, IT, KandDv, KandMa, Lärare 2016-11-02 Algebra I, 1MA004 Lektionsplanering Här anges rekommenderade uppgifter ur boken till

Läs mer

13. CHURCH S OCH GÖDELS SATSER. KORT ORIENTERING OM BERÄKNINGSBARHET, EFFEKTIV UPPRÄKNELIGHET OCH AVGÖRBARHET.

13. CHURCH S OCH GÖDELS SATSER. KORT ORIENTERING OM BERÄKNINGSBARHET, EFFEKTIV UPPRÄKNELIGHET OCH AVGÖRBARHET. 81 13 CHURCH S OCH GÖDELS SATSER KORT ORIENTERING OM BERÄKNINGSBARHET, EFFEKTIV UPPRÄKNELIGHET OCH AVGÖRBARHET Våra beräkningar skall utföras på symbolsträngar, där symbolerna tas från ett givet alfabet

Läs mer

Lösning till tentamensskrivning i Diskret Matematik för CINTE, CL2 och Media 1, SF1610 och 5B1118, onsdagen den 17 augusti 2011, kl

Lösning till tentamensskrivning i Diskret Matematik för CINTE, CL2 och Media 1, SF1610 och 5B1118, onsdagen den 17 augusti 2011, kl Matematiska Institutionen KTH Lösning till tentamensskrivning i Diskret Matematik för CINTE, CL och Media, SF60 och 5B8, onsdagen den 7 augusti 0, kl 4.00-9.00. Examinator: Olof Heden Hjälpmedel: Inga

Läs mer

LMA033/LMA515. Fredrik Lindgren. 4 september 2013

LMA033/LMA515. Fredrik Lindgren. 4 september 2013 LMA033/LMA515 Fredrik Lindgren Matematiska vetenskaper Chalmers tekniska högskola och Göteborgs universitet 4 september 2013 F. Lindgren (Chalmers&GU) Matematik 4 september 2013 1 / 25 Outline 1 Föreläsning

Läs mer

Hela tal LCB 1999/2000

Hela tal LCB 1999/2000 Hela tal LCB 1999/2000 Ersätter Grimaldi 4.3 4.5 1 Delbarhet Alla förekommande tal i fortsättningen är heltal. DEFINITION 1. Man säger att b delar a om det finns ett heltal n så att a Man skriver b a när

Läs mer

Exempel. Komplexkonjugerade rotpar

Exempel. Komplexkonjugerade rotpar TATM79: Föreläsning 4 Polynomekvationer och funktioner Johan Thim 2 augusti 2016 1 Polynomekvationer Vi börjar med att upprepa definitionen av ett polynom. Polynom Definition. Ett polynom p(z) är ett uttryck

Läs mer

Sista delen av kursen

Sista delen av kursen Sista delen av kursen handlar om hur program, delprogram och datatyper deklareras och vad det man åstadkommit egentligen betyder. Innehåll Syntaktisk (hur ser det ut) och semantisk (vad betyder det) beskrivning

Läs mer

729G74 IT och programmering, grundkurs. Tema 1, Föreläsning 3 Jody Foo,

729G74 IT och programmering, grundkurs. Tema 1, Föreläsning 3 Jody Foo, 729G74 IT och programmering, grundkurs Tema 1, Föreläsning 3 Jody Foo, jody.foo@liu.se Föreläsningsöversikt Kurslogistik Diskret matematik & Uppgifter i Python Kompletteringar Tema 1: Olika perspektiv

Läs mer

Föreläsning 9: Turingmaskiner och oavgörbarhet. Turingmaskinen. Den maximalt förenklade modell för beräkning vi kommer använda är turingmaskinen.

Föreläsning 9: Turingmaskiner och oavgörbarhet. Turingmaskinen. Den maximalt förenklade modell för beräkning vi kommer använda är turingmaskinen. Föreläsning 9: Turingmaskiner och oavgörbarhet Turingmaskinen Den maximalt förenklade modell för beräkning vi kommer använda är turingmaskinen. Data är ett oändligt långt band där nollor och ettor står

Läs mer

Rekursiva algoritmer sortering sökning mönstermatchning

Rekursiva algoritmer sortering sökning mönstermatchning Anders Haraldsson 1 Anders Haraldsson 2 Dagens föreläsning Programmering i Lisp Fö 6-7 Rekursiva strukturer rekursiva definitioner rekursiva funktioner rekursiva bevis: induktion - rekursion strukturell

Läs mer

Uppsala Universitet Matematiska Institutionen Thomas Erlandsson

Uppsala Universitet Matematiska Institutionen Thomas Erlandsson Uppsala Universitet Matematiska Institutionen Thomas Erlandsson LÄSANVISNINGAR VECKA 36 VERSION 1. ARITMETIK FÖR RATIONELLA OCH REELLA TAL, OLIKHETER, ABSOLUTBELOPP ADAMS P.1 Real Numbers and the Real

Läs mer

Turingmaskiner och oavgörbarhet. Turingmaskinen. Den maximalt förenklade modell för beräkning vi kommer använda är turingmaskinen.

Turingmaskiner och oavgörbarhet. Turingmaskinen. Den maximalt förenklade modell för beräkning vi kommer använda är turingmaskinen. Turingmaskiner och oavgörbarhet Turingmaskinen Den maximalt förenklade modell för beräkning vi kommer använda är turingmaskinen. Data är ett oändligt långt band där nollor och ettor står skrivna: Oändligt

Läs mer

Tentamensskrivning i Diskret Matematik för CINTE och CMETE, SF1610, onsdagen den 20 augusti 2014, kl

Tentamensskrivning i Diskret Matematik för CINTE och CMETE, SF1610, onsdagen den 20 augusti 2014, kl 1 Matematiska Institutionen KTH Tentamensskrivning i Diskret Matematik för CINTE och CMETE, SF1610, onsdagen den 20 augusti 2014, kl 14.00-19.00. Examinator: Olof Heden Hjälpmedel: Inga hjälpmedel är tillåtna

Läs mer

Kompletteringsmaterial. K2 Något om modeller, kompakthetssatsen

Kompletteringsmaterial. K2 Något om modeller, kompakthetssatsen KTH Matematik Bengt Ek Maj 2008 Kompletteringsmaterial till kursen SF1642, Logik för D1 och IT3: K2 Något om modeller, kompakthetssatsen Vi skall presentera ett enkelt (om man känner till sundhets- och

Läs mer

MATEMATIKENS SPRÅK. Avsnitt 1

MATEMATIKENS SPRÅK. Avsnitt 1 Avsnitt 1 MATEMATIKENS SPRÅK Varje vetenskap, liksom varje yrke, har sitt eget språk som ofta är en blandning av vardagliga ord och speciella termer. En instruktionshandbok för ett kylskåp eller för en

Läs mer

DD1361 Programmeringsparadigm. Carina Edlund

DD1361 Programmeringsparadigm. Carina Edlund DD1361 Programmeringsparadigm Carina Edlund carina@nada.kth.se Funktionell programmering Grundidéen med funktionell programmering är att härma matematiken och dess funktionsbegrepp. Matematiskt funktionsbegrepp

Läs mer

Några satser ur talteorin

Några satser ur talteorin Några satser ur talteorin LCB 997/2000 Fermats, Eulers och Wilsons satser Vi skall studera några klassiska satser i talteori, vilka är av betydelse bland annat i kodningsteknik och kryptoteknik. De kan

Läs mer

Föreläsningsanteckningar och övningar till logik mängdlära

Föreläsningsanteckningar och övningar till logik mängdlära Inledande matematisk analys tma970, 010, logik, mängdlära Föreläsningsanteckningar och övningar till logik mängdlära Dessa öreläsningsanteckningar kompletterar mycket kortattat kap 0 och appendix B i Persson/Böiers,

Läs mer

MA2047 Algebra och diskret matematik

MA2047 Algebra och diskret matematik MA2047 Algebra och diskret matematik Något om funktioner och relationer Mikael Hindgren 1 oktober 2018 Funktionsbegreppet Exempel 1 f (x) = x 2 + 1, g(x) = x 3 och y = sin x är funktioner. Exempel 2 Kan

Läs mer

Mängder. 1 Mängder. Grunder i matematik och logik (2015) 1.1 Grundläggande begrepp. 1.2 Beskrivningar av mängder. Marco Kuhlmann

Mängder. 1 Mängder. Grunder i matematik och logik (2015) 1.1 Grundläggande begrepp. 1.2 Beskrivningar av mängder. Marco Kuhlmann Marco Kuhlmann 1 Diskret matematik handlar om diskreta strukturer. I denna lektion kommer vi att behandla den mest elementära diskreta strukturen, som alla andra diskreta strukturer bygger på: mängden.

Läs mer

Föreläsning 5: Kardinalitet. Funktioners tillväxt

Föreläsning 5: Kardinalitet. Funktioners tillväxt Föreläsning 5: Kardinalitet. Funktioners tillväxt A = B om det finns en bijektion från A till B. Om A har samma kardinalitet som en delmängd av naturliga talen, N, så är A uppräknelig. Om A = N så är A

Läs mer

Lösning av tentamensskrivning i Diskret Matematik för CINTE och CMETE, SF1610, tisdagen den 27 maj 2014, kl

Lösning av tentamensskrivning i Diskret Matematik för CINTE och CMETE, SF1610, tisdagen den 27 maj 2014, kl 1 Matematiska Institutionen KTH Lösning av tentamensskrivning i Diskret Matematik för CINTE och CMETE, SF1610, tisdagen den 27 maj 2014, kl 14.00-19.00. Examinator: Olof Heden Hjälpmedel: Inga hjälpmedel

Läs mer

Formell logik Kapitel 1 och 2. Robin Stenwall Lunds universitet

Formell logik Kapitel 1 och 2. Robin Stenwall Lunds universitet Formell logik Kapitel 1 och 2 Robin Stenwall Lunds universitet Kapitel 1: Atomära satser Drömmen om ett perfekt språk fritt från vardagsspråkets mångtydighet och vaghet (jmf Leibniz, Russell, Wittgenstein,

Läs mer

Filosofisk logik Kapitel 15 (forts.) Robin Stenwall Lunds universitet

Filosofisk logik Kapitel 15 (forts.) Robin Stenwall Lunds universitet Filosofisk logik Kapitel 15 (forts.) Robin Stenwall Lunds universitet Dagens upplägg Antalet element i en mängd Kardinalitet Humes princip Cantors teorem Den universella mängden Några mängdteoretiska paradoxer

Läs mer

Avslutning. Vad? Hur? Anmärkningar inför tentan 2. Vad ska kunnas?

Avslutning. Vad? Hur? Anmärkningar inför tentan 2. Vad ska kunnas? Avslutning Anmärkningar inför tentan Vad ska kunnas? Avslutning 1 Vad? Anmärkningar inför tentan 1 Att ha en bra förståelse för det som behandlades på föreläsningarna och gruppövningarna räcker i princip.

Läs mer

inte följa någon enkel eller fiffig princip, vad man nu skulle mena med det. All right, men

inte följa någon enkel eller fiffig princip, vad man nu skulle mena med det. All right, men MATEMATISKA INSTITUTIONEN STOCKHOLMS UNIVERSITET Christian Gottlieb Gymnasieskolans matematik med akademiska ögon Induktion Dag 2. Explicita formler och rekursionsformler. Dag mötte vi flera talföljder,

Läs mer

Kapitel 4. Funktioner. 4.1 Definitioner

Kapitel 4. Funktioner. 4.1 Definitioner Kapitel 4 Funktioner I det här kapitlet kommer vi att undersöka funktionsbegreppet. I de första sektionerna genomgås definitionen av begreppet funktion och vissa egenskaper som funktioner har. I slutet

Läs mer

Induktion, mängder och bevis för Introduktionskursen på I

Induktion, mängder och bevis för Introduktionskursen på I Induktion, mängder och bevis för Introduktionskursen på I J A S, ht 04 1 Induktion Detta avsnitt handlar om en speciell teknik för att försöka bevisa riktigheten av påståenden eller formler, för alla heltalsvärden

Läs mer

Övningshäfte 6: 2. Alla formler är inte oberoende av varandra. Försök att härleda ett par av de formler du fann ur några av de övriga.

Övningshäfte 6: 2. Alla formler är inte oberoende av varandra. Försök att härleda ett par av de formler du fann ur några av de övriga. GÖTEBORGS UNIVERSITET MATEMATIK 1, MAM100, HT2005 MATEMATISK BASKURS Övningshäfte 6: Syftet med övningen är att utforska strukturen hos talsystemen under addition respektive multiplikation samt sambandet

Läs mer

Relationer. 1. Relationer. UPPSALA UNIVERSITET Matematiska institutionen Erik Melin. Specialkursen HT07 23 oktober 2007

Relationer. 1. Relationer. UPPSALA UNIVERSITET Matematiska institutionen Erik Melin. Specialkursen HT07 23 oktober 2007 UPPSALA UNIVERSITET Matematiska institutionen Erik Melin Specialkursen HT07 23 oktober 2007 Relationer Dessa blad utgör skissartade föreläsningsanteckningar kombinerat med övningar. Framställningen är

Läs mer

Föreläsning 5 Innehåll

Föreläsning 5 Innehåll Föreläsning 5 Innehåll Algoritmer och effektivitet Att bedöma och jämföra effektivitet för algoritmer Begreppet tidskomplexitet Datavetenskap (LTH) Föreläsning 5 VT 2019 1 / 39 Val av algoritm och datastruktur

Läs mer

Omgivningar. Omgivningar är viktiga eftersom de avgör vilka namn som är synliga och därmed dessas innebörd och de värden som är förknippade med dem.

Omgivningar. Omgivningar är viktiga eftersom de avgör vilka namn som är synliga och därmed dessas innebörd och de värden som är förknippade med dem. Omgivningar Omgivningar är viktiga eftersom de avgör vilka namn som är synliga och därmed dessas innebörd och de värden som är förknippade med dem. (define (sqrroot c) (define (fixpoint guess c eps) (define

Läs mer

Språket Python - Del 1 Grundkurs i programmering med Python

Språket Python - Del 1 Grundkurs i programmering med Python Hösten 2009 Dagens lektion Ett programmeringsspråks byggstenar Några inbyggda datatyper Styra instruktionsflödet Modulen sys 2 Ett programmeringsspråks byggstenar 3 ETT PROGRAMMERINGSSPRÅKS BYGGSTENAR

Läs mer

Kinesiska restsatsen

Kinesiska restsatsen Matematik, KTH Bengt Ek juli 2017 Material till kurserna SF1679 och SF1688, Diskret matematik: Kinesiska restsatsen Vi vet att för varje m Z + och varje a Z, ges alla x Z som uppfyller x a (mod m) av x

Läs mer

Tentamen i. TDDC67 Funktionell programmering och Lisp

Tentamen i. TDDC67 Funktionell programmering och Lisp 1 Linköpings tekniska högskola Institutionen för datavetenskap Anders Haraldsson Tentamen i TDDC67 Funktionell programmering och Lisp och äldre kurser TDDC57 Programmering, Lisp och funktionell programmering

Läs mer

Filosofisk Logik (FTEA21:4) föreläsningsanteckningar I. v. 2.0, den 24/4 2013

Filosofisk Logik (FTEA21:4) föreläsningsanteckningar I. v. 2.0, den 24/4 2013 Filosofisk Logik (FTEA21:4) föreläsningsanteckningar I v. 2.0, den 24/4 2013 Om detta kompendium: Filosofiska institutionen, Lunds Universitet staffan.angere@fil.lu.se Förberedande Det här kompendiet är

Läs mer

K2 Något om modeller, kompakthetssatsen

K2 Något om modeller, kompakthetssatsen KTH Matematik Bengt Ek Maj 2005 Kompletteringsmaterial till kursen 5B1928 Logik för D1: K2 Något om modeller, kompakthetssatsen Vi skall presentera ett enkelt (om man känner till sundhets- och fullständighetssatsen

Läs mer

TDDC74 Programmering: Abstraktion och modellering Tentamen, onsdag 19 oktober 2016, kl 14 18

TDDC74 Programmering: Abstraktion och modellering Tentamen, onsdag 19 oktober 2016, kl 14 18 TDDC74 Programmering: Abstraktion och modellering Tentamen, onsdag 19 oktober 2016, kl 14 18 Läs alla frågorna först, och bestäm dig för i vilken ordning du vill lösa uppgifterna. Skriv tydligt och läsligt.

Läs mer

1 Föreläsning Implikationer, om och endast om

1 Föreläsning Implikationer, om och endast om 1 Föreläsning 1 Temat för dagen, och för dessa anteckningar, är att introducera lite matematisk terminologi och notation, vissa grundkoncept som kommer att vara genomgående i kursen. I grundskolan presenteras

Läs mer

Instruktioner - Datortentamen TDDD73 Funktionell och imperativ programmering i Python

Instruktioner - Datortentamen TDDD73 Funktionell och imperativ programmering i Python Instruktioner - Datortentamen TDDD73 Funktionell och imperativ programmering i Python Hjälpmedel Följande hjälpmedel är tillåtna: Exakt en valfri bok, t.ex. den rekommenderade kursboken. Boken får ha anteckningar,

Läs mer

Block 2 Algebra och Diskret Matematik A. Följder, strängar och tal. Referenser. Inledning. 1. Följder

Block 2 Algebra och Diskret Matematik A. Följder, strängar och tal. Referenser. Inledning. 1. Följder Block 2 Algebra och Diskret Matematik A BLOCK INNEHÅLL Referenser Inledning 1. Följder 2. Rekursiva definitioner 3. Sigmanotation för summor 4. Strängar 5. Tal 6. Övningsuppgifter Referenser Följder, strängar

Läs mer

Grundläggande logik och modellteori

Grundläggande logik och modellteori Grundläggande logik och modellteori Kapitel 8: Predikatlogik Henrik Björklund Umeå universitet 2. oktober, 2014 Första ordningens predikatlogik Signaturer och termer Första ordningens predikatlogik Formler

Läs mer

TDDC74 Programmering: Abstraktion och modellering Tentamen, onsdag 9 juni 2016, kl 14 18

TDDC74 Programmering: Abstraktion och modellering Tentamen, onsdag 9 juni 2016, kl 14 18 TDDC74 Programmering: Abstraktion och modellering Tentamen, onsdag 9 juni 2016, kl 14 18 Läs alla frågorna först, och bestäm dig för i vilken ordning du vill lösa uppgifterna. Skriv tydligt och läsligt.

Läs mer

TATM79: Föreläsning 4 Funktioner

TATM79: Föreläsning 4 Funktioner TATM79: Föreläsning 4 Funktioner Johan Thim augusti 08 Funktioner Vad är egentligen en funktion? Definition. En funktion f är en regel som till varje punkt i en definitionsmängd D f tilldelar precis ett

Läs mer

Dagens teman. Mängdlära forts. Relationer och funktioner (AEE 1.2-3, AMII K1.2) Definition av de naturliga talen, Peanos axiom.

Dagens teman. Mängdlära forts. Relationer och funktioner (AEE 1.2-3, AMII K1.2) Definition av de naturliga talen, Peanos axiom. Dagens teman Mängdlära orts. Relationer och unktioner (AEE 1.2-3, AMII K1.2) Deinition av de naturliga talen, Peanos axiom. Relationer och unktioner Relationer Generell deinition: En relation R på mängden

Läs mer

Algoritmer, datastrukturer och komplexitet

Algoritmer, datastrukturer och komplexitet Algoritmer, datastrukturer och komplexitet Övning 12 Anton Grensjö grensjo@csc.kth.se 10 december 2015 Anton Grensjö ADK Övning 12 10 december 2015 1 / 19 Idag Idag Komplexitetsklasser Blandade uppgifter

Läs mer

Matematik 5 Kap 2 Diskret matematik II

Matematik 5 Kap 2 Diskret matematik II Matematik 5 Kap 2 Diskret matematik II Inledning Konkretisering av ämnesplan (länk) http://www.ioprog.se/public_html/ämnesplan_matematik/struktur_äm nesplan_matematik/struktur_ämnesplan_matematik.html

Läs mer

Sådana avbildningar kallar vi bijektioner mellan A och B (eller från A till B).

Sådana avbildningar kallar vi bijektioner mellan A och B (eller från A till B). BIJEKTION, INJEKTION, SURJEKTION Allmän terminologi. I samband med variabelbyte vid beräkning av integraler har vi en avbildning mellan två mängder A och B, dvs en funktion f : A B. Vi har oftast krav

Läs mer

TMV166/186 Linjär Algebra M/TD 2011/2012 Läsvecka 1. Omfattning. Innehåll 2012-01-20. Lay, kapitel 1.1-1.9, Linjära ekvationer i linjär algebra

TMV166/186 Linjär Algebra M/TD 2011/2012 Läsvecka 1. Omfattning. Innehåll 2012-01-20. Lay, kapitel 1.1-1.9, Linjära ekvationer i linjär algebra TMV166/186 Linjär Algebra M/TD 2011/2012 Läsvecka 1 Omfattning Lay, kapitel 1.1-1.9, Linjära ekvationer i linjär algebra Innehåll Olika aspekter av linjära ekvationssystem 1. skärning mellan geometriska

Läs mer

Dataabstraktion. TDDD73 Funktionell och impterativ programmering i Python Föreläsning 12. Peter Dalenius Institutionen för datavetenskap

Dataabstraktion. TDDD73 Funktionell och impterativ programmering i Python Föreläsning 12. Peter Dalenius Institutionen för datavetenskap Dataabstraktion TDDD73 Funktionell och impterativ programmering i Python Föreläsning 12 Peter Dalenius Institutionen för datavetenskap 2013-11-12 Översikt Vad är abstraktion? Vad är en abstrakt datatyp?

Läs mer

Kombinatorik. Kapitel 2. Allmänt kan sägas att inom kombinatoriken sysslar man huvudsakligen med beräkningar av

Kombinatorik. Kapitel 2. Allmänt kan sägas att inom kombinatoriken sysslar man huvudsakligen med beräkningar av Kapitel 2 Kombinatorik Allmänt kan sägas att inom kombinatoriken sysslar man huvudsakligen med beräkningar av det antal sätt, på vilket elementen i en given mängd kan arrangeras i delmängder på något sätt.

Läs mer

Två fall: q Tom sekvens: () q Sekvens av element: (a b c) ; (sum-rec '(2 4 6)) = 12. q Första elementet uppfyller vissa villkor: (2 a b c)

Två fall: q Tom sekvens: () q Sekvens av element: (a b c) ; (sum-rec '(2 4 6)) = 12. q Första elementet uppfyller vissa villkor: (2 a b c) Programmönster: # Listan som sekvens, Rekursiv process Enkel genomgång av sekvens (element på toppnivån i en lista)) TDDC60 Programmering: abstraktion och modellering Föreläsning 5 Rekursiva och iterativa

Läs mer

Diskret Matematik A för CVI 4p (svenska)

Diskret Matematik A för CVI 4p (svenska) MITTHÖGSKOLAN TFM Tentamen 2004 MAAA98 Diskret Matematik A för CVI 4p (svenska) Skrivtid: 5 timmar Datum: 3 juni 2004 Denna tentamen omfattar 10 frågor, där varje fråga kan ge 12 poäng. Delfrågornas poäng

Läs mer

Programkonstruktion och Datastrukturer

Programkonstruktion och Datastrukturer Programkonstruktion och Datastrukturer VT 2012 Tidskomplexitet Elias Castegren elias.castegren.7381@student.uu.se Problem och algoritmer Ett problem är en uppgift som ska lösas. Beräkna n! givet n>0 Räkna

Läs mer

Logik: sanning, konsekvens, bevis

Logik: sanning, konsekvens, bevis Logik: sanning, konsekvens, bevis ft1100 samt lc1510 HT 2016 Giltiga argument (Premiss 1) (Premiss 2) (Slutsats) Professorn är på kontoret eller i lunchrummet Hon är inte på kontoret Professorn är i lunchrummet

Läs mer

Lösningar till utvalda uppgifter i kapitel 3

Lösningar till utvalda uppgifter i kapitel 3 Lösningar till utvalda uppgifter i kapitel 3 3.37 (a) Att ` ' är reexiv, antisymmetrisk och transitiv följer direkt av att `den vanliga' är det på N och Z. (b) Följden m n = ( n, n) där n = 0, 1, 2,...

Läs mer

TDDC74 Programmering: Abstraktion och modellering Tentamen, lördag 27 augusti 2016, kl 8 12

TDDC74 Programmering: Abstraktion och modellering Tentamen, lördag 27 augusti 2016, kl 8 12 TDDC74 Programmering: Abstraktion och modellering Tentamen, lördag 27 augusti 2016, kl 8 12 Läs alla frågorna först, och bestäm dig för i vilken ordning du vill lösa uppgifterna. Skriv tydligt och läsligt.

Läs mer

Klassdeklaration. Metoddeklaration. Parameteröverföring

Klassdeklaration. Metoddeklaration. Parameteröverföring Syntax: Class Declaration Modifier Class Body Basic Class Member Klassdeklaration class Class Member Field Declaration Constructor Declaration Method Declaration Identifier Class Associations Motsvarar

Läs mer

Programmering i C++ En manual för kursen Datavetenskaplig introduktionskurs 5p

Programmering i C++ En manual för kursen Datavetenskaplig introduktionskurs 5p Programmering i C++ En manual för kursen Datavetenskaplig introduktionskurs 5p Skriven av Michael Andersson Introduktion Programmering I högnivåspråk fokuserar på själv problemet (algoritmen) istället

Läs mer

n Detta för att kunna koncentrera oss på n Tal: number? n Symboler: symbol? n Strängar: string? n Tecken: char? n Boolskt: boolean?

n Detta för att kunna koncentrera oss på n Tal: number? n Symboler: symbol? n Strängar: string? n Tecken: char? n Boolskt: boolean? Tidigare TDDC74 Programming: Abstraktion och modellering Föreläsning 4 Symboler, Par, Listor Representation av par, Grafisk notation för par Representation av listor mha par Typiska listhanteringsprocedurer

Läs mer

En bijektion mellan två mängder A och B som har ändligt antal element kan endast finnas om mängderna har samma antal element.

En bijektion mellan två mängder A och B som har ändligt antal element kan endast finnas om mängderna har samma antal element. BIJEKTION, INJEKTION, SURJEKTION NUMRERBARA (eller UPPRÄKNELIGA) MÄNGDER Allmän terminologi. I samband med variabelbyte vid beräkning av integraler har vi en avbildning mellan två mängder A och B, dvs

Läs mer

Abstrakt algebra för gymnasister

Abstrakt algebra för gymnasister Abstrakt algebra för gymnasister Veronica Crispin Quinonez Sammanfattning. Denna text är föreläsningsanteckningar från föredraget Abstrakt algebra som hölls under Kleindagarna på Institutet Mittag-Leffler

Läs mer

Typsystem. DA2001 (Föreläsning 23) Datalogi 1 Hösten / 19

Typsystem. DA2001 (Föreläsning 23) Datalogi 1 Hösten / 19 Typsystem Typsystem finns i alla programmeringsspråk. Avsikten med typsystem är att kontrollera att uttryck är säkra i den bemärkelsen att innebörden i operanderna är klar och inte är motsägelsefull och

Läs mer

Våra enkla funktioner eller procedurer

Våra enkla funktioner eller procedurer Föreläsning 3 Våra enkla funktioner eller procedurer Programmönster 1. Repetition 2. Högre-ordningens procedurer/programmönster - Procedurer som argument - Procedurer som returnerade värden 3. Scope och

Läs mer

Instruktioner - Datortentamen TDDE24 och TDDD73 Funktionell och imperativ programmering (i Python)

Instruktioner - Datortentamen TDDE24 och TDDD73 Funktionell och imperativ programmering (i Python) Instruktioner - Datortentamen TDDE24 och TDDD73 Funktionell och imperativ programmering (i Python) Hjälpmedel Följande hjälpmedel är tillåtna: Exakt en valfri bok, t.ex. den rekommenderade kursboken. Boken

Läs mer

Övningshäfte 1: Logik och matematikens språk

Övningshäfte 1: Logik och matematikens språk GÖTEBORGS UNIVERSITET MATEMATIK 1, MMG200, HT2014 INLEDANDE ALGEBRA Övningshäfte 1: Logik och matematikens språk Övning A Målet är att genom att lösa och diskutera några inledande uppgifter få erfarenheter

Läs mer

a = a a a a a a ± ± ± ±500

a = a a a a a a ± ± ± ±500 4.1 Felanalys Vill man hårddra det hela, kan man påstå att det inte finns några tal i den tillämpade matematiken, bara intervall. Man anger till exempel inte ett uppmätt värde till 134.78 meter utan att

Läs mer