Programmeringsmetodik (en introduktion till formella metoder) J. von Wright Kurskompendium, våren 2002 1 Att tala om program och deras egenskaper Programmeringsmetodik handlar om hur man kan skriva program som fungerar som avsett. Vi vill uppfatta program som matematiska objekt som kan diskuteras, jämföras och manipuleras. Vi koncentrerar oss på imperativa program som arbetar genom att steg för steg göra ändringar i programvariabler, via tilldelningar som struktureras med villkorskonstruktioner, iterationer, procedurer mm. Exempel på imperativa programmeringsspråk är C, Java och Pascal. 1.1 Programsystem och programfragment Klassisk programanalys handlar i första hand om sekventiella input/output-program, dvs program som beräknar ett sluresultat (utdata) på basen av på förhand givna indata. Många av dagens programsystem kan beskrivas som bestående av samverkande interaktiva komponenter. Deras uppgift är att hålla igång en verksamhet snarare än att beräkna ett resultat. Men om man tittar på de byggstenar som sådana system är uppbyggda av så hittar man, längst in, sekventiella programfragment som kan analyseras på traditionell sätt. Sådana programfragment utgörs i olika programmeringsspråk av procedurer, funktioner, metoder mm. Här försöker vi taka om programfragment på ett så allmänt plan som möjligt, oberoende av programmeringsspråk. 1.2 Konkret och abstrakt syntax Syntax handlar om de skrivregler som bestämmer hur uttryck eller satser i ett språk får se ut. Ett språk som skall läsas (tolkas) av en dator följer i allmänhet stränga syntaxregler. En konkret syntax beskriver de teckensträngar som är acceptabla. Detta kan göras med syntaxbeskrivningar, tex enligt BNF (Backus-Naur Form). Vi är här mer intresserade av abstrakt syntax, som beskriver uttryck och programsatser som träd, där en nod motsvarar ett uttryck, med ett subträd för varje deluttryck. Till 1
exempel kan uttrycket x + 2 y beskrivas som en addition av två deluttryck, där det vänstra är variabeln x och det högra är en multiplikation av talet 2 och variabeln y. I trädform blir detta + x y 2 Den abstrakta syntaxen skall, som namnet säger, vara abstrakt, dvs bortse från detaljer. Samma abstrakta syntaxträd motsvarar också andra konkreta syntaxer, tex x;2;y + (enligt sk omvänd polsk notation, där semikolon motsvarar enter ). En abstrakt syntax kan beskrivas med en tudelad syntaxbeskrivning, som på många sätt liknar en konkret syntax. Den abstrakta syntaxen beskrivs genom att vi anger syntaktiska kategorier, med ett namn och en metavariabel för varje kategori, och regler: en syntaxregel för varje syntaktisk kategori. Som exempel beskriver vi (något förenklat) uttryck som kan förekomma i ett programmeringsspråk. 1. De syntaktiska kategorierna är heltal (n), variabler v, logiska uttryck b och aritmetiska uttryck e. 2. Reglerna är e ::= n v e + e e e b ::= e = e not b b and b Märk att vi inte ger regler för heltal och variabler. Det kan bero på att de är atomära (variabler) eller att vi inte bryr oss om deras inre struktur (heltal). Reglerna visar hur vi kan bygga korrekta syntaxträd för aritmetiska och logiska uttryck. I praktiken vill vi inte skriva ut syntaxträd, utan vi använder parenteser för att visa strukturen, tex (x =1)and ((x +1)=((y z) + 2)) Med hjälp av regler för precedens (tex att multiplikation går före addition ) och association (tex att en serie additioner utförs från vänster till höger) kan vi minska mängden parenteser, tex x =1and x +1=y z+2 2
1.3 Syntaktiska objekt De syntaxträd som den abstrakta syntaxen beskriver är syntaktiska objekt som vi kan tala om och jämföra. Vi kan tex fråga om djupet hos ett syntaktiskt objekt (dvs längden hos den längsta stigen i syntaxträdet). Vi kan fråga om ett uttryck ingår som deluttryck i ett annat (märk tex att 2 + 3 inte är ett deluttryck i 4 2+3 5 eftersom multiplikation har högre precedens än addition). Vi kan också bevisa påståenden som handlar om de syntaktiska objekten. Tex i följande syntax (där de syntaktiska kategorierna är atomer a och satser s): s ::= a s seq s kan man visa att i varje uttryck är antalet atomer större än antalet seq. Beviset sker med strukturell induktion, dvs vi visar att det är sant för varje alternativ i syntaxen och i varje delbevis antar vi att påståendet är sant för de deluttryck som ingår. 1.4 Semantik Ett programmeringsspråks semantik handlar om vad program betyder. Den som skriver program måste ha något slags intuitiv bild av vad programmeringsspråkets olika konstruktioner betyder då de exekveras av en dator. Denna intuitiva semantik kan förklaras med hjälp av hur programsatser ändrar på tillståndet, dvs på programvariablernas värden. En mer exakt, formell semantik beskriver programmeringsspråkets betydelse med hjälp av matematisk och logisk notation. Vi återkommer till detta i ett senare avsnitt. 1.5 En enkel programnotation De flesta verkliga programmeringsspråk är så mångsidiga och vildvuxna att de inte lämpar sig som underlag för denna kurs. Dessutom har de ofta syntaktiska egenheter som är omotiverade och inte allmängiltiga, och som gör det svårt att se den underliggande strukturen i en programtext. Därför väljer vi att begränsa oss till en enkel programmeringsnotation som endast innehåller de grundläggande byggstenar som vi vill tala om. Syntaxen för denna notation är S ::= abort skip V := E S ; S if B then S else S fi do B S od där S står för programsatser, V står för programvariabler, E står för uttryck och B för logiska utryck. Några kommentarer till denna syntax är på sin plats: Vi antar att varje programvariabel x har en typ och att vid en tilldelning x := E har uttrycket E samma typ som x. Vi beskriver inte syntaxen för uttryck, utan vi antar att den följer rimliga standarder, för de olika typer som kommer ifråga. Exempel på rimliga uttryck är 2 x y (kan vara av typ num, dvs heltalsuttryck) och x<y(typ bool, dvs logiskt uttryck) Intuitive kan semantiken för denna programnotation beskrivas så här: 3
abort är ett misslyckat program - vi vet ingenting om hurudant resultat det ger. Det används för att modellera felsituationer, oändliga loopar mm. skip leder inte till några ändringar i programvariabler (kallas ibland no-op). En tilldelning x := E exekveras så att värdet på uttrycket E beräknas i det aktuella tillståndet och variabeln x får sedan detta värde. Inga andra variabler än x ändras. En sekvens S ; S exekveras så att S exekveras först, och sedan S. En villkorssats if B then S else S fi exekveras så att värdet på uttrycket B (villkorssatsens vakt) först beräknas. Om värdet är true så exekveras S, och om det är false så exekveras S. En loop do B S od exekveras så att värdet på vakten B först beräknas. Om värdet är true så exekveras S (loopens kropp) och sedan beräknas värdet på uttrycket B på nytt, osv. Ett exempel på ett programfragment skrivet i denna notation är var x, y, r : num r := 0 ; do y>0 r:= r + x ; y := y 1 od där vi antar att x, y och r alla är naturliga tal. Variabeldeklarationen i början anger namn och typ för alla variabler som ingår i programfragmentet. Märk att det inte finns skilda in- eller utmatningssatser i vår programmeringsnotation. I stället antar vi att programvariablerna har initialiserats före exekveringen (input) och att de kan avläsas efter att exekveringen avslutats (output), dvs att programfragmentet fungerar som en procedur 1 som får indata och ger resultat via parametrar. I exemplet ovan är x och y tänkta att vara input (värdeparametrar) och r output (resultatparameter). Om vi vill beskriva ett program som ett syntaxträd skall vi notera att tex loopen är en konstruktion med två delar medan if-satsen har tre. Till exempel ger programfragmentet var x, i : num ; a : array[n] of num i := 0 ; j = n ; do i<j if a[i] <xthen i := i +1else j := j 1 fi od 1 Vi använder här konsekvent begreppet procedur för för det som i olika programmeringsspråk kallas tex metod, funktion, procedur eller subrutin. 4
följande träd: ; ; := 0 i := while n j i<j if := a[i] <x := i j i +1 j 1 Märk att vi har lämnat uttryck som a[i] <xoch i + 1 ouppdelade, så att trädet inte skall bli alltför stort. 1.6 Multipla tilldelningar Man stöter ofta på situationer där flere variabler skall tilldelas värden som är oberoende av varandra, i den meningen att tilldelningarna lika väl kunde utföras samtidigt. Exempel på detta är initialiseringar av typen i := 1 ; m := a[0] För att göra det enkelt att hantera sådana tilldelningar som kommer efter varandra utökar vi begreppet tilldelning till att omfatta multipel tilldelning, där flere variabler samtidigt tilldelas värden, tex i, m := 1,a[0] som ger samma resultat som de två tilldelningarna ovan. Multipla tilldelningar förekommer inte i vanliga programmeringsspråk, så de skall uppfattas som en abstraktion, något som vi introducerar för att enkelt kunna tala om något som i ett verkligt programmeringsspråk måste uttryckas på något mer komplicerat sätt. Ett exempel på uttryckskraften hos multipla tilldelningar är att vi kan uttrycka swappande enkelt: x, y := y, x 1.7 Grundläggande datatyper Olika programmeringsspråk utgår ifrån olika uppsättningar datatyper och många moderna språk ger också möjligheter att definiera nya komplicerade datatyper för att beskriva 5
fenomen och strukturer i verkligheten. Här antar vi en liten men användbar samling datatyper som duger både för konkreta och mer abstrakta exempel. Den booleska dadatypen bool innehåller de två logiska värdena T (sant) och F (falskt). Vi använder logikens notation för logiska operationer ( för och, för eller, för inte, för ekvivalens och för implikation). De naturliga talen num är 0,1,... Märk att vi inte antar någon övre gräns för num. Vi använder den vanliga aritmetikens symboler (med för multiplikation, div för heltalsdivision och mod för resten vid division). Om vi någon gång vill tala om heltal (dvs också negativa tal) använder vi datatypen int, men i praktiken duger num sgs hela tiden. Datatypen list står för listor av element av en och samma typ. Tex är [1, 4, 2, 5] av typen num list. Följande operationer på listor antas bekanta: hd(l) ger första elementet (head) i listan l, tl(l) ger resten då första elementet tagits bort (tail) och x :: l skapar en ny lista med huvud x och svans l. Dessutom kan vi sammanfoga (append) två listor l 1 och l 2 genom att skriva l 1 @l 2. Skrivsättet [1, 4, 2, 5] är egentligen en förkortning av 1 :: (4 :: (2 :: (5 :: [ ]))) där [ ] är den tomma listan. Mängder bildas med set ochvianvänder vanliga mängdsymboler. Tex betyder x a att x ingårimängden a och a b betyder unionen av mängderna a och b. Slutligen kan vi bilda arraystrukturer (tabeller) med array. Tex betyder a : array[n] of num att a är en array med plats för n naturliga tal (där n kan vara en ospecificerad konstant). Vid behov kan arraystrukturer antas vara obegränsade, och vi skriver då bara tex array of num. Platserna i en array numreras med början från 0 och vi hänvisar till elementet på plats i i arrayn a som a[i]. Vid behov kan vi också tala om ett delområde a[i..j] av en array. 1.8 Samband med riktiga programmeringsspråk Den programmeringsnotation som vi använder här är en abstraktion; den bortser från många av de detaljer som man måste ta hänsyn till då man implementerar program i ett verkligt programmeringsspråk: in- och utmatning, begränsat talområde, minnesutrymme och arraybegränsningar, osv. Genom att arbeta med en abstraktion kan vi koncentrera oss på de uträkningar och datamanipulationer som görs i ett program. Och genom att arbeta med en minimal notation (dvs bara en enda villkorskonstruktion, en enda loopkonstruktion, osv) så behöver antalet regler för hur man drar slutsatser om program inte bli oöverskådligt många. För att kunna tillämpa kunskapen på verkliga programmeringsspråk måste vi i varje fall ha något sätt att jämföra program i de olika notationerna. En enkel översättningsregel till tex Java skulle då kunna innehålla bla följande: := motsvaras i Java av = do B S od motsvaras i Java av while (B) {S} osv. Märk att semikolon i vår notation skiljer åt en programsats från nästa (semikolon skapar sekvens) medan semikolon i Java är en avslutare. I Java finns det alltså också 6
ett semikolon efter den sista satsen i ett block eller ett program. I allmänhet rekommenderar man användning av långa variabelnamn som visar vad en variabel står för, men här använder vi oftast korta namn, för att undvika att formler och bevis blir svårlästa. Övningar 1. Rita de abstrakta syntaxträden som motsvarar följande aritmetiska uttryck (antag att multiplikation har högre precedens än addition och att båda associerar till höger): (a) 2 3+4 5 (b) 2 + 3 4+5. 2. Antag följande syntax för något slags uttryck: e ::= n eope där n står för heltal och op för operatorer. Motivera att följande är sant: i varje uttryck är antalet heltal större än antalet operatorer. 3. Rita det abstrakta syntaxträd som motsvarar följande program(antag att semikolon (;) associerar till höger): x := 3 ; y := 4 ; do x>0 x:= x +1;y:= y +1od. 4. Programfragmentet do y>0 r:= r +x;y := y 1 od exekveras i ett starttillstånd där x =3,y=4ochr= 0. Vad har r för värde när loopen terminerar? 5. Översätt programfragmentet do y>0 r:= r + x ; y := y 1 od till Java. 6. Skriv om följande genom att införa multipla tilldelningar, där det är möjligt: (a) x := x +1;y:= y +1 (b) x := x +1;y:= x +1 (c) x := y +1;y:= y +1 (d) x := y +1;y:= x +1 7. Denna uppgift handlar om de två enklaste datatyperna num och bool. (a) (b) Antag ett tillstånd där x har värdet 3, y har värdet 5 och f har värdet F. Vad är värdet av följande uttryck: x + y>3 x f (x>y) Antag ett tillstånd där x har värdet 5, y har värdet 3 och f har värdet F. Vilka värden har x, y och f efter att följande tilldelningar utförts: x, y := x + y, x + y f := (x >y) 8. Denna uppgift handlar om listdatatyper. 7
(a) (b) (c) Förenkla följande listuttryck: hd(tl([1, 3, 2, 4])) tl([1, 2, 3])@tl([4, 5, 6, 7]) Vi kan definiera en ny funktion över listor genom att säga vad den gör med den tomma listan och vad den gör med en sammansatt lista. Förklara vad funktionen f gör, om f([ ]) = [ ] f(h :: t) = f(t)@[h] Tips: försök beräkna f([1, 2, 3]) steg för steg. Antag att följande regler för append -operationen @ är givna: []@t = t (h :: t)@t = h :: (t@t ) Hur skulle man gå tillväga för att visa att t@[ ] = t gäller för alla listor t? 9. Antag att a är av typen array[n] of num och att i och j är indexvariabler vars värden är tillåtna, dvs högst n 1. (a) Beräkna värdet av följande uttryck, om n är 6, a innehåller (1, 3, 5, 7, 9, 11), och i =2ochj=3: a[i]+a[j]+a[i+j] a[a[j]] + a[a[i]] a[a[j i]] (b) Låt a(i e) betyda den array som uppstår då man ersätter det som finns på plats i i a med e. Omaär som i (a)-uppgiften, vad är då a(2 a[3] + a[4]) (a(1 2))(3 4) 2 Programannotationer Vi skall nu se hur man kan analysera en programtext genom att införa annotationer, dvs påståenden om programmets tillstånd. Avsikten med annotationer är att förklara hur programmet arbetar (dvs varför det åstadkommer det som det är avsett att åstadkomma). 2.1 Tillstånd och programvariabler Programvariablernas värden i en given situation kallas ett tillstånd (eng. state). Exekvering av ett programfragment (tex ett proceduranrop) startar i ett starttillstånd (eng. initial state). Exekveringen kan sedan antingen misslyckas (oändlig loop eller avbrott på grund av fel) eller så terminerar (eng. terminate) den i ett sluttillstånd (eng. final state). 8
Ett tillstånd kan alltså beskrivas med en tabell som visar varje programvariabels värde. Antag att vi talar om ett program med programvariablerna x och y (naturliga tal) och b (av logisk typ). Då är (x = 3,y = 2,b = T) ett tillstånd. Ett annat tillstånd är (x = 0,y = 2,b = F). Mängden av alla sådana möjliga tillstånd kallas programmets tillståndsrum (eng. state space). Vi använder ofta σ (lilla sigma) som namn på ett tillstånd och Σ (stora sigma) som namn på ett tillståndsrum. Grafiskt åskådliggör vi tillstånd som punkter och tillståndsrum som mängder: σ Σ 2.2 Tillståndspredikat Ett logiskt uttryck som säger något om ett tillstånd kallas ett tillståndspredikat (eng. state predicate). Ett exempel på ett tillståndspredikat är b x =3 Detta predikat är sant (vi säger också att predikatet gäller) i tillståndet (x = 3,y = 2,b = T) men falskt i tillståndet (x = 0,y = 2,b = F). Allmänt taget beskriver ett predikat en mängd av tillstånd, nämligen de tillstånd där predikatet är sant. Att evaluera ett predikat i ett tillstånd innebär att substituera in programvariablernas värden i predikatet och därefter avgöra dess värde (som blir antingen T eller F). I ett tillståndspredikat får vi använda programvariabler och logikens symboler, men vid behov också andra matematisk-logiska begrepp, förutsatt att vi har förklarat dem tillräckligt detaljerat (tex genom exakta definitioner). I praktiken kommer man långt med klassisk logik, aritmetik och mängdlära, utökad med begrepp som behövs för att hantera tex array-strukturer. Predikat är ordnade enligt styrka. Vi säger att predikatet p är starkare än predikatet q (och q är svagare än p) ifall p q gäller för alla tillstånd, dvs för alla möjliga värden på de olika programvariablerna. Tex är predikatet x 2 starkare än predikatet x>0, eftersom man kan visa att följande är sant: ( x x 2 x>0) Predikaten x = 2 och x = 3 är ojämförbara när det gäller styrka. Ett starkare predikat begränsar tillåtna tillstånd mer än ett svagt. Det starkaste av alla predikat är F medan T är det svagaste. Ett predikat kan uppfattas som em mängd av tillstånd, så att ett predikat p motsvarar alla de tillstånd där p är sant. Att p är starkare än q kan då skrivas p q (dvs p är en delmängd av q), medan p utgör komplementet till p: 9
Σ p p 2.3 Att skriva tillståndspredikat Man kan skriva rätt komplicerade tillståndspredikat med exakt matematisk-logisk notation. Tex kan vi uttrycka att en array a[0..n 1] är sorterad som predikatet ( i <n 1 a[i] a[i+ 1]) Märk alltså att tillståndspredikat som talar om program kan innehålla tex kvantifierare ( och ) som inte kan användas i booleska uttryck som ingår i program. Märk också att den bundna variabeln i en kvantifiering kan begränsas som i exemplet ovan; följande är ekvivalenta: ( i <n 1 a[i] a[i+ 1]) och ( i i<n 1 a[i] a[i+ 1]) liksom följande: ( i <n 1 a[i] a[i+ 1]) och ( i i<n 1 a[i] a[i+ 1]) Om vi vill skriva tillståndspredikat som tex talar om att arrays är sorterade så blir det snabbt långt och svårläst. Då kan vi definiera ett nytt begrepp sorted för arrays (och delar av arrays) enligt sorted(a[m..n]) def = ( i m i<n a[i] a[i+ 1]) Efter detta säger sorted(b[0..n 1]) att arrayn b är sorterad (om n är antalet element i b). Definitionen ger oss alltså en möjlighet att skriva korta och klara tillståndspredikat utan att ge avkall på logisk exakthet. 2.4 Annoterad programkod En annotation är en matematisk-logisk kommentar (vanligen ett tillståndspredikat) som skrivs in i programtexten. Avsikten är att visa vad som skall gälla om programmets tillstånd på den plats där annotationen finns. Annotationer skrivs inom klamrar ({...}, eng. braces eller curly brackets), tex var x, y : num x := 1 ; {x =1} y:= x +1 {x=1 y=2} 10
Detta är en annoterad version av programmet var x, y : num x := 1 ; y := x +1 Tilläggen inom klamrar är annotationer eller påståenden (eng. assertion) som ger information om programvariablernas värden efter varje programsats. I detta fall är det mycket enkelt att skriva in annotationer, eftersom (a) (b) programmets struktur är mycket enkel, och variablernas startvärden inte spelar någon roll. Ofta har vi någon information om programvariablernas startvärden, dvs vi vet ett förvillkor (eng. precondition). Detta kan då utgöra en startannotation som vi sedan använder för att steg för steg beräkna nya annotationer. Också omviintevetnågot speciellt om en programvariabel kan det vara bra att introducera ett namn på startvärdet, tex x 0 eller X om variabeln heter x. Till exempel kan vi annotera programfragmentet var x, y, z : num if x y then z := x else z := y fi på följande sätt. Först därefter var x, y, z : num {x = x 0 y = y 0 } if x y then z := x else z := y fi, var x, y, z : num {x = x 0 y = y 0 } if x y then {x = x 0 y = y 0 x y} z := x 11
else {x = x 0 y = y 0 x<y} z:= y fi, sedan och slutligen var x, y, z : num {x = x 0 y = y 0 } if x y then {x = x 0 y = y 0 x y} z := x {x = x 0 y = y 0 x y z = x} else {x = x 0 y = y 0 x<y} z:= y {x = x 0 y = y 0 x<y z=y} fi, var x, y, z : num {x = x 0 y = y 0 } if x y then {x = x 0 y = y 0 x y} z := x {x = x 0 y = y 0 x y z = x} else {x = x 0 y = y 0 x<y} z:= y {x = x 0 y = y 0 x<y z=y} fi {x=x 0 y=y 0 z=max(x 0,y 0 )} För att programmet skall bli rätt annoterat måste varje annotation kunna motiveras klart och tydligt. I det här fallet har vi gjort ett ganska stort tankehopp i det sista steget mer om det i nästa avsnitt. Programannotering kan också användas för att visa att (och varför) ett program inte fungerar. Om någon tror att vi kan få x och y att byta värden genom att utföra x := y ; y := x så kan följande annoteringar (med lämplig muntlig förklaring) fungera som motargument: 12
var x, y : num {x =3 y=5} x:= y ; {x =5 y=5} y:= x {x =5 y=5} Om man vill visa att ett program inte fungerar räcker det med att ge ett motexempel. Här har vi valt startvärden godtyckligt (3 för x och5för y) och sedan fyllt i de följande annotationerna. Slutannotationen visar att x och y inte bytte värden. Annoterad programtext kan göras mer lättläst om man utnyttjar hypertextens möjligheter. 2 2.5 Automatisk annotering Givet en startannotation kan annotationer beräknas automatiskt för program som inte innehåller loopar. Följande regler är tillräckliga: (a) En regel för skip: {p} skip {p} Denna regel skall tolkas så att om vi har en annotation {p} före en skip-sats så kan vi fylla i samma annotation efter skip. (b) En regel för tilldelning: {p} x := e { x 0 p[x := x 0 ] x = e[x := x 0 ]} där p[x := x 0 ] betyder p med x 0 substituerat för x. (c) En regel för början av en if-sats: {p} if b then {b p} S else { b p} S fi (d) En regel för slutet av en if-sats: if b then S {p} else S {p } fi {p p } 2 På http://www.abo.fi/~jockum.wright/0102/progmet/outlines/annot1.html visas ett exempel på blädderbar annotering. 13
Man kan använda dessa regler mekaniskt men då måste man ofta förenkla de nya annotationerna, eftersom man annars får uttryck som blir alltför komplicerade för att kunna förstås. Följande exempel illustrerar detta: {x =1 y=z+1} x:= x + y +1 { x 0 x 0 =1 y=z+1 x=x 0 +y+1} Den senare annotationen kan här förenklas till x = y + 2 y = z + 1 (se uppgifterna). Ett annat exempel är den if-sats som vi såg på tidigare: var x, y, z : num {x = x 0 y = y 0 } if x y then z := x {x = x 0 y = y 0 x y z = x} else z := y {x = x 0 y = y 0 x<y z=y} fi {x=x 0 y=y 0 z=max(x 0,y 0 )} Den sista annotationen (efter fi) får vi enligt regeln ovan som disjunktionen av de två annotationerna i if- och else-grenarna. Den är alltså egentligen (x = x 0 y = y 0 x y z = x) (x = x 0 y = y 0 x<y z=y) men detta kan förenklas till x = x 0 y = y 0 z = max(x 0,y 0 ). 2.6 Bakåtannotering Det går bra att annotera små programsnuttar men för större programtexter blir det opraktiskt. Dels är regeln för tilldelningar komplicerad, och annotationerna blir stora och ohanterliga. Men ännu viktigare är att det svårt att veta vilken information i en annotation som är viktig. Ett program är alltid skrivet för ett visst ändamål, så det är naturligt att utgå ifrån vad som borde vara sant i slutet av programmet och sedan annotera programmet bakåt för att se vad som skall gälla i början för att målet skall nås (eller om det är omöjligt). Reglerna för bakåtannotering liknar reglerna för framåtannotering. (a) En regel för skip: {q} skip {q} 14
(b) En regel för tilldelning: {q[x := e]} x := e {q} (c) En regel för slutet av en if-sats: if b then S {q} else S {q} fi {q} (d) En regel för början av en if-sats: {(b q) ( b q )} if b then {q} S else {q } S fi Alla dessa regler skall tolkas så att om annotationen q (och q ) efteråt är given så kan vi lägga till de andra annotationerna. Antag som exempel att vi vill bekräfta att följande programfragment byter värden på x och y: var x, y, z : num z := x ; x := y ; y := z Vi låter x 0 och y 0 som vanligt stå för startvärdena på x och y och startar med den önskade slutannotationen {x = y 0 y = x 0 }.Dåvianvänder reglerna ovan får vi följande samling annoteringar {y = y 0 x = x 0 } z := x ; {y = y 0 z = x 0 } x := y ; {x = y 0 z = x 0 } y := z {x = y 0 y = x 0 } vilket visar att programmet fungerar som det skall. I stället för att arbeta med planlösa framåtannotationer ( nu skall vi se vad som händer i det här programmet ) så visar vi att programmet utträttar precis det som det är avsett att göra. Annotationerna blir alltså enklare (tex behöver vi inte veta något om startvärdet på z eftersom slutannotationen inte använder detta startvärde). 2.7 Kommentarer och annotationer Man kan undra om annotationer egentligen bara är ett slags formaliserade kommentarer. Är det inte lika bra att skriva tydliga kommentarer i textform i stället? En viktig skillnad är att annotationer tvingar programmeraren att göra en statisk tillståndsbeskrivning som är ett verkligt komplement till den dynamiska beskrivning som den egentliga programtexten ger. Kommentarer av typen s initialiseras eller arraysumman lagras i s är däremot egentligen bara en omskrivning (eller tolkning) av det som redan står i programtexten. 15
Reglerna för beräkning av bakåtannoteringar är i själva verket regler för att beräkna det svagaste förvillkoret (weakest precondition) och dessa regler kan användas som en definition på vad programsatserna egentligen betyder, dvs som en programsemantik (se överkursavsnittet 12). Övningar 1. Antag att σ är ett tillstånd där x har värdet 0, y har värdet 2, b har värdet F och a är en array med de fem värdena 5, 2, 4, 6, 9. Avgör om följande tillståndspredikat är sanna eller falska i σ: (a) x y b. (b) a[x] a[y]. (c) ( i <5 a[i] x). (c) ( i <5 a[i]=y). (d) b ( i <4 a[i]=a[i+ 1]). 2. I exemplen har följande förenklingar gjorts. Motivera dem: (a) (x y z = x) (x <y z=y) har förenklats till z = max(x, y). (b) ( x 0 x 0 =1 x=x 0 +y+ 1) har förenklats till x =1+y+1. (c) x = x 0 y = y 0 z = max(x, y) har förenklats till x = x 0 y = y 0 z = max(x 0,y 0 ). 3. Antag följande variabeldeklaration: var x, y, z : num ; b : Bool ; a : array[20] of num Skriv tillståndspredikat som motsvarar följande påståenden: (a) z är större än både x och y. (b) b y och falskt annars. (c) Alla värden i arrayn a (dvs hela a[0..19]) är nollor. (d) Arrayavsnittet a[0..9] är ett palindrom (dvs samma oberoende om det läses från vänster till höger eller från höger till vänster) (e) Värdet på x förekommer i arrayn a. (f) Alla tal i arrayn a är olika. (g) Arrayn a är strikt sorterad (dvs den är sorterad och alla tal är olika). 4. Antag följande variabeldeklaration: var x, y, z : num ; f : Bool ; a, b : array[n] of num Uttryck följande tillståndspredikat på naturligt språk (dvs på så vardaglig svenska som möjligt): 16
(a) x<z z<y. (b) b ( z : num 1 <z<x/xmod z 0). (c) ( i <n a[i] = 0). (d) (x y z = x) (y x z = y) (e) ( i j i<20 j<20 a[i] =b[j]. 5. Ersätt frågetecknen med ett lämpliga tillståndspredikat (så starkt som möjligt) i följande annoterade programfragment: var x, y, s : num {x =2 y=5 s=0} s:= s + x ; {? } s := s + y ; {? } 6. Annotera följande program: var x, y, z : num {x =3 y=5} z:= x ; x := y ; y := z där startannotationen är given. 7. Ersätt frågetecknet med ett lämpligt tillståndspredikat: var x, y : num {x =1 y=3} if x +2 y 1then x := y else y := x fi {? } 8. Annotera följande program: var x, y, z : num z := x ; if y x then z := y else skip fi 17
Använd {x = x 0 y = y 0 } som startannotation. 9. Annotera följande program: var x, y : num x := x + y ; y := x y ; x := x y Använd {x = x 0 y = y 0 } som startannotation. 10. Bakåtannotera programmet i föregående uppgift (använd resultatet från uppgiften för att avgöra vilken den önskade slutannotationen är). 11. Bakåtannotera programfragmentet som användes i exemplet som lagrar det större av talen x och y i z. 3 Annotering av loopar För enkla, rätlinjiga program (eng. straight-line programs) kan annotationer alltid beräknas automatiskt (även om detta lätt ger oläsbara uttryck som måste förenklas innan en människa kan försöka förstå dem). Så fort ett program innehåller loopar blir situationen den omvända. För att annotera loopar måste vi använda vår kunskap och kreativitet. 3.1 Loopinvarianter En speciellt viktig typ av annotation är en loopinvariant. Avsikten med en loopinvariant är att att ange ett tillståndspredikat som är sant före (och efter) varje varv i loopen. Samtidigt skall invarianten visa att när loopen exekverats till slut så har man nått det önskade slutmålet. Tag som exempel följande programfragment som skall summera talen i en array a[0..n 1]: var s, i : num ; a : array[n] of num s := 0 ; i := 0 ; do i<n s:= s + a[i];i:= i +1 od Här ändrar både i och s värde för varje varv i loopen, men de ändrar på ett sätt som bibehåller ett slags balans: s är alltid summan av de i första elementen i arrayn a. Alltså är s = sum(a[0..i 1]) 18
en invariant för loopen. Dessutom finns det annan information som är sann hela tiden, nämligen att i n (så vilägger till detta i invarianten). Vi kan då annotera programmet med invarianten: var s, i : num ; a : array[n] of num s := 0 ; i := 0 ; {i n s = sum(a[0..i 1])} do i<n {i n s=sum(a[0..i 1])} s := s + a[i];i:= i +1 {i n s=sum(a[0..i 1])} od men för att visa att det uttryckligen handlar om en loopinvariant (och för att inte behöva skriva den tre gånger) använder vi hellre följande skrivsätt: var s, i : num ; a : array[n] of num s := 0 ; i := 0 ; do i<n {invariant i n s = sum(a[0..i 1])} s := s + a[i];i:= i +1 od dvs vi visar invarianten genast i början av loopen genom en annotation med nyckelordet invariant. 3.2 Varianter Utöver en invariant vill man dessutom visa att loopen faktiskt terminerar. Detta görs enklast genom att man visar en variant (ibland kallad termineringsargument), dvs ett uttryck vars värde (ett naturligt tal) minskar för varje varv i loopen. I exemplet ovan växer i hela tiden och kommer närmare n, sån iär en lämplig variant. Vi kan då införa både invarianten och varianten som annotationer: var s, i : num ; a : array[n] of num {unchanged a} s := 0 ; i := 0 ; {s =0 i=0} do i<n {invariant i n s = sum(a[0..i 1])} {variant n i} s := s + a[i];i:= i +1 od 19
Märk att vi har lagt till en annotation i början av programmet som säger att arrayvariabeln a inte ändras. Vi skriver unchanged a ibörjan i stället för att behöva införa ett namn (tex a 0 ) och sedan i varje annotation skriva... a = a 0. Annotationen visar att vi kan uppfatta a som en konstant när vi analyserar detta program. 3.3 Annotation efter loopen En invariant och en variant är inga självändamål. Varianten visar att loopen inte är oändlig, och invarianten förklarar hur loopen arbetar. Dessutom ger invarianten oss en annotation efter loopen, genom att vi där vet att invarianten är sann men vakten (dvs i<n) är falsk. I vårt exempel blir slutannotationen (i <n) i n s=sum(a[0..i 1]) vilket lätt förenklas till i = n s = sum(a[0..n 1]). Det slutliga annoterade programfragmentet blir alltså följande: var s, i : num ; a : array[n] of num {unchanged a} s := 0 ; i := 0 ; {s =0 i=0} do i<n {invariant i n s = sum(a[0..i 1])} {variant n i} s := s + a[i];i:= i +1 od {i = n s = sum(a[0..n 1])} 3.4 Att upptäcka loopinvarianter Det finns inga enkla regler för att avgöra en invariant (eller den bästa invarianten) för en given loop. I allmänhet måste man ha en klar bild av vad loopen skall uträtta, helst i form av ett förvillkor och en slutannotation. Betrakta följande loop: var i, x : num ; a : array[n] of num i := 0 ; do i<n a[i] x i:= i +1 od För att kunna hitta en invariant måste vi dels ha klart för oss vad vi antar att gäller i början, dels måste vi veta vad loopen uträttar (söker efter värdet x i arrayn a[0..n 1] 20
och sätter i till n om x inte finns där). Vi kan sammanfatta detta genom att fylla i startoch slutannotationer för loopen: var i, x : num ; a : array[n] of num {unchanged a, x, n} i := 0 ; {i =0} do i<n a[i] x i:= i +1 od {(i <n a[i]=x) (i=n ( k<n a[k] x)} Slutannotationen är i det här fallet rätt komplicerad, men den är vanligen den bästa startpunkten då vi vill upptäcka en invariant: slutannotationen skall ju påstå att invarianten är sann men loopens vakt är falsk. Sålänge vakten i<n a[i] xär sann kan endast en del av slutannotationen vara sann, nämligen att x inte finns i a[0..i 1]. Då vi tillägger att i hålls mellan 0 och n får vi en invariant: i n ( k <i a[k] x) Vi skall senare se hur man kan kontrollera en invariant, här nöjer vi oss med informellt bekräfta den. Ett annat exempel är följande loop, som färdigt har start- och slutannotationer. Den beräknar x! (x fakultet) och lagrar resultatet i r, samtidigt som x räknas ned till 0. {x = x 0 r =1} while x>0do r := r x ; x := x 1 od {x =0 r=x 0!} Också i detta fall vår vi tips till en invariant från slutannotationen: då loopen inte ännu är avslutad har x inte ännu räknat ned till 0 och r innehåller inte ännu hela resultatet. Ett skrivbordstest kan bekräfta att följande gäller x x 0 r = x 0 (x 0 1)... (x+1) vilket är en invariant. Den kan också skrivas x x 0 r x! =x 0!. 3.5 Klassinvarianter En klassinvariant i ett objektorienterat system liknar på många sätt en loopinvariant. Skapande av ett objekt (med en konstruktor) motsvarar att komma till den punkt i programmet där loopen börjar medan ett metodanrop motsvarar ett varv i loopen. Klassinvarianten måste vara sann i början (dvs efter att konstruktorn utförts) och sedan måste 21
den bevaras av varje metod (dvs om den är sann före ett metodanrop så är den också sann efter anropet). Klassinvarianten kan alltså uppfattas som en annotation av klassen, och man kan övertyga sig om att en metod faktiskt bevarar invarianten genom att annotera metoden så som beskrivits ovan. Vi skall senare se (i avsnitt 5) att klassinvarianter kan bekräftas mera formellt (bevisas) på samma sätt som loopinvarianter. Övningar 1. Följande program beräknar en summa med användning av addition och subtraktion med 1(såkallade inc- och dec-operationer): var x, y : num {x = x 0 y = y 0 } do x>0 x:= x 1,y := y +1 od Annotera programmet, dvs bestäm invariant, variant och slutannotation. 2. Följande program beräknar x y och lagrar resultatet i r: var x, y, r : num {x >0} r:= 1 ; do y>0 y:= y 1,r := x r od Annotera programmet. 3. Följande program avgör om värdet x förekommer i arrayn a[0..n 1] eller inte. var x, i : num ; f : Bool ; a : array[n] of num f := F ; i := 0 ; do i<n if a[i] =xthen f := T ; i := n else i := i +1 fi od Bestäm invarianten och varianten. Vilken är slutannotationen? 22
4. Annotera följande program, som lagrar heltalskvadratroten av y i x: var x, y : Nat x := 0 ; do (x +1) (x+1) y x:= x +1 od 5. Följande program söker efter den största äkta faktorn i talet x (vi antar att värdet på x är 2 eller större). var x, z, r : num ; {x 2} r := 1 ; z := 2 do z<x if (x div z) z = x then r := z ; z := z +1 else z := z +1 fi od Bestäm invarianten och varianten. Vilken är slutannotationen? 6. Antag att arrayn a[0..n 1] (där vi antar n>0) är sorterad och vi vet att det finns ett index k sådant att a[k] =x. Vi vill nu söka efter x i a så att vi hela tiden vet att det sökta indexet k uppfyller i k<jdär i och j är indexvariabler. Eftersom arrayn är sorterad kan vi använda binär sökning, dvs vid starten är i =0 och j = n och sedan skall avståndet mellan i och j halveras för varje varv i loopen. Målet är alltså att skriva ett program med följande variabeldeklaration: var x, i, j : int ; a : array[n] of int och programmet skall inte ändra på a eller x. Märk att k inte hör till programmet, utan bara till förklaringen! (a) (b) (c) Uttryck invarianten och varianten i ord. Skriv programfragmentet och förklara varför invarianten faktiskt bevaras och varianten faktiskt minskas. Skriv programfragmentet med fullständiga annotationer. 23
7. Vi vill beskriva en klass Key som modellerar de skåpnycklar som delas ut i tex en simhall. Nycklarna är numrerade från 0 till n-1 och systemet håller reda på vilka nycklar som är ute (variabeln taken : set of num) men också vilken som är nästa nyckel att ges ut (variabeln next : num). Man önskar att nyckarna utdelas i en obestämd ordning (dvs enligt något system som bestäms senare). Ange en invariant för klassen, dvs ett booleskt uttryck som talar om taken och next och som alltid skall vara sant. 4 Specifikationer Annotationer är ett sätt att använda tillståndspredikat för att beskriva ett program. Ett annat sätt är att ange önskade egenskaper hos ett pogram som inte ännu skrivits. En sådan beskrivning av ett program kallas en specifikation. 4.1 För- och eftervillkor Ett förvillkor (eng. precondition) är ett tillståndspredikat som beskriver de tillåtna starttillstånden för ett programfragment. Om det handlar om en procedur kan vi uppfatta förvillkoret som det krav vi har rätt att ställa på inparametrarna när vi skriver procedurens programkod. Till exempel kan förvillkoret för en procedur som tar två inparametrar x och y (av typ num) och beräknar x y vara att x inte är noll och att y inte är negativt: x 0 y 0 Ett eftervillkor (eng. postcondition) är ett tillståndspredikat som beskriver de tillåtna sluttillstånden. I praktiken vill vi kunna tala om startvärden i eftervillkoret, och för detta finns det olika standardkonventioner: (a) (b) startvärden indexeras med en nolla (så att startvärdet för x heter x 0 medan slutvärdet heter x) slutvärden anges med primtecken (så att startvärdet för x heter x medan slutvärdet heter x ) Vi använder här regeln att eftervillkoret faktiskt är ett tillståndspredikat, och om vi vill namnge startvärden så visar vi det uttryckligt i förvillkoret (och vi använder vanligen noll-indexering). En specifikation för ett programfragment som beräknar x y och lagrar resultatet i r kan då seutsåhär: var x, y, r : num pre x = x 0 y = y 0 x 0 y 0 post r = x y 0 0 Märk att denna specifikation tillåter att värdena på x och y ändras. 24
4.2 Exempel på specifikationer Ett annat exempel är följande specifikation: var x, y : num pre x = x 0 x 0 0 post y 2 x 0 < (y +1) 2 Det kan krävas en stunds eftertanke för att man skall bli övertygad om att den säger att y skall bli heltalskvadratroten av x. Följande exempel säger att summan av arrayn a[0..n 1] skall beräknas och lagras i s: var s : num ; a : array[n] of num pre true post s = sum(a[0..n 1]) Här har vi inte definierat begreppet sum, utan vi antar att det har en intuitiv betydelse som är tillräckligt exakt. Ifall vi vill ange att arrayn a inte får förstöras (ändras) då summan beräknas, kan vi skriva var s : num ; a : array[n] of num pre a = a 0 post a = a 0 s = sum(a[0..n 1]) eller så kan vi införa en skild rad som anger vilka variabler som inte får ändras: var s : num ; a : array[n] of num unchanged a pre true post s = sum(a[0..n 1]) 4.3 Informella och formella specifikationer De exempel på specifikationer som getts ovan är formella, dvs de använder matematisklogisk notation för att uppnå en exakthet som gör att specifikationens betydelse är absolut klar och otvetydig. I praktiken är det ofta svårt att åstadkomma fullständigt formella specifikationer. Ofta kan det tyckas att det är mycket enklare att specificera informellt. Tag som exempel följande informella specifikation: sök efter värdet x i arrayn a[0..n 1]. En formell specifikation blir lätt komplicerad, men samtidigt visar den att problemet inte är riktigt så enkelt som vi tänkt oss, och den tvingar oss att formulera allting exakt: hur skall resultatet av sökningen lagras, vad skall vi göra om x inte hittas, får x ändra värde 25
under sökningen,...? Vi kan tex ha en skild variabel som säger om x hittades eller inte: var x, i : num ; f : Bool ; a : array[n] of num unchanged x, a pre true post (f ( k <n a[k]=x)) (f i n a[i] =x) men vi kan också tänka oss att vi låter i stå för all information: var x, i : num ; a : array[n] of num unchanged x, a pre true post (i <n a[i]=x) (i=n ( k<n a[k] x)) En halvformell specifikation innehåller in blandning av formella och informella delar. En informell del kan vara text i vardagsspråk, men om möjligt lönar det sig att försöka skriva också den informella delen med matematisk notation, men med begrepp som inte definierats exakt. Om vi tex vill specificera att arrayn a[0..n 1] skall sorteras kan vi beskriva det så här: var a : array[n] of num pre a = a 0 post sorted(a[0..n 1]) permutation(a[0..n 1],a 0 [0..n 1]) där begreppet sorted är definierat exakt men begreppet permutation kan beskrivas informellt så att permutation(a[0..n 1],b[0..n 1]) betyder att a[0..n 1] och b[0..n 1] innehåller samma element men de får komma i olika ordning. Då kan vi senare införa en exakt definition för permutation utan att själva specifikationen behöver ändras. 4.4 Specifikationsspråk Den notation som använts ovan för att skriva specifikationer (med var, unchanged, pre och post, och med allmänna principer för hur tillståndspredikat skall skrivas) kan ses som ett enkelt specifikationsspråk. För att kunna specificera stora program och system måste vi kunna beskriva delar (moduler) var för sig och sedan ha olika sätt att kombinera delarna till större helheter. Det finns en mängd olika formella specifikationsspråk, för olika tillämpningar (tex Z, VDM, B). Till endel språk finns dessutom verktygsstöd för verifiering, kodgenerering, mm. Övningar 1. Uttryck följande specifikationer med hjälp av för- och eftervillkor. Ge alltid variabeldeklaration och ange vid behov vilka variabler som inte får ändras. Uppfinn vid behov nya begrepp men förklara dem då exakt (gärna med en formell definition). 26
(a) Differensen (icke-negativ) mellan x och y skall lagras i z, så att tex om x är 7 och y är 3 skall z bli 4 men om x är2ochyär 5 skall z bli 3. (b) Variabeln z : num skall få ett värde som ligger mellan värdena på x och y. (c) Avgör om arrayn a[0..n 1] är ett palindrom eller inte och lagra svaret i den logiska variabeln f. (d) Avgör om arrayerna a[0..n 1] och b[0..n 1] innehåller samma värden i samma ordning och lagra svaret i den logiska variabeln f. (e) Avgör hur många gånger talet x förekommer i arrayn a[0..n 1] och lagra svaret i den variabeln n. 2. Tolka följande specifikationer, dvs förklara i ord vad de säger: (a) var z : num ; A : set of num unchanged A pre A post z A ( x x A x z) (b) var a : array[n] of num pre a = a 0 post a[0] = a 0 [n 1] ( i 1 i<n a[i]=a 0 [i 1]) Kom ihåg att set betyder mängd, och det är alltså tillåtet att använda vanliga mängdoperationer, som (tillhör), (delmängd), mm. 3. Skriv specifikationer med för- och eftervillkor som motsvarar följande (välj själv variabelnamn och -typer): (a) (b) Beräkna kvadratroten av ett givet reellt tal (som inte får vara negativt), med en noggrannhet på 0.01. Förvandla ett givet heltal till binär form (dvs en array av nollor och ettor) 4. Vi vill beskriva en klass Key som modellerar de skåpnycklar som delas ut i tex en simhall. Nycklarna är numrerade från 0 till n-1 och systemet håller reda på vilka nycklar som är ute (variabeln taken : set of num) men också vilken som är nästa nyckel att ges ut (variabeln next : num). Man önskar att nyckarna utdelas i en obestämd ordning (dvs enligt något system som bestäms senare). Skriv specifikationer med för- och eftervillkor för (a) (b) (c) konstruktorn, som ger det starttillstånd där all nycklar är inne, metoden handout där en nyckel ges ut till en kund (och en ny nyckel väljs ut att vara nästa ), och metoden return där en nyckel (variabeln inkey) returneras. 27
5. Skriv specifikationer med för- och eftervillkor som motsvarar följande: (a) (b) (c) Efteråt skall x innehålla det större och y det mindre av de ursprungliga värdena på x och y. Vänd om (reverse) arrayn a[0..n 1], så att det element som var först kommer sist, osv. En kortlek modelleras med en array[52] of int (och varje spelkort kodas som ett heltal 0..51 på något sätt som vi inte behöver bry oss om). Specificera en blandning, så att inget kort efter blandningen ligger bredvid samma kort som det låg bredvid före blandningen. 6. Uttryck följande specifikationer med hjälp av för- och eftervillkor. Välj själv datarepresentation, dvs de variabler som används för att representera den information som specifikationen handlar om. (a) Avgör om en given array är sorterad eller inte. (b) Förvandla ett givet naturligt tal till binär form (dvs som en följd av nollor och ettor) (c) Uppdela ett givet tal i primfaktorer (d) Sätt in ett givet värde på rätt plats i en sorterad array (så att resten skuffas högerut ). 7. I ett datasystem vill man ha en uppsättning nycklar (ID-koder), som utgörs av femsiffriga tal. (a) (b) Vi tänker oss att de redan använda nycklarna bildar en mängd A : set of num. Specificera operationen Ge en ny (oanvänd) nyckel med för- och eftervillkor. I en implementation håller vi de använda nycklarna i en array (och antalet använda nycklar i en skild variabel). Specificera operationen Ge en ny (oanvänd) nyckel i detta fall. 8. Vi modellerar en telefonbok som en mängd av par av formen (namn,nummer): var c : set of (string, num) Vi vill nu specificera ett antal procedurer som arbetar med en sådan telefonbok. (a) En procedur specificeras enligt följande: var n : string ; f : Bool ; c : set of (string, num) unchanged n, c pre true post f =( x (n, x) c) Förklara i ord vad denna specifikation betyder. 28
(b) (c) Skriv specifikationen för en procedur som hämtar numret för en given person. Förvillkoret skall säga att namnet faktiskt finns i telefonboken. Vi vill göra en begränsning som telefonboken måste uppfylla hela tiden: samma person får inte ha fler än ett telefonnummer. Uttryck detta som ett tillståndspredikat. 9. Vi kan implementera telefonboken i föregående uppgift med hjälp av två arrays och en variabel som säger hur många namn som finns i telefonboken: var n : num ; a : array of string ; b : array of num; med tanken att namnet a[i] hör ihop med numret b[i] (för 0 i<n). Vi vill nu implementera proceduren som hämtar numret för en given person (se b-fallet i föregående uppgift). (a) (b) (c) Specificera denna procedur med för- och eftervillkor. Skriv en implementation och annotera den. Uttryck begränsningen att samma person får inte ha fler än ett telefonnummer som ett tillståndspredikat. 5 Riktighetsbevis Om vi har en specifikation med förvillkor p och eftervillkor q och en(påstådd) implementering S inställer sig osökt frågan hur vi kan kontrollera att S faktiskt implementerar den givna specifikationen. Vi skall nu beskriva ett system för att bevisa detta. Den metod som vi beskriver kallas ibland Hoare-logik, efter C.A.R. Hoare som uppfann idén i slutet av 1960-talet. 5.1 Riktighetspåståenden Vi använder skrivsättet p { S } q för att säga att programmet S är (totalt) riktigt med avseende på förvillkor p och eftervillkor q. I vardagstermer betyder detta att om programmet S exekveras i ett starttillstånd där predikatet p är sant så kommer exekveringen att terminera i ett sluttillstånd där predikatet q är sant. Programsatsen S sägs då åstadkomma (eng. establish) eftervillkoret q. Eller annorlunda uttryckt: programmet S är en godtagbar implementering av specifikationen som anger förvillkoret till p och eftervillkoret till q. Vi talar här endast om total riktighet (eng. total correctness). Många böcker behandlar partiell riktighet, vilket innebär att man visar att om exekveringen terminerar så är eftervillkoret q sant i sluttillståndet. Vid total riktighet kräver man mera; man måste visa att exekveringen säkert terminerar och att eftervillkoret blir uppfyllt. Skillnaden mellan dessa två angreppssätt blir stor när man behandlar tex loopar och rekursiva procedurer. Läsaren skall speciellt se upp med att liknande notation (tex {p} S {q}) kan användas både för total och för partiell riktighet. 29
5.2 Riktighetsregler Systemet för att bevisa riktighet byggs upp så att det finns regler för att bryta ned ett riktighetspåstående som handlar om ett komplicerat program till riktighetspåståenden som handlar om programmets delar. Genom att tillämpa dessa regler upprepade gånger kommer vi ned till den mest grundläggande nivån, tex tilldelningar för vilka det sedan finns skilda regler. Som enkelt exempel kan vi ta programkonstruktionen skip. Den gör ingenting, dvs den lämnar alla programvariabler oförändrade. Det betyder att om p är ett villkor som gäller innan skip exekveras så gäller p också efter exekveringen. Detta betyder också att vad som helst som följer logiskt av p kommer att gälla efter exekveringen. Riktighetsregeln för skip är följande: p q p { skip } q Detta är en inferensregel, dvs en regel som säger hur vi kan dra slutsatser om riktighetspåståenden. Liksom de andra riktighetsregler som ges senare används den på följande sätt. Anta att vi skall visa att riktighetspåståendet y = 2{ skip } y > 1är sant. Regeln kan då användas så att p motsvarar (eng. match) uttrycket y = 2och q motsvarar uttrycket y > 1. Alltså är riktighetspåståendet sant om bara det logiska påståendet y =2 y>1är sant (och det är det, för om y är2såär y större än 1). Ett alternativt sätt att skriva riktighetsregeln för skip vore följande: OM SÅ p q p { skip } q eller baklänges : Om du vill visa Såräcker det med att visa p { skip } q p q Vi följer här det först givna formatet som stämmer överens med normalt skrivsätt för inferensregler inom logik. 5.3 Tilldelningssatsen Tilldelningssatsen är den mest grundläggande satskonstruktionen i imperativ programmering. Dess riktighetsregel är p q[x := e] p { x := e } q där q[x := e] betyder q med e substituerat för x (ibland skrivs detta q[e/x] eller q[x\e] eller q e x). Denna regel motiveras bäst med exempel. Vi kan tex visa det uppenbara riktighetspåståendet x =2{ x := x +1 } x = 3. Regeln säger att vi skall visa x =2 x+1=3(märk att 30
högra sidan är resultatet då man substituerar x +1 för x i x = 3). Själva beviset är en enkel härledning: x =2 x+1=3 {ekvationsaritmetik} x =2 x=2 {implikation är reflexiv} T Riktighetsregeln för tilldelningar duger utan ändringar också för multipla tilldelningar, om vi uppfattar x som en vektor av (olika) variabler och e som en vektor (av samma längd som x) av uttryck: p q[x := e] p { x := e } q Samtidigt måste vi uppfatta q[x := e] som en parallel substitution, dvs variablerna x ersätts med uttrycken e samtidigt. Till exempel förvandlas riktighetspåståendet x<y{ x, y := y +1,x 1 } x>y med hjälp av tilldelningsregeln till x<y y+1>x 1 vilket sedan lätt visas vara sant. 5.4 Sekventiell komposition Sekventiell komposition av två programsatser S 1 ; S 2 innebär att S 1 exekveras först och därefter S 2. Riktighetsregeln är följande: p { S 1 } q q { S 2 } r p { S 1 ; S 2 } r dvs om vi vill visa ett riktighetspåstående av typen p { S 1 ; S 2 } r så skall vi hitta ett sådant predikat q att både p { S 1 } q och q { S 2 } r gäller. Predikatet q kallas det mellanliggande predikatet (eng. intermediate predicate) och beskriver det mellanliggande tillståndet som exekveringen av S 1 leder till. Exempel: För att visa riktighetspåståendet x =1 y=2{ x := y ; y := x } x =2 y=2 väljer vi x = 2 y = 2 som mellanliggande predikat. Riktighetsregeln för sekventiell komposition säger då att vi skall visa de två riktighetspåståendena x =1 y=2{ x := y } x =2 y=2 x=2 y=2{ y := x } x =2 y=2 31