3
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 5

4

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);
        overriding procedure Adjust(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 Adjust(Self : in out Item) is
            Temp : Private_Access := Self.Impl;
        begin
            if Temp /= null then
                Self.Impl := new Private_Item'(Temp.all); --'
            end if;
        end Adjust;
        
        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;
1

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).

2
  • Yep, the main benefit to mine is that it works with limited types as well whereas the Indefinite_Holders only work with non limited types.
    – Jere
    Commented Nov 3, 2021 at 23:36
  • @jere, Reviewing your answer closely for my presentation "Avoiding Access Types" at Ada-Europe 2024, I noticed that your memory management is incorrect. I presume that you did not intentionally post an error, so you have helpfully demonstrated that memory management is hard, even in this relatively simple case. This justifies my thinking that access types should be avoided whenever possible, and since the question did not require them, it is a bad idea to suggest them. (I am confident that the memory management in my answer is correct.) Commented Jun 16 at 10:33
1

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?

1
  • To answer your final question: I don't think the OP is actually wanting to use child packages but is concerned another programmer will make a child package to get to the underlying private data structure and methods and be able to finagle with them at will. I think they are ultimately looking for how to create parts of a type that meet the general programming meaning of "private" rather than the Ada meaning of "private" which is closer to "protected" in languages like C++ and Java (not related to the Ada meaning of protected).
    – Jere
    Commented Nov 3, 2021 at 23:49
0

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

1
  • I need to prevent not extending Item, but changing the fields of an object of this type from child packages.
    – user15552120
    Commented Aug 18, 2021 at 21:06
0

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?

2
  • I mean, if the child package has an Item instance, obtained, for example, from the Get function, then it can directly change its fields, bypassing the Set function, which is intended for this.
    – user15552120
    Commented Aug 18, 2021 at 21:03
  • Does there need to be a child package? Simply move the declaration of the set procedure to the public area of the package and "with" the package parent. The contents of the public part of parent become visible, but not the private part of parent.
    – Jim Rogers
    Commented Aug 18, 2021 at 23:19