Sammanfattning av Ada

1. Introduktion

Denna sammanfattning riktar sig till dig som behöver fräscha upp dina kunskaper i Ada, men inte vill plöja igenom en stor och tjock bok som innehåller onödigt mycket information. Dokumentet är i princip en utökad lathund med väl valda och kommenterade exempel. Möjligen kan sammanfattningen vara nyttig även för folk som vill lära sig Ada från början och som har ett annat språk i bakhuvudet.

Denna sammanfattning är till sin natur kortfattad och tar inte upp allt som finns i Ada, utan bara så mycket som brukar ingå i kurser på IDA (Institutionen för datavetenskap). Du kan välja att läsa alla avsnitt i en följd, eller leta upp svar på konkreta problem med hjälp av innehållsförteckningen nedan.


2. Kompilera och köra

Innan vi börjar gå in på hur Ada fungerar ska vi diskutera några praktiska detaljer som vad källkodsfilerna ska heta och hur man kompilerar och kör dem.

hello.adb
with Ada.Text_IO;
use Ada.Text_IO;

procedure Hello is

begin
  Put_Line("Hello world!");
end Hello;
Exempel 1: Ett enkelt program

Exemplen i det här dokumentet kommer se ut som exempel 1 ovan. Överst står namnet på filen och därunder följer innehållet (dvs själva programmet). Från det här exemplet kan vi lära oss följande saker:

su4-7 <25> gnatmake hello.adb
gcc -c hello.adb
gnatbind -x hello.ali
gnatlink hello.ali
su4-7 <26> hello
Hello world!
su4-7 <27> 
Exempel 1b: Kompilera och köra program

Exempel 1b ovan visar hur du kompilerar och kör dina program när du sitter vid en arbetsstation på IDA. Du kompilerar Ada-källkoden i ett skalfönster med hjälp av kommandot gnatmake. Om allt går som det ska kommer du att se tre rader som talar om vad kompilatorn gör. Därefter kan du köra programmet enligt ovan.

Om du får felmeddelanden som talar om att kompilatorn inte hittas måste du installera den med kommandot module add prog/gnat.

Mer information om installationen av Ada på IDA finns på Tommy Olssons sida Ada på IDA.


3. Programstruktur och paket

exempel2.adb
with Ada.Text_IO, Ada.Integer_Text_IO;
use Ada.Text_IO, Ada.Integer_Text_IO;

procedure Exempel2 is

  A, B, C : Integer;

begin
  -- Programmet summerar två tal
  Put("Mata in ett tal: ");
  Get(A);
  Put("Ett till: ");
  Get(B);
  C := A+B;
  Put("Summan blev ");
  Put(C);
  New_Line;
end Exempel2;
Exempel 2: Summera två tal

Förhoppningsvis inser du att programmet i exempel 2 ovan kommer läsa in två tal från användaren, summera dessa och skriva ut resultatet på skärmen. Strukturen på Ada-programmet är från början till slut följande:

3.1. Använda paket med with och use

De flesta av funktionerna i exempel 3 finns i olika paket. Den enda rad i huvudprocedurens kropp som klarar sig utan paket är raden C := A+B;. Övriga rader är beroende av de paket som inkluderas på de två första raderna. Vad är då skillnaden mellan with och use?

Paketet Ada.Text_IO innehåller funktioner och procedurer för att läsa och skriva text. I exempel 3 är procedurerna som finns på huvudprocedurens rad 1, 3, 6 och 8 hämtade från Ada.Text_IO. Paketet Ada.Integer_Text_IO används för att läsa och skriva heltal. Raderna med Get samt raden Put(C); anropar procedurer i det paketet.

3.2. Överlagring

Titta på de två sista Put-satserna i exempel 3. De kommer från olika paket men har samma namn. Detta fenomen kallas överlagring och innebär att två olika procedurer har samma namn. Vi kan skilja dem åt dels genom att de ligger i olika paket, dels genom att deras argument är av olika typ. Först kommer en Put-sats som skriver ut en textsträng, därefter en som skriver ut ett heltal. (Vi kan alltså inte skriva ut både ledtexten och summan med samma Put-sats.)

3.3. Underprogram

Ofta vill man dela upp sitt program i flera olika procedurer och funktioner. Oavsett hur man gör måste det finnas en huvudprocedure med samma namn som filen. Underprogram deklareras på samma ställe som variabler m.m., dvs efter raden procedure ... is men innan raden begin.

Underprogrammen följer samma struktur som huvudprogrammet och kan i sin tur innehålla deklarationer av variabler och underprogram. Underprogram behöver inte bara vara procedurer utan kan också vara funktioner, dvs i princip procedurer som returnerar ett värde. Här följer två exempel på förstarader från underprogram:

procedure Proc(A : in Integer; B : out Integer; C : in out Integer) is ...

function Func(X : in Integer) return Integer is ...

