AdaPower Logged in as Guest
Ada Tools and Resources

Ada 95 Reference Manual
Ada Source Code Treasury
Bindings and Packages
Ada FAQ


Join >
Home >
Articles >
Getting Started >
Ada FAQ >
Source Treasury >
Books & Tutorials >
Packages for Reuse >
Latest Additions >
Ada Projects >
Press Releases >
Ada Audio / Video >
Home Pages >
Links >
Contact >
About >
Login >
Back
Data Structures Containing Limited Items (Matthew Heaney)

In this article I show how to implement a stack whose items are limited.

Most data structures you see are implemented this way:

generic
  type Item_Type is private;
package Stacks is

  type Stack_Type is limited private;

  procedure Push (Item : in     Item_Type;
                  On   : in out Stack_Type);
...
end Stacks;

The generic formal type Item_Type is non-limited, so it comes with
assignment.  This allows you to implement operation Push (for a bounded
stack) like this:

  procedure Push (Item : in     Item_Type;
                  On   : in out Stack_Type) is
  begin
     On.Top := On.Top + 1;
     On.Items (On.Top) := Item;
  end;

But suppose you need a stack of, say, files?  Type File_Type, in
Text_IO, is limited:

  type File_Type is limited private;

so you wouldn't be able to instantiate the stack package above.  But
even if you could, it's not clear how you'd implement Push, which
requires assignment.

This isn't an issue just for stacks.  In my previous posts on
reference-counted lists, Cons and Set_Head did the same thing, using
assignment to copy the item to the node at the head of the list.

Here is the solution: allocate an empty slot at the top of the stack,
which creates an uninitialized object, to which a client later refers
indirectly, via an access object.

Whew!  All that means is that Push will look like this:

   procedure Push (Stack : in out Stack_Type);

which may seem a bit strange, since Push doesn't take an Item
parameter.  All Push does here is allocate the top object, but doesn't
actually give it a value.

In a traditional implementation, stack objects are hidden from the
client, who can only set and get top values.  In this alternative
implementation, we let the client manipulate the actual object:

   type Item_Access is access all Item_Type;

   function Set_Top (Stack : Stack_Type) return Item_Access;

Operation Set_Top returns an access object which designates the object
at top of the stack.  The client can then refer to the object by
dereferencing the pointer:

   Push (Stack);

   declare
      File : File_Type renames Set_Top (Stack).all;
   begin
      Create (File, Out_File, "myfile.dat");
      ...
   end;

We can combine these two separate operations into one, so that Push
returns the reference itself:

   function Push
     (Stack : in Stack_Type) return Item_Access;

Rewriting the example above:

  declare
    File : File_Type renames Push (Stack).all;
  begin
    Create (File, Out_File, "myfile.dat");
    ...
  end;

The File_Manager (see the code) is a high-level abstraction for
manipulating a collection of instances of type File_Type, which is
implemented using our special stack.

The test driver pushes four files on the stack, then lists their names
and contents:

$ test_file_manager
Name is '/home/matt/patterns/collections-of-limited-items/eddie.txt'
Name is '/home/matt/patterns/collections-of-limited-items/french.txt'
Name is '/home/matt/patterns/collections-of-limited-items/emacs.txt'
Name is '/home/matt/patterns/collections-of-limited-items/wave.txt'
van halen rules!
je vous aime
emacs: everything but the kitchen sink
killer wave, dude


Implementation Issues

The bounded stack in this example is implemented in the traditional way,
as an array of items, and an index for the top:

   type Item_Array is
      array (Positive range 1 .. Max_Depth) of aliased Item_Type;

   type Stack_Type is
      limited record
         Items : Item_Array;
         Top   : Natural := 0;
      end record;

The only wrinkle here is that Item_Array is an array of "aliased" items.
This is so, because we need to return access objects that designate
individual array elements.

All Push has to do to "allocate a new top item" is increment the top
index:

   procedure Push (Stack : in out Stack_Type) is
   begin
      Stack.Top := Stack.Top + 1;
   end;

