Föreläsning 2: Underprogram En betydande del vid programmering är att kunna dela upp den uppgift man skall lösa i mindre delar. Varje del kan sen ses som en ny uppgift och denna kan eventuellt i sin tur delas upp i mindre delar. För att strukturera upp sin programkod och för att öka läsbarheten samt att slippa skriva samma kod på flera ställen kan man nyttja det vi kallar underprogram (sen gammalt subrutiner). Vi har redan använt oss av färdiga underprogram och vi börjar med att titta på hur detta har fungerat. Vi ser på följande programexempel. with Ada.Integer_Text_IO; use Ada.Integer_Text_IO; procedure Xxx is A, B : Integer; ”Anrop” av underprogrammet begin ”Get” i Ada.Integer_Text_IO. Get(A); Vi återvänder då Get Get(B); programmet har kört klart. Put(A); Put(B, Width => 3); Swap(A, B); Put(A); end Xxx; Det vi gör när vi anropar "Get" i utförandedelen av vårt program är att vi egentligen säger till datorn att hoppa till ett underprogram som heter "Get" och utföra det som står där och sen komma tillbaka till oss för att utföra nästa sak vi har att göra. Det underprogram vi anropar skulle kunna se ut som programmet nedan (nu är detta underprogram antagligen inte skrivet i Ada, men vi antar detta för ett ögonblick för att exemplifiera). procedure Get____________________ is Här står ev. deklarationer som "Get" behöver. begin Här står det kod som utför "Get":s uppgift. Hämta data från tangentbord och lagra det i den variabel som vi vill lägga datat i. end Get; Som detta program ser ut är det ju precis som vilket program som helst i Ada. Det som dock är en skillnad (som inte är ditskrivet än) är att detta program skall leverera data tillbaka till vårt huvudprogram (observera att det inte är intressant att skicka in något till Get). Hur gör man då detta? Jo, vi använder oss av något som kallas parametrar/argument och dessa skriver vi inom parenteser efter namnet på underprogrammet. procedure Get(Item : ___ Integer) is Här står ev. deklarationer som "Get" behöver. begin Här står det kod som utför "Get":s uppgift. Hämta data från tangentbord och lagra det i den variabel som vi vill lägga datat i. Försök till att skriva kod som gör detta. Item := "Data från tangentbordet"; end Get; Dessa parametrar (i detta fall bara en) utgör gränssnittet mellan vårt huvudprogram och underprogrammet. Parametrarna skall överensstämma i antal med de parametrar vi anropar med (se i huvudprogrammet "Get(A)") och dessutom skall datatyperna överensstämma för att det skall vara ok ("A : Integer" i HP och "Item : Integer" i parameterlistan). Underprogrammets parametrar behöver inte ha samma namn som HP:s parametrar, men det är tillåtet. I vårt exempel har vi olika namn för att visa detta, men det hade gått lika bra att skriva: procedure Get(A : ___ Integer) is Om vi hade varit tvunga att ha samma namn på den aktuella och den formella parametern. Då hade vi inte kunna anropa samma "Get" i steg 2 i HP (där det står "Get(B)"). Detta hade inte varit bra. Vi vill ju kunna lagra data i olika variabler vid olika tillfällen och detta är anledningen till att namnen ej behöver vara samma på de formella och aktuella parametrarna. Några definitioner av termer inom detta. De parametrar vi har i HP när vi anropar ett underprogram kallas för "aktuella parametrar". De parametrar som står i parameterlistan i underprogrammet kallas för "formella parametrar". Man kan komma ihåg detta genom att underprogrammet egentligen är en formell beskrivning av hur man utför en uppgift (vi har de formella parameternamnen) och i det aktuella fallet vi vill göra något (i HP) anropar vi med de aktuella parametrarna. För att återgå till underprogramsexemplet tittar vi på tilldelningen inuti "Get". Där står "Item := ...". Observera att vi i definitionen av detta underprogram använder oss av den formella parametern för att utföra de operationer vi vill göra. Hur kommer då vår aktuella variabel "A" att få detta värde? Jo, det löser vi i Ada på det sättet att vi i listan av parametrar talar om att vi vill, när vi kommer till "end Get", skicka ut värdet som "Item" har till det anropande programmet. Detta gör vi genom att placera ett "out" efter parametern. procedure Get(Item : out Integer) is Observera att vi kan ändra på "Item" ända fram tills dess att vi kommer till "end Get" och det är det värde som "Item" har på slutet som "A" kommer att få. Vi tittar på nästa del i HP. "Put(A)" innebär ju på samma sätt som tidigare att vi anropar ett underprogram, men i detta fall vill vi ju inte få data tillbaka från underprogrammet utan istället vill vi skicka data till underprogrammet. Detta visar vi genom att, i underprogrammets parameterlista, skriva "in" efter parametern. procedure Put(Item : in Integer) is begin Kod som utför detta ... end Put; Många tycker att det känns lite konstigt att Put (som skriver ut data på skärmen) har "in" i sin parameterlista och att Get (som läser in data från tangentbordet) har "out" i sin parameterlista. Det är viktigt att förstå varför detta ser ut som det gör. Man skall alltid se underprogrammen (och huvudprogrammet) som den enhet som är den viktigaste just när man håller på med den. När jag skriver min definition av "Get" är jag ju intresserad av att skicka ut(!) data till den som anropar mig och när jag skriver min definition av "Put" är jag intresserad av att få in(!) data från den som anropar mig för att jag skall kunna utföra mitt jobb. Det är samma sak när vi säger att vi läser in data från tangentbordet så menar vi alltså att programmet läser data från tangentbordsbufferten och inte att vi som användare gör det (vi skriver ju på tangentbordet). Vi återgår till vårt exempel och funderar lite på det här med att vi kan skriva "Put(B, Width => 3)" i HP och på detta sätt styra hur utskriften kommer att se ut. Detta skulle väl innebära att vi måste ha ett till underprogram som har två parametrar om vi nu måste ha samma antal parametrar i HP och i underprogramsdefinitionen. Svaret är ja, men ändå nej. Det finns nämligen en möjlighet att ha fler parametrar i definitionen av underprogrammet än man har i anropet från HP. Dock måste dessa extra parametrar ha ett standardvärde (skönsvärde, eng. default value) om det är så att den som skriver HP inte vill skicka med data till dessa. procedure Put(Item : in Integer; Width : in Natural := 10) is begin ... end Put; Observera att vi måste separera de olika parametrarna med ett skiljetecken. I HP använder vi "," för att visa att vi har flera parametrar och i definitionen av underprogrammet använder vi ";". I detta fall har den andra parametern ett standardvärde som används om vi inte anropar med mer än en parameter (t.ex. "Get(A)" i HP). Om vi anropar med två parametrar i HP ("Get(B, Width => 3)") kommer andra parametern istället att få det aktuella värdet. Observera att vi i HP har skrivit "Put(B, Width => 3)". Ordet "Width" är det exakta namnet på den andra parametern. Om den andra parametern hade hetat något annat skulle ha behövt skriva detta istället i HP.Det vi har gjort är att vi har angivit att just den parametern skall få värdet 3 i anropet. Vi skulle kunna skriva anropet i HP på följande olika sätt också. Put(B, 3); Put(Item => B, Width => 3); Put(Width => 3, Item => B); Intressant variant! Det är dock inte tillåtet att skriva på följande sätt: Put(Item => B, 3); Variant 1. Put(3, Item => B); Variant 2. Variant 1 beroende på att man måste skriva de formella namnen på alla parametrar på resten om det är så att man har påbörjat detta och variant 2 beroende på att parametern "Item" kommer att få värdet 3 eftersom det står först. Om man inte anger namnet på den formella parametern gäller alltså ordningen. Vad gäller alltså för parametrar? En kort sammanfattning: 1) Antalet aktuella och formella parametrar skall vara lika (undantag finns om det finns avslutande formella parametrar med standardvärden). 2) Typerna på de formella och aktuella parametrarna måste stämma överens. 3) Ordningen på parametrarna är viktig (undantag om man använder sig av de formella parametrarnas namn). 4) Man separerar de formella parametrarna med ';' och de aktuella parametrarna med ','. 5) Om ett underprogram inte skall ta emot eller lämna ifrån sig några data skall man inte ens ha parenteser efter underprogramnamnet i anropet. Exempel: begin Skriv_Text; end ...; De exempel vi hittills har använt oss av är underprogram som redan existerar. De finns ju i paketet "Ada.Integer_Text_IO". Nu kan man ju själv skapa underprogram som gör olika saker. Ett exempel på detta är det anropet "Swap(A, B)" som står i HP. I detta fall vill vi att värdena i "A" och "B" skall byta plats. Hur löser vi detta? Ja, till att börja med vill vi ju skicka data från HP till "Swap" ("A" och "B"), men sen vill vi ju också få tillbaka de nya värdena så att de hamnar i samma variabler. Detta leder till att vi varken kan ha "in" eller "out" i definitionen av parameterlistan utan att vi måste ha både "in" och "out". procedure Swap(A, B : in out Integer) is C : Integer := A; begin A := B; B := C; end Swap; Själva algoritmen är ju känd från lektionen med grundläggande satser. Den egna uppgiften. Det man kan poängtera är att man redan i deklarationsdelen kan initiera variablers värden. Dessutom kan man se att det är tillåtet att ha flera parametrar med ',' mellan om de har exakt samma typ och kommunikationsriktning "in"/"out"/"in out". Varför står inte C tillsammans med A och B här? Det är för att A och B är parametrar, d.v.s de skall kommuniceras med huvudprogrammet. C är ju bara något som vi behöver internt inne i Swap för att lösa problemet där. Alltså deklareras C mellan procedure och begin, precis som en vanlig variabel, och blir då en lokal variabel inuti Swap. Vi skulle kunna ha löst samma problem med fyra parametrar också, men det är i detta fall onödigt. Det man skulle kunna titta på är att vi har samma namn på de formella och aktuella parametrarna. Det är ju tillåtet och i detta fall kändes det kanske ganska naturligt att bara ha ett par enkla namn på de formella parametrarna. Ett par varianter på anrop från HP: begin ... Swap(A => A, B => B); Swap(B => A, A => B); Vad händer här? Swap(A, B => B); ... end ...; När man anropar en procedur som har "out"-parametrar (eller "in out") är det viktigt att veta att man här kommer att få tillbaka ett värde som skall påverka den aktuella parametern. Det är alltså på det viset att en formell "out"-parameter kräver att den aktuella parametern är en variabel och inte en konstant. Kompilatorn kommer att klaga om man försöker göra något sådant (jämförelse med Fortran 77 där man kunde ändra en konstants värde på detta sätt (illa)). Det vi nu sagt om parameterlistor är generellt och gäller för alla underprogram i Ada. Det som är lite speciellt är att det finns två olika sorters underprogram. Den ena är procedurer (det vi pratat om tills nu) och den andra är funktioner. Dessa skall vi ta och gå igenom nu. Funktioner har ni kommit i kontakt med i matematiken och man kan säga att ursprunget till att man ville ha funktioner även i programspråk var att man ville efterlikna matematiken. Vi har i matematiken t.ex. sinusfunktionen som är användbar. Vi tar denna som exempel även om denna också finns i ett av Ada:s standardbibliotek. with Ada.Float_Text_IO; use Ada.Float_Text_IO; procedure Xxx is X, Y : Float; begin Get(X); Y := Sin(X); Put(Y, Fore => 1, Aft => 3, Exp => 0); end Xxx; Nu har vi inte tagit med Ada:s standardbibliotek där "Sin" finns definierad. Detta innebär att vi själva måste definiera denna funktion. Att det är en funktion ser vi i anropet i HP. Ett anrop till ett underprogram som står som en egen sats är alltid ett proceduranrop. Ett anrop till ett underprogram där anropet inte står som en egen sats är däremot alltid ett anrop till en funktion. I detta fall ser vi att anropet står som uttrycket i en tilldelningssats och ett uttryck får ju inte vara en egen sats. Alltså är det ett funktionsanrop. Vi tittar på hur funktionen definieras. function Sin(X : in Float) ____________ is begin end Sin; Att parametern skall ha "in" beror ju på att vi tar emot data från HP, d.v.s. vi får in data till funktionen. Detta kan man dessutom utveckla lite till och se att funktioner alltid skall ta emot data och i Ada är man bestämt att funktioner endast(!) får ha "in"-parametrar. En liten detalj som man inte får glömma är dessutom att en "in"-parameter anses vara en konstant i underprogrammet! Det som skiljer en procedur från en funktion vid definitionen är att en funktion alltid skall returnera ett värde. Det är nämligen så att funktioner alltid skall anropas i ett uttryck (t.ex. i en tilldelningssats eller ett villkor i en "if-sats") och där funktionsanropet står vill man ju att det skall bli ett värde som sen kan användas till något. I HP ovan ser man t.ex. att "Y := Sin(X)" borde bli något i stil med "Y := [värde]", d.v.s. funktionsanropet ersätts av det returnerade värdet från funktionen. Hur skriver vi då detta i vår definition av funktionen. function Sin(X : in Float) return Float is begin Här beräknas "resultatet". return "resultatet"; end Sin; Det vi får göra är att till att börja med tala om att vår funktion kommer att returnera ett värde av en viss typ. Vi skriver datatypen innan "is" så har vi markerat detta. Det andra vi måste göra är att någonstans mellan "begin" och "end" tala om att nu skall vi returnerna det resultat som skall ersätta funktionsanropet i det anropande programmet. Observera att det enligt Ada:s standard är så att man direkt vid "return [uttryck]" avslutar funktionen och att vi direkt kommer tillbaka till det anropande programmet. Det är tillåtet att skriva "return [uttryck]" på flera ställen mellan "begin" och "end", men så fort vi når ett av dessa avbryts alltså funktionen. Vi har nu infört en ny sats också "return-satsen". Det går att använda sig av en "return-sats" även i en procedur, men då utan värde. Då betyder det bara att proceduren skall avbrytas direkt och återhopp till den som anropar sker. Detta är inte så vanligt, men är alltså en tillåten sats i Ada. Man brukar säga att en funktion har en "ingång" (man anropar funktionen och får in data via parametrarna) och man brukar rekommendera att man bara har en "utgång" (att man bara har ett "return") och att denna utgång ligger sist i funktionen. Detta för att få så läsbar kod som möjligt. Hur skulle vi beräkna resultatet i funktionen ovan? Kanske med hjälp av en Taylor-utveckling eller något liknande. Det krävs antagligen någon form av iterationssats och kanske några extra variabler och lite tilldelningssatser. Vi hoppar över detta och koncentrerar oss på det som har med själva definitionen av underprogram att göra. Observera att returvärdet inte alls behöver vara av samma typ som parametrarna till funktionen. Det skulle lika gärna kunna vara så att vi skickar in en sträng till en funktion och får ut ett heltal (t.ex. om vi vill beräkna antalet tecken i stärngen). Det bara råkade vara så att vår sinusfunktion vill ha ett flyttal in och att resultatet också blir ett flyttal. När vi nu vet allt detta om parametrar och vilka regler som gäller för det kan vi ta upp mer detaljer kring detta med överlagring. Man kan ju ha flera underprogram som heter samma sak och trots detta håller kompilatorn reda på vilken rutin som skall användas. Detta beror alltså på att kompilatorn går igenom alla definitioner av underprogram och håller reda på vilket namn och vilka parametrar som gällde för respektive underprogram. Tack vara detta kan man alltså anropa en procedur eller funktion som heter samma sak som en annan och med hjälp av den eller de variabler man har som parametrar i anropet klarar kompilatorn av att hitta rätt. Det blir dock besvärligare (omöjligt) att skilja på två underprogram som har likadana parameterlistor (har inget med parameternamnen att göra) om det inte är en procedur och en funktion förstås. Kompilatorn klarar heller inte av att kontrollera vilken funktion som skall anropas om det bara är returtypen som skiljer sig åt. En fråga som kan vara intressant är: Var skall vi skriva dessa procedurer och funktioner? Svaret på detta är flerdelat. Det första och enkla alternativet är att definitionerna av underprogram skall stå i deklarationsdelen av ett program (eller underprogram då även dessa kan ha egna underprogram). D.v.s. där variablerna deklareras. En rekommendation är att man lägger definitionerna ovanför variablerna så slipper man problem med "felaktigt" nyttjande av variabler i underprogram. En regel som alltid gäller är att anrop av underprogram måste ske senare (längre ner) i programkoden än definitionen (eller deklarationen) av underprogrammet. Om ett underprogram anropar ett annat underprogram måste alltså det andra underprogrammet vara deklarerat antingen ovanför eller i deklarationsdelen i det första. Om man definierar ett underprogram inuti ett annat kommer man inte att kunna anropa det inre av dessa från något annat program/underprogram. Detta kallas "information hiding". Samma sak gäller för variabler. Om en variabel deklareras inuti ett underprogram kommer variabeln inte att finnas utanför detta. Vill man kunna anropa ett underprogram från flera andra ställen måste man lägga det "synligt" för alla dessa rutiner. När det gäller variabler skickar man dessa som parametrar till och från underprogram om man vill kunna utnyttja data som finns i andra delar av programmet. Man skall undvika att använda variabler som finns deklarerade i en annan "modul" om detta är möjligt. Man skall dessutom försöka "gömma" så mycket information som möjligt för utomstående (t.ex. HP) så att de slipper fundera på varför man har denna eller hur man löst den specifika uppgift underprogrammet skall lösa. Om man deklarerar en variabel ovenför ett underprogram är denna variabel också synlig inuti underprogrammet, men man bör alltså undvika att använda denna i underprogrammet då man kan råka ut för oönskade "sidoeffekter". Underprogram kan också läggas på separat fil. Observera att detta inte är något som är helt vanligt. Oftast gör man på andra sätt för att kunna återanvända programkod som på något sätt är generell. Antag att vi har en procedur som heter "Swap" som den vi redan tidigare har pratat om och att denna inte ligger i filen tillsamman med HP utan i en separat fil. Filnamnet på den separata filen skall då vara "swap.adb" för att kompilatorn skall finna proceduren senare. Vi tittar på hur denna fil ser ut till att börja med. procedure Swap(A, B : in out Integer) is C : Integer := A; begin A := B; B := C; end Swap; Vi ser att koden blir exakt som den var definierad sen förut. Inga paket behöver inkluderas då proceduren endast använder sig av grundläggande operationer som finns i språket. Om vi behövt inkludera några paket hade dessa behövt stå innan procedurhuvudet precis som i det vanliga programmet. Vi tittar på hur HP ser ut också. Filen där HP ligger heter som tidigare "xxx.adb". with Ada.Integer_Text_IO; use Ada.Integer_Text_IO; with Swap; procedure Xxx is Här var tidigare proceduren Swap placerad. A, B : Integer; begin Get(A); Get(B); Put(A); Put(B, Width => 3); Swap(A, B); Put(A); end Xxx; Det som är nytt i HP är att vi inte har kvar koden för Swap i deklarationsdelen av HP samt att vi har en ny "with" innan procedurhuvudet. Det är viktigt att se att det inte står något "use Swap" i koden. Man får nämligen inte göra "use" på underprogram som ligger på separata filer. Det är bara tillåtet att göra "use" på paket. När vi skrivit "with Swap" är det tillåtet att använda proceduren precis som om den var definierad i HP. Observera också att filerna "xxx.adb" och "swap.adb" skall ligga i samma bibliotek. Precis som tidigare är det så att filnamnet skall matcha procedur-/funktionsnamnet, men att det skall vara endast gemener (små bokstäver) i filnamnet. Det finns några saker som är viktiga när man gör underprogram som man skall lägga ut på separata filer. En är att man inte kan göra "use" i HP som, men det finns ett par saker till att tänka på. En av dessa är att man bara kan ha en(!) modul/enhet per fil. T.ex. ett HP eller ett underprogram (eller ett paket, men det återkommer vi till senare). Ska man ha flera separata underprogram så får man ha flera filer och göra "use" på var och en av dessa filer. En annan är att man inte kan flytta ut underprogram som är beroende av typer som inte kan ligga på den separata filen (t.ex. egendefinierade typer som vi återkommer till på nästa FÖ). Om man vill flytta ut sådana underprogram måste man skapa paket. Om vi nu går vidare finns det faktiskt ytterligare en form av underprogram, nämligen operatorer. Dessa är inte lika vanliga att man använder, men kan faktiskt komma till stor nytta lite då och då. Om vi t.ex. tittar på följande program: procedure Abc is I : Integer := 1; X : Float := 2.0; begin X := X + I; end Abc; I detta program är operatorn tecknet "+", och det som står på vänster och höger om sida om det är operanderna d.v.s I och X. Som ni säkert ser så går detta program inte att kompilera. I Ada finns operatorn "+" för två heltal, och "+" för två flyttal (dessa är alltså två separata underprogram), men det finns ingen operator "+" för ett heltal och ett flyttal och som returnerar ett flyttal. Varför detta inte finns kan man fråga sig. En enkel förklaring är att det finns mer datatyper än Integer och Float för att representera tal, och om man skulle definiera alla operatorer för att täcka upp alla kombinationer av alla datatyper skulle detta bli en herrans massa. Dessutom skulle det vara ganska onödigt, eftersom man lätt kan ordna detta genom att skriva så här: X := X + Float(I); Med detta så konverteras I till ett flyttal, och den operator som tar två flyttal som parametrar kan nu anropas. Man skulle kunna tänka sig detta också: X := Integer(X) + I; Det finns dock två problem med detta. För det första blir det nu typfel eftersom det till höger om tilldelningen står ett flyttal och till vänster blir ett heltal. För det andra så avrundas ju faktiskt X till närmsta heltal, och vi kanske förlorar information. Men om man nu verkligen vill skapa en operator som klarar av situationen i programmet Abc så kan vi givetvis skapa en sådan själva. Hur detta skall lösas har vi ju redan tänkt ut, allt vi behöver göra är att skriva koden: function "+"(Left : in Float; Right : in Integer) return Float is begin return Left + Float(Right); end "+"; Operatorer skrivs alltså alltid som funktioner fast namnet är endast ett tecken och skall omslutas med dubbla citationstecken. Det är faktiskt bara en begränsad mängd tecken som man kan göra detta med. Man kan t.ex. inte skapa en operator av tecknet "Q", tyvärr. Det andra som är lite speciellt är parametrarna. Här har jag själv döpt dem till Left och Right, de kan såklart heta precis vad som helst men namnen är lämpliga eftersom operatorer fungerar på det sättet att det som står till vänster om operatorn i anropet kommer överföras till den första parametern, och det som står till höger till den andra parametern. I övrigt beter sig operatorn som en funktion, d.v.s den måste göra return och kan bara ha "in" på sina parametrar. Övning på lektion Viktigt ang. trigonometrin i lab 2 Laborationens sista delmoment innehåller en del klurigt tänkande och det är viktigt att de förstår hur de skall tänka vad det gäller ett par saker (detta brukar ta ca 15 minuter, så se till att lämna plats för det). 1) Beräkningarna av längd, och de två vinklarna ger inte rätt resultat om man inte avrundar längden på vektorn innan man räknar ut vinklarna. Rätt i bemärkelsen att man bör kunna återskapa ursprungsvärdena skapligt om man går bakvägen. Det ger bättre resultat om man gör "rätt" och avrundar längden först. 2) Viklarna skall beräknas i rätt ordning. Be studenterna verkligen DELA UPP PROBLEMET. Försöker man ta allting på samma gång blir det för mycket att hantera. 3) Visualisera detta på LE om möjligt. Fysiskt koordinatsystem (i rätt riktningar) och hur allt ser ut. Ta hjärna bilden från laborationshandledningen som OH. 4) Poängtera att underprogrammet Create_Vector_Data har datatypen Integer på parametrarna! Detta är ett krav i laborationshandledningen. Ordningen på parametrarna är också viktigt att det är rätt. Givetvis är det lämpligt att direkt ovandla till flyttal lokalt inne i underprogrammet. 5) Tipsa om hur man "flyttar" vektorn till origo, d.v.s att man tar B-A. Tipsa om att skapa tre nya variabler C_X, C_Y, och C_Z som har dessa tre differenser. 6) Absolut sist i laborationen skall man flytta ut underprogrammet Create_Vector_Data till en egen fil. Tipsa om att inte göra denna del föränn de verkligen ser att underprogrammet fungerar som det skall. Det är viktigt att de tänker INNAN de programmerar och verkligen gör det troligt för sig själva att det är rätt tänkt. Det är MYCKET svårare att rätta till i efterhand (och finna felen om inte annat). Själva lektionen Uppgift 1: Läs in 5 tal, summera dessa och skriv sedan ut summan på skärmen. Lös detta rent med hjälp av underprogram. Låt gruppen hjälpa till att lösa detta problem och vara bara den som styr hela programmeringsarbetet. Låt olika personer hjälpa till och diskutera och ifrågasätt lösningen och varför man gör på detta sätt. I vilken ordning görs olika saker? Skriv ett fullständigt program. Repetera detta med var vi börjar och hur vi gör för att slippa så många fel som möjligt? Observera att vi från början inte behöver bry oss om vilka paket vi måste inkludera. Det är först när vi behöver ett paket som vi tar med det. Man kan här säga att man vill att de skall skriva ett program som utför allt detta, men att de bara har 3 rader på sig att förverkliga detta i HP. Börja med huvudprogrammet enligt alla konstens regler. :-) Vi får fram att HP inte behöver några "with". procedure Summa_Version_2 is A, B, C, D, E, Summan : Integer; begin Las_In(A, B, C, D, E); Summera(A, B, C, D, E, Summan); Skriv_Ut(Summan); end Summa; Detta är nog ungefär vad man kan tro att de kommer fram till om man låter dem iterera lite med vad variabler m.m. skall heta och vilka parametrar som skall skickas till och/eller från respektive procedur. Om det är någon som tycker att det borde vara en funktion istället för procedur vid summeringen kan man väl göra det så istället. Jag brukar iallafall ta den andra versionen efteråt för att se att det går att lösa på olika sätt (vi antar att de tyvker att det skall vara en procedur till att börja med så blir det en funktion senare). Låt någon välja vilken del som skall utföras först. Kanske summeringen eller inmatningen eller kanske utskriften av svaret. Det blir säkert "Las_In", men det spelar ingen roll egentligen som vi ju alla vet. :-) Här brukar jag skriva underprogrammen på separata tavlor. Detta för att kunna bolla lite med arnopsordning m.m., men det spelar ingen egentlig roll. De får hjälpa till och ta fram hur en procedur skall se ut och vilka parameternamn vi skall ha och om det skall skickas data "in" eller "out" eller kanske "in out" och vilka datatyper vi skall ha. Efter en stund har vi nog kommit fram till något i stil med: procedure Las_In(A, B, C, D, E : out Integer) is begin Put("Mata in 5 heltal: "); Get(A); Get(B); Get(C); Get(D); Get(E); end Las_In; Observera att det inte blir några extra "lokala" variabler utan att vi använder dem som deklareras i parameterlistan. Dessutom är det så att vi nu upptäcker att vi behöver inkludera två av de där standardpaketen ("Ada.Text_IO" och "Ada.Integer_Text_IO"). Beroende på hur vi löser det här med filuppdelning m.m. så skall dessa with placeras lite olika. Diskutera detta och kom fram till att vi lägger allt i ett program denna gång och att "with" och "use" då skall ligga före HP. Vi stretar vidare och tar "Summera"-proceduren. Här kommer vi fram till at det är fem parametrar som skall vara "in" och en som skall vara "out". Frågan om man inte skulle kunna göra "in out" på allihop kommer säkert och det är bara att motivera genom att säga att man skall visa för andra programmerare (som sen skall rätta er kod) vad man tänkt sig med de olika parametrarna och det är skäl nog för att inte göra "in out". Vi får något som kanske ser ut på följande sätt: procedure Summera(A, B, C, D, E : in Integer; Summan : out Integer) is begin Summan := A + B + C + D + E; Trivial, men ... end Summera; Vi kan ju se att utskriftsdelen inte blir svårare att göra så vi låter dem fundera på denna hemma i lugn och ro. Vi går vidare genom att modifiera summeringen till den andra versionen av underprogram (i mitt fall gör vi alltså om den till en funktion). Vad behöver ändras i HP för att det skall bli rätt? Här kan man t.ex. ta upp att man inte behöver variabeln "Summan" i HP därför att vi lika gärna kan skicka resultatet av beräkningen direkt till utskriften, men vi behöver kanske inte skriva om programmet så mycket. Viktigt är dock att man inte kan anropa en funktion så som en procedur! Vi måste alltså skriva om satsen lite (det blir en tilldelning kommer de säkert på). Vad händer med underprogrammet då. Jag brukar skriva om hela underprogrammet så att de får "öva"/"repetera" det där med "in"/"out"/"in out", typer m.m. en gång till. Det är ju trots allt parameteröverföring som är en av de saker som de flesta nybörjarstudenter har problem med. Ta gärna lite extra diskussion kring detta om det behövs. Här kommer det vi brukar komma fram till efter en stunds skrivande och diskuterande. function Summera(A, B, C, D, E : in Integer) return Integer is Summan : Integer; begin Summan := A + B + C + D + E; return Summan; end Summera; Det är intressant att avsiktligt skriva fel ("funktion" istället för "function") och se om de ser det eller om man senare kan säga att det finns ett kompileringsfel och att detta leder till många följdfel. :-) Det är bra om man får ovanstående då man kan diskutera lite om lokala variabler (varför de inte skall deklareras i HP m.m.) och att man ofta behöver lokala variabler i underprogram för att klara av att lösa sin uppgift. Man kan ju ändra lite för att visa att man i detta fall dessutom kan tilldela variabeln sitt värde redan i deklarationsdelen i underprogrammet och till sist att man inte ens behöver någon extra variabel utan kan returnera uttrycket direkt i "return-satsen". Till sist jämför vi proceduren och funktionen och ser att det som skiljer är att proceduren returnerar värdet (summan) som parameter (i anropet måste alltså HP ha en variabel som tar emot värdet i parameterlistan) och att funktionen returnerar sitt värde via "return-satsen" (detta leder till att man i HP måste ha anropet i ett uttryck, t.ex. i en tilldelning). I funktionshuvudet har vi dessutom ett "return Datatyp" som anger vad det är för typ av resultat. Tag en diskussion om var vi placerar underprogrammen om detta inte kommit spontant under uppgiftens gång. Uppgift 2: Läs in 5 positiva tal (större än noll), summera dessa och skriv ut summan på skärmen. Här ser vi ju att det är samma uppgift som tidigare, men där inmatningen skall modifieras. Vi försöker resonera oss fram till att det skulle vara bra om vi hade en "Get" som internt kontrollerade att det var ett positivt tal och gav eventuella felmeddelanden m.m. På detta sätt slipper vi ju ändra något alls i vårt gamla program. Jag brukar dock rekommendera att vi döper vår egen "Get" till något lite annat för att slippa fundera på vad som händer annars. Vi kan välja "Get_Safe" eller något likande. Vad skall då "Get_Safe" göra? Ja, det är ju att läsa in ett(!) heltal och att upprepa inmatningen om det var ett felaktigt tal till det att korrekt data matats in. Kanske skriva ut något felmeddelande också om man vill det. Vi skriver kanske följande med hjälp av studenterna: procedure Get_Safe(Item : out Integer) is begin loop Get(Item); exit when Item > 0; Put_Line("FEL! ...."); end loop; end Get_Safe; Skriver man fel igen ("procedur" istället för "procedure") blir det också väldigt många följdfel vid kompileringen. :-) Om man tittar strikt på detta så är det faktiskt ett fel som smugit sig in i koden. Det är inte alltid kompilatorer ger ifrån sig felmeddelanden om detta, men kan ni se vad som är lite tveksamt? Fundera på ovanstående innan ni läser vidare. Det kan vara en bra övning för er själva för att se hur studenternas situation är. :-) Egentligen är det så att en "out"-variabel inte får användas på annat sätt än att tilldelas värden inuti ett underprogram. Detta i analogi med att en "in"-variabel inte får ändra värde. Intressant och faktiskt gällde denna regel tidigare i Ada, men man har släppt lite på detta och jag tror att det faktiskt är ok att använda "out"-variabler på detta sätt numer. Jag brukar rekommendera att man använder sig av en extra lokal variabel i detta fall så slipper man bry sig om vilket som gäller. Nu börjar det bli intressant med placeringen av procedurerna och funktionerna för nu har vi ett underprogram som anropar ett annat. Här finns det lite olika alternativ igen. Antingen lägger vi "Get_Safe" inuti "Las_In" eller så ovanför. Helt beroende på hur vi vill få åtkomst från de andra programmen i fortsättningen. När vi nu kommit så här långt kan man diskutera en del kring det här med att det faktiskt går att deklarera variabler i HP och använda dem i underprogrammen. Räckvidd är ett begrepp som kan tas upp här. Rita gärna figurer som visar detta för att förtydliga. Jag brukar rekommendera mina studenter till att definiera sina variabler efter definitionerna av underprogrammen så slipper man problem med oönskade "sidoeffekter" och man tvingar sig att använda parameteröverföring. Det kommer säkert att vara några som tyckar att det är bra med "globala" variabler och det är ju sant i vissa fall. T.ex. om man har STORA data som skall skickas till underprogrammet. Man bör nog ta upp lite om det här med att ett anrop till ett underprogram faktiskt tar tid beroende på att data skall kopieras och att man gör ett hopp i programkoden (man behöver ju spara undan återhoppsadresser m.m.). Om man använder globala data i ett underprogram skall detta dokumenteras (kommenteras) mycket noggrant i koden. Vi återgår till underprogram igen och funderar på hur vi kan göra proceduren "Get_Safe" lite mer generell. Antag att man vill ha en sådan rutin lite då och då i olika program och att det kanske inte alltid är så att man vill ha inmatningar som är positiva utan kanske inom ett intervall istället. Hur gör vi? Studenterna kommer ofta med förslag som är bra här. T.ex. att man skickar med ett par extra parametrar "Min" och "Max" till proceduren och det kan vi ju enkelt lägga in i vårt program. procedure Get_Safe(Item : out Integer; Min, Max : in Integer) is Temp : Integer; begin loop Get(Temp); exit when (Temp >= Min) and (Temp <= Max); Put_Line("FEL! ..."); end loop; Item := Temp; end Get_Safe; Ännu bättre för att öva vidare är ju att lägga villkoret som avbryter "loop" i en funktion så får vi dessutom in att en funktion inte behöver returnera bara tal. function Ok(Item, Min, Max : in Integer) return Boolean is begin if (Temp >= Min) and (Temp <= Max) then return True; else return False; end if; end Ok; Observera att detta kan leda till kompileringsvarningar p.g.a. att vi inte har något "return" efter "ifsatsen". Hur fixar vi detta? Alternativ 1: function Ok(Item, Min, Max : in Integer) return Boolean is begin if (Temp >= Min) and (Temp <= Max) then return True; end if; return False; end Ok; Alternativ 2: function Ok(Item, Min, Max : in Integer) return Boolean is begin return (Temp >= Min) and (Temp <= Max); end Ok; Man kan också tänka sig en lokal variabel av typen "Boolean" för att lösa samma problem. I vissa fall kanske den snyggaste lösningen. Efter detta har man nog fyllt hela lektionen. Det brukar ta en del tid att lösa problem tillsammans med studenterna.
© Copyright 2024