Med hjälp av markeringarna in, out och in out anges dataflödesriktningen för argumentet. Det innebär helt enkelt att argumentet A endast är indata till Proc och inte får byta värde. B är endast utdata från Proc och dess initialvärde är odefinierat. Vid anropet till Proc måste det dessutom stå en variabel på andra positionen så att utdatat kan lagras någonstans. C är både in- och utdata.

Exempel på underprogram finns bland annat i exempel 8 nedan.

3.4. Underprogram i separata filer

Om man vill kan man lägga underprogrammen i separata filer, men då finns det några regler man måste komma ihåg. Precis som för huvudprogrammet måste filens namn överensstämma med procedurnamnet, dock med undantaget att filnamnet måste bestå av enbart små bokstäver. Vidare måste man i varje underprogramsfil ange with och use, eftersom varje fil kompileras för sig. Slutligen inkluderar man underprogrammen i huvudprogrammet genom att göra with (naturligtvis utan .adb på slutet), dock inte use.


4. Programflödeskontroll

Med det långa ordet programflödeskontroll menar vi olika sätt att få programmet att göra olika saker. När programmet i exempel 3 körs kommer de olika satserna i källkoden att exekveras (köras) en i taget tills programmet är slut. Ibland vill man dock se till att körningen av programmet tar en annan väg. Detta sker i princip på två olika sätt: repetition eller selektion. Att repetera innebär förstås att man gör någonting flera gånger. Att selektera innebär att välja ut en av flera möjliga saker att göra. Vi ska nedan gå igenom dessa två tekniker och hur de ser ut i Ada.

4.1. Repetition med loop-satser

exempel3.adb
with Ada.Integer_Text_IO;
use Ada.Integer_Text_IO;

procedure Exempel3 is

   J, K : Integer;

begin

   for I in 1..5 loop
      Put(I);
   end loop;

   J := 1;
   while (J <= 5) loop
      Put(J);
      J := J+1;
   end loop;

   K := 1;
   loop
      exit when (K > 5);
      Put(K);
      K := K+1;
   end loop;

end Exempel3;
Exempel 3: Repetition med loop-satser

Det finns i princip tre olika sätt att upprepa saker och ting i Ada och dessa finns exemplifierade i exempel 3.

4.2. Selektion med if- och case-satser

exempel4.adb
with Ada.Text_IO, Ada.Integer_Text_IO;
use Ada.Text_IO, Ada.Integer_Text_IO;

procedure Exempel4 is

   Tal : Integer;

begin
   Put("Ange ett heltal: ");
   Get(Tal);
   if (Tal < 0) then
      Put_Line("Talet är negativt!");
   elsif (Tal = 0) then
      Put_Line("Talet är noll!");
   else
      Put_Line("Talet är positivt!");
   end if;
end Exempel4;
Exempel 4: Selektion med if

Den absolut vanligaste selektionssatsen är if-satsen. I exempel 4 testar vi om ett tal är negativt, noll eller positivt. En if-sats kan byggas ihop på många olika sätt, så länge den börjar med if och avslutas med end if. Delarna elsif och else är valfria.

exempel5.adb
with Ada.Text_IO, Ada.Integer_Text_IO;
use Ada.Text_IO, Ada.Integer_Text_IO;

procedure Exempel5 is

   Månad : Integer;

begin
   Put("Ange ett månadsnummer: ");
   Get(Månad);
   case Månad is
      when 4 | 6 | 9 | 11 =>
	 Put_Line("30 dagar");
      when 2 =>
	 Put_Line("Normalt 28 dagar");
      when 1 | 3 | 5 | 7 | 8 | 12 =>
	 Put_Line("31 dagar");
      when others =>
	 Put_Line("Det där var inget månadsnummer!");
   end case;
end Exempel5;
Exempel 5: Selektion med case

En bättre metod när man har många olika val är att använda sig av en case-sats.


5. Introduktion till typer

Ada har flera olika typer, men de fem viktigaste är följande:

exempel6.adb
with Ada.Text_IO, Ada.Float_Text_IO;
use Ada.Text_IO, Ada.Float_Text_IO;

procedure Exempel6 is

   I : Integer := 45;
   F : Float := 3.14;
   B : Boolean := True;
   C : Character := 'P';
   S : String := "elle";

   F2 : Float;

begin
   F2 := Float(I) + F + 5.0;

   if (B = True) then
      Put(F2);
      New_Line;
   end if;

   Put(C);
   Put_Line(S);
end Exempel6;
Exempel 6: Grundläggande typer

Exempel 6 visar hur man deklararer och initierar variabler av de fem viktigaste datatyperna. Dessutom visar vi hur man kan använda dem. Här kan vi lära oss följande:

su4-7 <22> exempel6
 5.31400E+01
Pelle
su4-7 <23>
Exempel 6b: Utmatning

Information om hur man kan justera utmatningen av flyttal finns i avsnitt 6.


6. Utmatning

utmatning.adb
with Ada.Text_IO, Ada.Integer_Text_IO, Ada.Float_Text_IO;
use Ada.Text_IO, Ada.Integer_Text_IO, Ada.Float_Text_IO;