Set_Top is named so because we want to contrast it to Get_Top, which has
its traditional implementation, returning the value of the item at the
top of the stack:

   function Get_Top
     (Stack : Stack_Type) return Item_Type is
   begin
      return Stack.Items (Stack.Top);
   end;

Get_Top does not change the state of the stack.  Neither does Set_Top
really, because that operation just returns a pointer to the top object,
but we wish to emphasize that state changes are involved (because the
client is going to be change the value of the top object).

In an ideal language, all subprograms would allow you to have in out
parameters, even value-returning subprograms.  We would really like to
declare Set_Top this way:

  function Set_Top (Stack : in out Stack_Type) return Item_Access;

However, we can't do this in Ada, because some people have the strange
belief that if you spell a value-returning subprogram f-u-n-c-t-i-o-n,
that that spelling somehow gives the subprogram special status, a status
that disallows in out parameter modes.  Weird.

We can work around this egregious limitation by making Stack an access
parameter:

  function Set_Top (Stack : access Stack_Type) return Item_Access;

This is actually safer than the technique I'm about to show you, but
there are a couple of issues with it.

One issue is that it adds even more syntactic overhead to an operation
that already has a fair amount of syntactic overhead.  You have to
declare the stack object as aliased, and then take the 'Access of that
stack:

  Stack : aliased Stack_Type;
