Hiding record from child packages

Viewed 159
package Parent is

   type Item is private;
   
   function Get return Item;
   
private
   
   type Item is
      record
         Value : Boolean;
      end record;
   
   procedure Set 
     (Object : Item;
      Value  : Boolean);

end Parent;

Please tell me how in this example to prevent changing the Item record from child packages directly, leaving the ability to call the private method Set?

5 Answers

This is one of my gripes with Ada (only one of very very few) where it allows people to bypass privacy simply by making a child package of your package. I haven't messed with private child packages to see if I could make something work, but the PIMPL pattern does work in Ada if you are ok with heap allocation.

Basically you create an incomplete type in the package specification and use an access parameter of that type in the private record declaration. The spec has no clue what that record incomplete type looks like but since you are only using an access type to it, the spec will compile. One also should hide all the desired private operations like Set to the package body only.

Then in the package body you define the incomplete type fully and I recommend using Ada.Finalization to ensure the parameter is always allocated and deallocated completely.

I'll give a fully compilable example (tested with the online tutorialspoint ada compiler).

I also don't know what to do with your Get operation so just defaulted it to something and also added a Get_Value operation to get the boolean value out. You can remove/adapt this as you like.

It's not the most generic work around, but it is the one I have found works in Ada. Again, I haven't explored "private" child packages to see if they could be leverage in that way, so something to explore maybe.

with Ada.Finalization;
with Ada.Unchecked_Deallocation;

with Ada.Text_IO; use Ada.Text_IO;
procedure Hello is
    
    package Parent is
    
        type Item is tagged private;
        
        function Get return Item;
        function Get_Value(Self : in Item) return Boolean;
       
    private
    
        type Private_Item;
        type Private_Access is access Private_Item;
       
        type Item is new Ada.Finalization.Controlled with record
            Impl : Private_Access := null;
        end record;
        
        overriding procedure Initialize(Self : in out Item);
        overriding procedure Finalize(Self : in out Item);
    
    end Parent;
    
    package body Parent is
    
        type Private_Item is record
            Value : Boolean := False;
        end record;
        
        procedure Set 
            (Object : in out Item;
             Value  : Boolean)
        is begin
            Object.Impl.Value := Value;
        end Set;
        
        -- What is this supposed to do????
        function Get return Item is (Ada.Finalization.Controlled with Impl => new Private_Item);
        
        function Get_Value(Self : in Item) return Boolean is
        begin
            return Self.Impl.value;  -- raises null exception if not initialized
        end Get_Value;
            
             
        procedure Initialize(Self : in out Item) is
        begin
            if Self.Impl = null then
                Self.Impl := new Private_Item;
            end if;
        end Initialize;
        
        procedure Free is new Ada.Unchecked_Deallocation(Private_Item, Private_Access);
        
        procedure Finalize(Self : in out Item) is
        begin
            if Self.Impl /= null then
                Free(Self.Impl);
            end if;
        end Finalize;
    
    end Parent;
    
    I : Parent.Item;

begin
    Put_Line("Hello, world!");
    Put_Line(Boolean'Image(I.Get_Value));
end Hello;

As Jere has pointed out, this is a consequence of using child pkgs to provide programming by extension. Programming by extension is generally a bad idea, as it emphasizes ease of writing over ease of reading, and violates S/W-engineering principles.

Jere presented the standard way to hide the actual type from child pkgs, using access types. This works, but as it involves manual memory management is error prone.

A way to avoid this problem with programming by extension without using access types is to use ... more programming by extension:

private -- Parent
   type Root is abstract tagged null record;

   function Equal (Left : in Root'Class; Right : in Root'Class) is
      (Left = Right);

   package Class_Holders is new Ada.Containers.Indefinite_Holders
      (Element_Type => Root'Class, "=" => Equal);

   type Item is record
      Value : Class_Holders.Holder;
   end record;
end Parent;

package body Parent is
   type Real_Item is new Root with record
      Value : Boolean;
   end record;

You can store a Real_Item in a Holder. When retrieving the value, you have to convert it to Real_Item:

R : Real_Item;
V : Item;
...
R.Value := True;
V.Value.Replace_Element (New_Item => R);
...
R := Real_Item (V.Value.Element);

There are ways to use this approach in which Root can be an interface type, and others where it cannot. I always use an abstract tagged type to avoid having to remember which is which.

The function Equal is needed because class-wide types have no primitive operations (note that GNAT will compile this without Equal and with no association for "=", but this is a compiler error).

Yes, of course you can do it. Well, sort of.

But like most things Ada-esque, it requires a modicum of thought and planning.

Here's one way (the only way?)

Ada Hierarch Example

The corresponding declarations are,

package GrandParent is
   type Item is private;
private
   type Item is
      record
         Value : Boolean;
      end record;
end GrandParent;

package GrandParent.Parent is
   function Get
     (The_Item : in Item)
      return Boolean;
end GrandParent.Parent;

private package GrandParent.Child1 is
   procedure Set
     (The_Item : in out Item;
      Value    : in     Boolean);
end GrandParent.Child1;

The package bodies are,

package body GrandParent.Child1 is
   
   procedure Set
     (The_Item : in out Item;
      Value    : in     Boolean)
   is
   begin
      The_Item.Value := Value;
   end Set;
   
end GrandParent.Child1;

private with GrandParent.Child;

package body GrandParent.Parent is
   
   function Get
     (The_Item : in Item)
      return Boolean
   is
     (The_Item.Value);
   
   procedure Set
     (The_Item : in out Item;
      Value    : in     Boolean)
   is
   begin
      GrandParent.Child.Set
        (The_Item => The_Item,
         Value    => Value);
   end Set;
   
end GrandParent.Parent;

If you then try to have,

(private) with GrandParent.Child;
package GrandParent.Parent.Child is
end GrandParent.Parent.Child;

This raises a compile time error that the current unit must also be a direct descendant of GrandParent thus effectively making the GrandParent.Child1 package private to GrandParent.Parent.

Clients of GrandParent also won't have visibility to GrandParent.Child1. However, other children of GrandParent will have the same visibility as GrandParent.Parent

That's how one could hide the Set subprogram. What if you want to hide the private type from the package children?

Firstly, that's probably questionable, since children of a package with a private type are designed to fully interact with that type, since as others have described, children are about extending the capabilities of their respective parent package.

If you want to do that, then your best bet is to hide away the type Item and both the Get and Set routines into GrandParent.Child so that only GrandParent.Parent can see them (in it's private body) and expose whatever functionality you want to allow the GrandParent.Parent's children to have in the GrandParent.Parent package.

However, I'm not sure that's particularly useful. A question - if Parent's children should not have access to the inner workings of Item, why are they Parent's childern?

Type Item is not a tagged record. It is therefore not extendable in a child package.

Your question is confused. The package declaration you show does not declare an "Item record" object (a variable) that could be changed "directly" from any point; it declares only a record type called Item and some subprograms. Nothing a child package can do at run-time can change the record type; it is fixed at compile time.

Perhaps your example is incomplete, and does not reflect your real problem?

Related