procedure Utmatning is

   I : Integer := 45;
   F : Float := 3.14;

begin
   Put(I); New_Line;
   Put(I, Width => 3);  New_Line;
   Put(I, Width => 0);  New_Line;
   Put(F); New_Line;
   Put(F, Fore => 3, Aft => 2, Exp => 0); New_Line;
end Utmatning;
Exempel 7: Utmatning av tal

Exempel 7 visar olika sätt för att mata ut heltal och flyttal. Resultatet av detta illustreras i exempel 7b.

su4-7 <53> utmatning
         45
 45
45
 3.14000E+00
  3.14
su4-7 <54> 
Exempel 7b: Resultat av utmatning

Som vi ser av testkörningen i exempel 7b så kommer heltal att skrivas ut på ett ganska klumpigt sätt om vi inte anger något annat. Standardmässigt skrivs heltalen ut med exakt tio tecken där själva talet är högerjusterat om det skulle bestå av färre än tio siffor. Detta är i praktiken ganska oanvändbart och därför använder vi det sätt som anges på andra raden i exempel 7. Med hjälp av Width kan vi ange hur många tecken vi vill att utskriften ska använda. Genom att här ange 0 talar vi om att heltalet ska skrivas ut med exakt så många siffror som behövs.

När det gäller flyttal är det lite mer komplicerat. På fjärde raden i testkörningen ser vi att det standardmässiga sättet att skriva ut flyttal på är att ange det som ett tal i intervallet -9 till 9 samt med exponent. Om vi vill justera detta måste vi använda oss av Fore, Aft och Exp som anger hur många siffror som ska visas före respektive efter decimalpunkten, samt hur många siffror som ska användas för att visa exponenten. Om vi sätter Exp till 0 tvingar vi fram utskrift utan exponent.

I exempel 7 finns en New_Line-sats direkt efter varje Put. Detta är helt i sin ordning, ty i Ada kan man mycket väl ha flera satser på samma rad så länge de avslutas med semikolon.

Mer information om in- och utmatning av strängar finns i avsnitt 8.


7. Arrayer

En array (sv. fält) är en datastruktur som består av flera enheter av samma typ. Man kan jämföra med vektorer och matriser från matematiken. Ada har flera finesser som gör det lätt att använda arrayer.

7.1. Enkla arrayer

array_demo.adb
with Ada.Text_IO, Ada.Integer_Text_IO;
use Ada.Text_IO, Ada.Integer_Text_IO;