...
  Set_Top (Stack'Access).all := ...;

or

  Close (Set_Top (Stack'Access).all);

It's bad enough that we have to apply the .all selector to the result of
a function, but having to take the 'Access of the input parameter too is
like adding insult to injury.

The other issue is that these data structures are written to support
static polymorphism.  Suppose we had an unbounded stack, implemented as
a linked list of nodes allocated on the heap.  We could legally
implement Set_Top as

  type Node_Type is
     limited record
        Item : aliased Item_Type;
        Next : Node_Access;
     end record;

  function Set_Top (Stack : in Stack_Type) return Item_Access is
  begin
     return Stack.Top.Item'Access;
  end;

because the node is on the heap, and there's no need to worry about
accessibility levels.  So the bounded version of Set_Top would have an
access parameter, but the unbounded version would have an in parameter.

But this is no good, because we want to have a consistent interface for
all stacks, so that if we want to change to an unbounded form (say),
then the only code we have to modify is the instantiation.  This is how
Direct_IO and Sequential_IO are designed.

So we want Set_Top to take an in parameter for two reasons: so we can
avoid syntactic overhead, and to have an interface consistent with other
stacks.

The language requires that you have an "aliased view" when you take the
'Access of an object, so this wouldn't work:

   function Set_Top
     (Stack : Stack_Type) return Item_Access is
   begin
      return Stack.Items (Stack.Top)'Access;
   end;

because the Stack parameter isn't an access parameter, and isn't
tagged.

We work around that problem by using our friend
System.Address_To_Access_Conversions, which provides a portable way of
converting between addresses and access objects.

Instantiating that package on Stack_Type provides the operations we need
to get an aliased view of the stack, which we can then use to legally
take the 'Access of a stack item:

   function Set_Top
     (Stack : Stack_Type) return Item_Access is

      SA : constant Object_Pointer :=
        To_Pointer (Stack'Address);
   begin
      return SA.Items (SA.Top)'Access;
   end;

Note that taking the 'Address of a subprogram parameter only makes sense
if the parameter is passed by reference.  The language mandates that
limited types and tagged types are passed by reference, so all we need
to do is implement the full view of Stack_Type as a limited record:

   type Stack_Type is
      limited record
         Items : Item_Array;
         Top   : Natural := 0;
      end record;

If you're implementing a non-limited private type, then just privately
implement the type as tagged, and this will guarantee that objects of
that type get passed by reference.

The stack example also exports an active iterator with a Set_Item
operation (analogous to Set_Top) which allows us to modify any item in
the stack, not just the top one.

I realize that this is a bit of a back-door technique, and that it isn't
as safe as the access parameter alternative.  However, I take the
attitude that programmers basically know what they are doing, and that
type safety will be built into the next higher level of abstraction.

Matt

The code below is in a format suitable for use with gnatchop.


--STX
with System.Address_To_Access_Conversions;

generic

   type Item_Type is limited private;

   Max_Depth : in Positive;

package Stacks is

   type Stack_Type is limited private;


   type Item_Access is access all Item_Type;

   for Item_Access'Storage_Size use 0;

   function Push
     (Stack : in Stack_Type) return Item_Access;

   procedure Push (Stack : in out Stack_Type);

   procedure Pop (Stack : in out Stack_Type);


   function Get_Top
     (Stack : Stack_Type) return Item_Type;

   function Set_Top
     (Stack : Stack_Type) return Item_Access;


   function Get_Depth
     (Stack : Stack_Type) return Natural;


   generic
      with procedure Process
        (Item : in out Item_Type;
         Done : in out Boolean);
   procedure For_Every_Item (Stack : in out Stack_Type);


   type Stack_Iterator is private;

   function Initialize
     (Stack : Stack_Type) return Stack_Iterator;

   function Is_Done
     (Iterator : Stack_Iterator) return Boolean;

   function Get_Item
     (Iterator : Stack_Iterator) return Item_Type;

   function Set_Item
     (Iterator : Stack_Iterator) return Item_Access;

   procedure Advance
     (Iterator : in out Stack_Iterator);

private

   type Item_Array is
      array (Positive range 1 .. Max_Depth) of aliased Item_Type;

   type Stack_Type is
      limited record
         Items : Item_Array;
         Top   : Natural := 0;
      end record;


   package Address_To_Access_Conversions is
     new System.Address_To_Access_Conversions (Stack_Type);

   type Stack_Iterator is
      record
         Stack : Address_To_Access_Conversions.Object_Pointer;
         Index : Natural;
      end record;

end Stacks;


package body Stacks is

   use Address_To_Access_Conversions;


   function Push
     (Stack : in Stack_Type) return Item_Access is

      SA : constant Object_Pointer :=
        To_Pointer (Stack'Address);
   begin
      SA.Top := SA.Top + 1;
      return SA.Items (SA.Top)'Access;
   end;


   procedure Push (Stack : in out Stack_Type) is
   begin
      Stack.Top := Stack.Top + 1;
   end;

   procedure Pop (Stack : in out Stack_Type) is
   begin
      Stack.Top := Stack.Top - 1;
   end;


   function Get_Top
     (Stack : Stack_Type) return Item_Type is
   begin
      return Stack.Items (Stack.Top);
   end;


   function Set_Top
     (Stack : Stack_Type) return Item_Access is

      SA : constant Object_Pointer :=
        To_Pointer (Stack'Address);
   begin
      return SA.Items (SA.Top)'Access;
   end;


   function Get_Depth
     (Stack : Stack_Type) return Natural is
   begin
      return Stack.Top;
   end;


   procedure For_Every_Item (Stack : in out Stack_Type) is
      Done : Boolean := False;
   begin
      for I in reverse Integer range 1 .. Stack.Top loop
         Process (Stack.Items (I), Done);
         exit when Done;
      end loop;
   end For_Every_Item;


   function Initialize
     (Stack : Stack_Type) return Stack_Iterator is
   begin
      return (To_Pointer (Stack'Address), Stack.Top);
   end;


   function Is_Done
     (Iterator : Stack_Iterator) return Boolean is
   begin
      return Iterator.Index = 0;
   end;


   function Get_Item
     (Iterator : Stack_Iterator) return Item_Type is
   begin
      return Iterator.Stack.Items (Iterator.Index);
   end;


   function Set_Item
     (Iterator : Stack_Iterator) return Item_Access is
   begin
      return Iterator.Stack.Items (Iterator.Index)'Access;
   end;


   procedure Advance
     (Iterator : in out Stack_Iterator) is
   begin
      Iterator.Index := Iterator.Index - 1;
   end;


end Stacks;


with Stacks;
with Ada.Text_IO; use Ada.Text_IO;

procedure Test_Stacks is

   package File_Stacks is new Stacks (File_Type, 10);
   use File_Stacks;

   Stack : Stack_Type;

   procedure Display_File
     (File : in out File_Type;
      Quit : in out Boolean) is

      Line : String (1 .. 80);
      Last : Natural;

   begin

      Put ("Contents of file ");
      Put (Name (File));
      New_Line;

      Reset (File, In_File);

      while not End_Of_File (File) loop
         Get_Line (File, Line, Last);
         Put_Line (Line (1 .. Last));
      end loop;

      New_Line (2);

   end Display_File;

   procedure Display_Files is
     new For_Every_Item (Display_File);

begin

   Push (Stack);

   declare
      File : File_Type renames Set_Top (Stack).all;
   begin
      Create (File, Out_File, "first_file.txt");
      Put_Line (File, "this is the first file pushed on the stack");
   end;

   Push (Stack);

   declare
      File : File_Type renames Set_Top (Stack).all;
   begin
      Create (File, Out_File, "second_file.txt");
      Put_Line (File, "this is the second file pushed on the stack");
   end;

   Display_Files (Stack);

end Test_Stacks;



with Ada.Text_IO;  use Ada.Text_IO;

package File_Manager is

   procedure Create_File (Name : in String);


   function Get_File return File_Type;


   type File_Access is access all File_Type;

   function Set_File return File_Access;


   procedure List_Files;

   generic
      with procedure Process
        (File : in out File_Type;
         Done : in out Boolean);
   procedure Visit_Files;

   procedure Close_Files;

end File_Manager;


with Stacks;

package body File_Manager is

   package File_Stacks is
     new Stacks (File_Type, Max_Depth => 10);

   use File_Stacks;

   Stack : Stack_Type;


   procedure Create_File (Name : String) is

      File : File_Type renames Push (Stack).all;
   begin
      Create (File, Mode => Out_File, Name => Name);
   end;


   function Get_File return File_Type is
   begin
      return Get_Top (Stack);
   end;


   function Set_File return File_Access is
   begin
      return File_Access (Set_Top (Stack));
   end;


   procedure List_Files is

      procedure Put_Name
        (File : in out File_Type;
         Done : in out Boolean) is

      begin
         Put_Line ("Name is '" & Name (File) & "'");
      end;

      procedure Put_Names is
        new For_Every_Item (Put_Name);

   begin

      Put_Names (Stack);

   end List_Files;


   procedure Visit_Files is

      Iter : Stack_Iterator :=
        Initialize (Stack);

      Done : Boolean := False;

   begin

      while not Is_Done (Iter) loop

         Process (Set_Item (Iter).all, Done);

         exit when Done;

         Advance (Iter);

      end loop;

   end Visit_Files;



   procedure Close_Files is
   begin

      while Get_Depth (Stack) > 0 loop

         Close (Set_Top (Stack).all);

         Pop (Stack);

      end loop;

   end Close_Files;


end File_Manager;


with File_Manager;  use File_Manager;
with Ada.Text_IO;   use Ada.Text_IO;

procedure Test_File_Manager is
begin

   Create_File ("wave.txt");
   Put_Line (Get_File, "killer wave, dude");

   Create_File ("emacs.txt");
   Put_Line (Get_File, "emacs: everything but the kitchen sink");

   Create_File ("french.txt");
   Put_Line (Get_File, "je vous aime");

   Create_File ("eddie.txt");
   Put_Line (Get_File, "van halen rules!");

   List_Files;


   declare

      procedure Put_File
        (File : in out File_Type;
         Done : in out Boolean) is

         Line : String (1 .. 80);
         Last : Natural;
      begin
         Reset (File, Mode => In_File);
         Get_Line (File, Line, Last);
         Put_Line (Line (1 .. Last));
      end;

      procedure Put_Files is
        new Visit_Files (Put_File);

   begin

      Put_Files;

   end;


   Close_Files;

end Test_File_Manager;


(c) 1998-2004 All Rights Reserved David Botton