Föreläsning 2: Underprogram

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.