procedure Array_Demo is

   A : array(1..3) of Integer;

   type My_Array is array(1..5) of Integer;
   
   procedure Print_Array(A : in My_Array) is
   begin
      Put("Arrayen börjar vid: "); Put(A'First, Width => 0); New_Line;
      Put("Arrayen slutar vid: "); Put(A'Last, Width => 0); New_Line;
      Put("Arrayens längd är: "); Put(A'Length, Width => 0); New_Line;
      Put("Innehållet kommer här: ");
      for I in A'Range loop
	 Put(A(I), Width => 3);
      end loop;
   end Print_Array;

   B : My_Array := (14, 99, 16, 17, 18);

begin
   B(2) := 15;
   Print_Array(B);
end Array_Demo;
Exempel 8: Arrayer

I början av exempel 8 ovan ser vi en enkel deklaration av en array A. Den består av tre heltal som benämns med indexen 1, 2 och 3. Oftast är dock detta sätt att deklarera arrayer inte särskilt bra. Om vi t.ex. vill skicka arrayen till en procedure för vidare bearbetning måste vi först skapa en egen array-typ. Det gör vi på nästa rad.

Precis innan huvudprogrammet deklareras en array B av vår hemmagjorda array-typ My_Array. Här ser vi också ett sätt att placera data i arrayen. Ett annat sätt ser vi på första raden i huvudprogrammet. B(2) betecknar position 2 i arrayen B.

I underprogrammet Print_Array ser vi fyra olika attribut som är användbara när man arbetar med arrayer. Storleken på arrayen A (inte samma som i huvudprogrammet) kan härledas ur attributen:

7.2. Flerdimensionella arrayer

array_demo2.adb
with Ada.Text_IO, Ada.Integer_Text_IO;
use Ada.Text_IO, Ada.Integer_Text_IO;

procedure Array_Demo2 is

   type Row is array(1..3) of Integer;
   type Board_A is array(1..3) of Row;

   type Board_B is array(1..3,1..3) of Integer;

   A : Board_A;
   B : Board_B;

begin
   A(1)(1) := 2;
   B(1,1) := 2;
end Array_Demo2;
Exempel 9: Flerdimensionella arrayer

Det finns två sätt att åstadkomma flerdimensionella arrayer. I exempel 9 visar vi dessa två olika sätt genom att implementera spelplaner om 3 x 3 rutor för Tic-tac-toe.

Åtkomsten av enskilda positioner i arrayerna blir också olika beroende på vilket sätt man väljer. Med det första sättet måste vi skriva A(1)(1) medan vi för det andra sättet skriver B(1,1).

Varför vill man då göra på det första sättet med arrayer av arrrayer? Ibland kanske man vill behandla raderna på något speciellt sätt. Då kan det vara bra att kunna komma åt dem med en speciell typ.


8. Strängar

string_demo.adb
with Ada.Text_IO, Ada.Strings.Fixed;          
use Ada.Text_IO;

procedure String_Demo is

   S : String(1..10);
   L, P, I : Integer;

begin

   -- (a) Läsa in sträng
   Put("Skriv in en sträng med tio tecken: ");
   Get(S);
   Skip_Line;
   Put("Du matade in strängen: ");
   Put(S);
   New_Line;

   -- (b) Läsa in till del av sträng
   Put("Skriv in ett tal: ");
   Get_Line(S,L);
   Put("Du matade in strängen: ");
   Put(S(1..L));
   New_Line;

   -- (c) Hitta delsträngar och omvandla tal till strängar
   P := Ada.Strings.Fixed.Index(S,"1");
   Put_Line("Siffran 1 finns i position" & Integer'Image(P) & ".");

   -- (d) Omvandla strängar till tal
   I := Integer'Value(S(1..L));
   Put_Line("Talet är" & Integer'Image(I) & ".");

   -- (e) Plocka ut delar av strängar
   if L > 2 then
      Put_Line("Utan första tecknet blir det: " & S(2..L) & ".");
   end if;

   -- (f) Hur mycket som är deklarerat
   Put_Line("Strängen kan bli max" & Integer'Image(S'Last) & " tecken.");

end String_Demo;
Exempel 10: Stränghantering

Strängar i Ada är egentligen arrayer av tecken och kan behandlas precis som sådana. Det finns dock en hel del speciella saker man kan göra med just strängar. I exempel 10 visar vi de grundläggande saker som kan vara bra att kunna.

  1. Först försöker vi läsa in strängen med Get. När vi deklarerade strängen angav vi att den skulle vara tio tecken. Därför kommer Ada att läsa in tio tecken, varken mer eller mindre. Dessutom kommer Ada att strunta i att vi trycker Enter och vi måste därför "äta upp" radslutstecknet med Skip_Line. Matar vi in fler än tio tecken kommer dessa att kastas bort, vilket vi ser i exempel 10b.
  2. Ett mycket bättre sätt att läsa in strängar är med Get_Line. Då läser Ada in strängen tills vi trycker på Enter och returnerar sedan inte bara strängen utan också antalet tecken som lästes in. I exempel 10 sparar vi antalet inlästa tecken i variabeln L. När vi sedan ska skriva ut det vi nyss läste in anger vi att vi bara vill skriva ut de L första tecknen med notationen S(1..L). Detta är en så kallad array slice, dvs en del av fältet.
  3. För att hitta en delsträng i en sträng kan vi använda oss av Index som finns i paketet Ada.Strings.Fixed. Vi anger originalsträng och delsträng som vi vill söka efter. Index returnerar en siffra som talar om var delsträngen börjar. Här ser vi också hur man omvandlar ett tal till en sträng med Integer'Image() samt hur man kan konkatentera (slå ihop) strängar med operatorn &.
  4. För att omvandla strängar till tal använder vi Integer'Value().
  5. Som vi redan sett kan man plocka ut delsträngar.
  6. Vi kan få reda på hur stor strängen är genom att använda samma fyra attribut som finns för arrayer (se avsnitt 7).
su4-7 <18> string_demo
Skriv in en sträng med tio tecken: abcdefghijklmnop
Du matade in strängen: abcdefghij
Skriv in ett tal: 214
Du matade in strängen: 214
Siffran 1 finns i position 2.
Talet är 214.
Utan första tecknet blir det: 14.
Strängen kan bli max 10 tecken.
su4-7 <19> 
Exempel 10b: Utmatning från sträng-exemplet

9. Deklarera egna typer

I Ada kan du deklarera egna typer som liknar eller utökar de typer som redan finns.

type_test.adb
procedure Type_Test is

   type My_Number is new Integer;

   type Small_Number is range -127..128;

   type Weekday is (Mon, Tue, Wed, Thu, Fri, Sat, Sun);

   subtype Small_Integer is Integer range -127..128;

   I : Integer;
   MN : My_Number;
   SN : Small_Number;
   W : Weekday;
   SI : Small_Integer;

begin

   I := 14;
   MN := 2 + My_Number(I);
   SN := 2 + Small_Number(I);
   SI := 2 + I;

   W := Fri;

end Type_Test;
Exempel 11: Egna typer
record_test.adb
with Ada.Text_IO, Ada.Strings, Ada.Strings.Fixed;
use Ada.Text_IO;

procedure Record_Test is

   type Person is
      record
	 Name : String(1..20);
	 Birth_Year : Integer;
      end record;

   procedure Put(P : in Person) is
   begin
      Put(Ada.Strings.Fixed.Trim(P.Name, Ada.Strings.Right));
      Put(" (" & Integer'Image(P.Birth_Year) & " )");
   end Put;

   P : Person;

begin
   P.Name := "James Smith         ";
   P.Birth_Year := 1967;
   Put(P);
end Record_Test;
Exempel 12: Poststrukturer

I Ada kan man deklarera så kallade poster (eng. record). Dessa kan användas för att samla information som hör ihop, men inte nödvändigtvis är av samma typ. Ett typexempel på användningen av poster är personregister. I exempel 12 deklarerar vi en posttyp Person som innehåller namn och födelseår för en person. Dessutom skriver vi en egen Put-funktion för att kunna skriva ut posttypen. Detta funkar alldeles utmärkt, eftersom funktioner överlagras i Ada. De enskilda fälten i posttypen kommer vi åt med punkt-notationen, dvs vi anger postvariabeln följt av en punkt följt av ett fältnamn.

su4-7 <46> record_test
James Smith ( 1967 )
su4-7 <47>
Exempel 12b: Användning av poststrukturer

10. Felhantering med undantag

error1.adb
with Ada.Text_IO, Ada.Integer_Text_IO;
use Ada.Text_IO, Ada.Integer_Text_IO;

procedure Error1 is

   subtype Digit is Integer range 0..9;

   I : Integer;
   D : Digit;

begin
   Put("Skriv in en siffra: ");
   Get(I);
   D := I;
end Error1;
Exempel 13: Program utan undantagshantering

Programmet i exempel 13 ser ganska enkelt och oskyldigt ut, men saknar felhantering. En rad olika fel kan uppstå, särskilt kring inmatning. I körexemplet nedan ser vi att programmet fungerar utmärkt när användaren matar in förutsägbara data. När användaren matar in ett tal som ligger utanför intervallet 0 till 9 genereras ett undantag (eng. exception) och programmet avbryts. Samma sak händer om användaren matar in något som inte kan tolkas som ett tal.

su4-7 <58> error1
Skriv in en siffra: 5
su4-7 <59> error1
Skriv in en siffra: 17

raised CONSTRAINT_ERROR : error1.adb:14
su4-7 <60> error1
Skriv in en siffra: abcdef

raised ADA.IO_EXCEPTIONS.DATA_ERROR : a-tiinio.adb:91 
  instantiated at a-inteio.ads:20
su4-7 <61>
Exempel 13b: Undantag

Programmet i exempel 14 har utökats så att det kan fånga upp de undantag som genereras. Vi gör detta genom att lägga in en exception-sektion mellan begin och end. Där skriver vi ut ett lämpligt felmeddelande.

error2.adb
with Ada.Text_IO, Ada.Integer_Text_IO;
use Ada.Text_IO, Ada.Integer_Text_IO;

procedure Error2 is

   subtype Digit is Integer range 0..9;

   I : Integer;
   D : Digit;

begin
   Put("Skriv in en siffra: ");
   Get(I);
   D := I;
exception
   when Data_Error =>
      Put_Line("Du skrev inte in ett tal!");
   when Constraint_Error =>
      Put_Line("Talet var i fel intervall!");
   when others =>
      Put_Line("Ett okänd fel inträffade!");
end Error2;
Exempel 14: Uppfångande av undantag

su4-7 <62> error2
Skriv in en siffra: 17
Talet var i fel intervall!
su4-7 <63> error2
Skriv in en siffra: abc
Du skrev inte in ett tal!
su4-7 <64>
Exempel 14b: Körexempel för uppfångande

Att bara avbryta programmet och skriva ut ett meddelande är inte alltid det bästa sättet att hantera undantag. Om användaren råkade slinta på tangentbordet kanske han eller hon borde få försöka igen. Vi kan lägga en loop-konstruktion runt inmatningen, fånga upp felen och försöka igen tills ett accepterbart resultat har uppnåtts. En sådan konstruktion återfinns i exempel 15.

error3.adb
with Ada.Text_IO, Ada.Integer_Text_IO;
use Ada.Text_IO, Ada.Integer_Text_IO;

procedure Error3 is

   subtype Digit is Integer range 0..9;

   I : Integer;
   D : Digit;

begin
   loop
      begin
	 Put("Skriv in en siffra: ");
	 Get(I);
	 D := I;
	 exit;
      exception
	 when Data_Error =>
	    Put_Line("Du skrev inte in ett tal!");
	 when Constraint_Error =>
	    Put_Line("Talet var i fel intervall!");
	 when others =>
	    Put_Line("Ett okänd fel inträffade!");
      end;
      Skip_Line;
   end loop;
end Error3;
Exempel 15: Felhantering

I exempel 15 använder vi återigen Skip_Line. Detta gör vi för att tömma inläsningsbufferten. I annat fall skulle den gamla misslyckade inläsningen ligga kvar och tolkas en gång till, varvid en oändlig loop skulle uppstå.

su4-7 <69> error3
Skriv in en siffra: abc
Du skrev inte in ett tal!
Skriv in en siffra: 17
Talet var i fel intervall!
Skriv in en siffra: 5
su4-7 <70>
Exempel 15b: Körexempel på felhantering

Man kan också deklarera egna undantag i Ada. Detta sker i deklarationsdelen av en procedure och kan t.ex. se ut My_Exception : exception. Därefter kan man generera ett undantag med kommandot raise My_Exception; någonstans i koden.


11. Filhantering

Vad är en fil egentligen? En klassisk definition är att en fil är en sekvens av tecken. Vi talar ofta om textfiler och binärfiler. Skillnaden mellan dessa är rent akademisk, dvs det beror på hur vi väljer att betrakta dem. En viss fil (sekvens av tecken) kanske innehåller enbart läsbara tecken (bokstäver, siffror, etc) och har radbrytningar med lämpliga intervall. Då kan vi betrakta den som en textfil och behandla den därefter. Vi kan dock lika gärna betrakta samma sekvens av tecken som binärdata och behandla filen därefter.

Det finns två grundläggande sätt att bearbeta filer: sekventiellt eller med direktåtkomst. Det förra innebär att man läser eller skriver filen från början till slut utan att backa. Det senare innebär att man kan peka ut en position i filen och läsa eller skriva där. Vi ska enbart gå igenom sekventiell bearbetning här.

copy_file.adb
with Ada.Text_IO;   use Ada.Text_IO;

procedure Copy_File is

   Original : File_Type;
   Copy     : File_Type;
   Ch       : Character;

begin
   Open(Original, In_File, "A.TXT");
   Create(Copy, Out_File, "B.TXT");

   while not End_Of_File(Original) loop
      while not End_Of_Line(Original) loop
	 Get(Original, Ch);
	 Put(Copy, Ch);
      end loop;
      Skip_Line(Original);
      New_Line(Copy);
   end loop;

   Close(Original);
   Close(Copy);
end Copy_File;
Exempel 16: Kopiera en textfil

I exempel 16 kopierar vi en textfil. För att kunna hantera filer överhuvudtaget måste vi deklarera en eller flera filtyper. Dessa fungerar som ett slags handtag som håller reda på själva filen. Vi öppnar respektive skapar filer med Open och Create. Vi kan läsa och skriva med Get och Put precis som vanligt. Vi kan också testa om vi har kommit till slutet på en rad eller slutet på själva filen med End_Of_Line respektive End_Of_File.

Vi använder Skip_Line för att "äta upp" radslutstecknet och New_Line för att mata ut ett nytt. Detta gör vi eftersom radslut markeras på olika sätt på olika datorer. Vi hade eventuellt kunna behandla radslutsmarkeringarna som vanliga tecken, men sättet ovan är bättre.

Det finns tre olika sätt att öppna en fil och dessa anges som andra argument till Open eller Create. De tre sätten är: In_File, Out_File och Append_File. Man kan alltså läsa eller skriva till en fil, inte båda. Om man vill byta läge måste man stänga filen och öppna den igen. Append_File används om man vill lägga till data i slutet av filen.

copy_binary_file.adb
with Ada.Sequential_IO;

procedure Copy_Binary_File is

   package Integer_IO is
      new Ada.Sequential_IO(Integer);
   use Integer_IO;
   
   Original : File_Type;
   Copy     : File_Type;
   I        : Integer;

begin
   Open(Original, In_File, "A.BIN");
   Create(Copy, Out_File, "B.BIN");

   while not End_Of_File(Original) loop
      Read(Original, I);
      Write(Copy, I);
   end loop;

   Close(Original);
   Close(Copy);
end Copy_Binary_File;
Exempel 17: Kopiera en binärfil

I exempel 17 kopierar vi en binär fil, dvs vi betraktar filen som en sekvens av Integer. I exempel 16 med textfiler använde vi gamla vanliga Ada.Text_IO, men för binärfiler måste vi fixa ett eget paket. Det finns ett generisk paket Ada.Sequential_IO som vi kan använda, men då måste vi instansiera det först (dvs det generiska paketet är bara en mall och vi måste skapa ett verkligt paket).

För binärfiler använder vi Read och Write för att läsa och skriva. I övrigt fungerar det på samma sätt som för textfiler, bortsett från att vi inte kan testa om vi kommit till ett radslut.


12. Skapa egna paket

12.1. Enkla paket

I Ada kan man gruppera ihop funktioner, procedurer m.m. och lägga dem i särskilda paket. Vi har redan sett hur vi använder standardpaketen i Ada, men vi kan också göra egna paket. I exempel 18 återfinner vi ett exempel på ett paket för hantering av komplexa tal.

Ett paket definieras med två filer, en som innehåller paketets huvud (efternamn .ads) och en som innehåller paketets kropp (efternamn .adb).

komplex_paket.ads
package Komplex_Paket is

   type Komplex_Typ is private;

   Komplex_Fel : exception;

   procedure Get(K : out Komplex_Typ);
   procedure Put(K : in Komplex_Typ);
   function Absolut(K : in Komplex_Typ) return Float;
   function Mult(K1, K2 : in Komplex_Typ) return Komplex_Typ;
   
private

   type Komplex_Typ is record
      Re, Im : Float;
   end record;

end Komplex_Paket;
komplex_paket.adb
with Ada.Text_IO;                       use Ada.Text_IO;
with Ada.Float_Text_IO;                 use Ada.Float_Text_IO;
with Ada.Strings.Fixed;                 use Ada.Strings.Fixed;
with Ada.Numerics.Elementary_Functions; use Ada.Numerics.Elementary_Functions;

package body Komplex_Paket is

  procedure Get(K : out Komplex_Typ) is 

     Indata : String(1..25);
     Langd, Plus_Pos, I_Pos : Integer;
     Re, Im : Float;

  begin
     Get_Line(Indata, Langd);
     Plus_Pos := Index(Indata, "+");
     I_Pos := Index(Indata,"i");
     Re := Float'Value(Indata(1..Plus_Pos-1));
     Im := Float'Value(Indata(Plus_Pos+1..I_Pos-1));
     K.Re := Re;
     K.Im := Im;
  exception
     when Constraint_Error | Data_Error =>
	raise Komplex_Fel;
  end Get;

  procedure Put(K : in Komplex_Typ) is

  begin
     Put(K.Re, Fore => 0, Aft => 2, Exp => 0);
     Put('+');
     Put(K.Im, Fore => 0, Aft => 2, Exp => 0);
     Put('i');
  end Put;

  function Absolut(K : in Komplex_Typ) return Float is

  begin
     return Sqrt(K.Re**2 + K.Im**2);
  end Absolut;

  function Mult(K1, K2 : in Komplex_Typ) return Komplex_Typ is

     Temp : Komplex_Typ;

  begin
     Temp.Re := (K1.Re*K2.Re)-(K1.Im*K2.Im);
     Temp.Im := (K1.Re*K2.Im)+(K1.Im*K2.Re);
     return Temp;
  end Mult;

end Komplex_Paket;
Exempel 18: Paket för komplexa tal

Paketets huvud är dess gränssnitt mot omgivningen. Här talar vi om vad vi vill att andra paket och program ska känna till om just det här paketet.

I paketets kropp (som i det här fallet återfinns i filen komplex_paket.adb) hittar vi implementationen av de funktioner och procedurer som utlovades i pakethuvudet.

komplex_test.adb
with Ada.Text_IO;        use Ada.Text_IO;
with Ada.Float_Text_IO;  use Ada.Float_Text_IO;
with Komplex_Paket;      use Komplex_Paket;

procedure Komplex_Test is

   K1, K2 : Komplex_Typ;

begin
   Put("Ange ett komplext tal: ");
   Get(K1);
   Put("Talet är: ");
   Put(K1);
   New_Line;
   Put("Absolutbelopp: ");
   Put(Absolut(K1), Fore => 0, Aft => 2, Exp => 0);
   New_Line;
   Put("Ange ett komplext tal till: ");
   Get(K2);
   Put(K1);
   Put(" * ");
   Put(K2);
   Put(" = ");
   Put(Mult(K1,K2));
   New_Line;
exception
   when Komplex_Fel =>
      Put_Line("Felaktig inmatning!");
end Komplex_Test;
Exempel 19: Test av paketet för komplexa tal

I exempel 19 ser vi hur man kan använda det egendefinierade paketet för komplexa tal.

su4-7 <9> komplex_test
Ange ett komplext tal: 3+2i
Talet är: 3.00+2.00i
Absolutbelopp: 3.61
Ange ett komplext tal till: -4+5i
3.00+2.00i * -4.00+5.00i = -22.00+7.00i
su4-7 <10>
Exempel 19b: Körexempel från test av komplexa tal

12.2. Generiska paket

Låt säga att vi vill implementera en stack i Ada. En stack är en datastruktur som funkar ungefär som en trave med tallrikar. Man lägger något på toppen eller så tar man bort något från toppen. Vi kan t.ex. lagra en stack i en array, men då skulle vi tvingas göra massor av olika typer av stackar. Ponera att vi vill ha en stack för heltal, en stack för flyttal och en stack för strängar. Vi vill inte skapa tre olika paket som ser nästan likadana ut. Istället löser vi det genom att skapa ett generiskt paket.

Ett generiskt paket är egentligen inte ett paket, det är en paketmall. Vi anger några generiska parametrar i början som talar om vad det är som ska kunna anpassas. En generisk stack återfinns i exempel 20.

gen_stack.ads
generic

   Max : Positive;
   type Item is private;
   with procedure Put(I : in Item);

package Gen_Stack is

   procedure Push(I : in Item);
   function Pop return Item;
   procedure Put_Stack;

end Gen_Stack;
gen_stack.adb
package body Gen_Stack is

   S : array(1..Max) of Item;
   Top : Integer range 0..Max;

   procedure Push(I : in Item) is
   begin
      Top := Top+1;
      S(Top) := I;
   end Push;

   function Pop return Item is
   begin
      Top := Top-1;
      return S(Top+1);
   end Pop;

   procedure Put_Stack is
   begin
      for I in 1..Top loop
	 Put(S(I));
      end loop;
   end Put_Stack;

end Gen_Stack;
Exempel 20: Generiskt paket för stackar

Vi har tre generiska parametrar i pakethuvudet:

I exempel 21 instansierar vi det generiska paketet. Det innebär att vi fyller paketmallen med innehåll och skapar ett verkligt paket.

gen_stack_test.adb
with Ada.Text_IO, Ada.Integer_Text_IO;
use Ada.Text_IO, Ada.Integer_Text_IO;

with Gen_Stack;

procedure Gen_Stack_Test is

   procedure Put_Stack_Item(I : in Integer) is
   begin
      Put("[");
      Put(I, Width => 0);
      Put("] ");
   end Put_Stack_Item;

   package Int_Stack is new Gen_Stack(100, Integer, Put_Stack_Item);
   
begin

   Int_Stack.Push(14);
   Int_Stack.Push(19);
   Int_Stack.Push(45);

   Put("Jag hämtar ");
   Put(Int_Stack.Pop, Width => 0);
   New_Line;

   Int_Stack.Push(34);
   Int_Stack.Put_Stack;

end Gen_Stack_Test;
Exempel 21: Test av generiskt stackpaket

I exempel 21 skapar vi en stack som kan lagra maximalt 100 st Integer. Dessa kan skrivas ut med hjälp av vår egendefinierade procedure Put_Stack_item.

su4-7 <41> gen_stack_test
Jag hämtar 45
[14] [19] [34] 
su4-7 <42>
Exempel 21b: Körexempel från test av generiskt stackpaket

13. Pekare

En pekare är en referens till någonting - ett sätt att indirekt komma åt data. Att använda pekare är inte så krångligt, men man måste hålla tungan rätt i munnen.

Om vi deklarerar en variabel I som Integer så innebär det att kompilatorn ser till att det finns utrymme för att lagra ett heltal någonstans. Om vi istället använder pekare måste vi själva se till att allokera minne när programmet körs.

access_test.adb
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;

with Ada.Unchecked_Deallocation;

procedure Access_Test is

   type Integer_Reference is access Integer;

   procedure Delete is 
     new Ada.Unchecked_Deallocation(Object => Integer,
				    Name => Integer_Reference);
     
   I_Ref1, I_Ref2 : Integer_Reference;

begin

   -- (a) Allokera först och tilldela värde sedan
   I_Ref1 := new Integer;
   I_Ref1.all := 45;

   -- (b) Allokera och tilldela värde på samma gång
   I_Ref2 := new Integer'(45);
   
   -- (c) Jämför pekare
   if (I_Ref1 = I_Ref2) then
      Put_Line("Same address");
   else
      Put_Line("Not the same address");
   end if;

   -- (d) Jämför innehåll
   if (I_Ref1.all = I_Ref2.all) then
      Put_Line("Same contents");
   else
      Put_Line("Not the same contents");
   end if;

   -- (e) Lämna tillbaka allokerat minne
   Delete(I_Ref1);
   Delete(I_Ref2);   
end Access_Test;
Exempel 22: Demonstration av pekare

I exempel 22 laborerar vi med pekare till heltal. För att kunna göra detta måste vi först definiera en pekartyp. Vi definierar också två stycken pekare I_Ref1 och I_Ref2. Att allokera minne för att lagra saker sker enkelt med new, men för att kunna lämna tillbaka minnet måste vi instansiera den generiska proceduren Ada.Unchecked_Deallocation och tala om vilken typ av data det är som ska avallokeras och hur vi refererar till den.

Innehållet i programmet är som följer:

  1. Vi allokerar minne för att lagra en Integer med hjälp av new. Adressen till denna minnesbit sparas i I_Ref1. Därefter placerar vi ett heltal på den plats som pekas ut av I_Ref1 med notationen .all.
  2. Vi kan också allokera minne och placera data i det på samma gång.
  3. Om vi bara skriver I_Ref1 så menar vi pekaren och inte det data som pekaren pekar ut. Om vi jämför pekarna som i exemplet jämför vi i praktiken alltså två adresser som ju är olika eftersom de pekar ut skilda data.
  4. Med notationen .all menar vi det data som pekaren pekar ut. Detta är samma, eftersom båda pekarna pekar på heltalet 45 (som dock finns lagrat på två olika platser i minnet).
  5. Slutligen lämnar vi tillbaka det minne vi allokerade med proceduren Delete.

Om man vill lagra större objekt, t.ex. egendefinierade poster, behöver man inte använda .all-notationen. Om vi har en postpekare P som pekar på en post med bl.a. fältet Name betyder alltså P.all.Name samma sak som P.Name.


14. Lista över exempel


© 2001 Peter Johansson