Moving a record to private

Suppose I have a simple package:

package Test is
   type Point is record
      X : Integer;
   end record;
end Test

And a user of the package like:

package User is
   procedure Test_User;
end User;
with Test;

package body User is

   procedure Test_User is
      P : Test.Point;
      Q : Test.Point;
   begin
      P.X := 1;
      Q.X := P.X;
   end Test_User;

end User;

How can I move the declaration of Point to the private part of package Test without changing the code of User?

I mean, read access is easy (just declare Point as tagged and a function X):

package Test is
   type Point is tagged private;

   function X (P : Point) return Integer is (P.X);

private:
   type Point is tagged record
      X : Integer;
   end record;
end Test

But how to keep the syntax for write access P.X := 1 instead of something like P.Set_X (1)?

Thanks

I don’t now if there is a perfect solution in current Ada. There’s a suggestion to the ARG to allow for by reference function results, but it will be a ways off before they even decide to accept or reject the suggestion I imagine.

The following is not a perfect solution but it is as close as I could get:

Ada allows for the creation of a “reference type”. Using a supplementary type with the Implicit_Dereference aspect set as a proxy for the item you want to return by reference.

Using this you can get a function that returns a reference to your X field if the field is aliased and you make the P parameter in out:

package Test is
    
       type Point is tagged private;
    
       -- This is a proxy type that "looks" like an Integer in "most" cases (not all though)
       type Reference_Type(Value : not null access Integer) is limited private
          with Implicit_Dereference => Value;
          
       function X (P : in out Point) return Reference_Type;  -- returns the reference, not the "in out"
    
    private
    
       type Point is tagged record
          X : aliased Integer;  -- note the aliased
       end record;
       
       type Reference_Type(Value : not null access Integer) is limited null record;
       
       function X (P : in out Point) return Reference_Type is (Value => P.X'Access);
       
    end Test;

But it isn’t perfect you will run into issues where the user will have to “qualify” the type as the function result can look like both a Reference_Type and an Integer, so if the compiler can’t figure it out, you have to add type qualification. A big example is your line later on:

Q.X := P.X;

Here the compiler won’t know which type you mean since both sides can be either or. It can be fixed like this:

Q.X := Integer'(P.X); -- Tells the compiler which type the assignment is supposed to be.

But that does mean your user code will have to change slightly.

However, lines like

P.X := 1; 

and

Temp : Integer := P.X;

will work just fine. Sorry it isn’t a perfect solution though.

1 Like

Thank you, I had something like that in mind but couldn’t find the right way. Now, it’s clearer, and the drawbacks too.

So it’s worth the burden to make every record type private from the start. Even if writing getters and setters for every field is not funny when you start writing a package.

Moving the private record in the package body (using a holder) seems also a good idea so that child packages can’t rely on the inner of the record.

Thanks!

If you need to ‘writ[e] getters and setters for every field’, the record shouldn’t be private.

It would be reasonable to have getters and setters for components of a private object, for example the coordinates of a pixel.

1 Like

Well, it helps if you change your mind later for the implemention of the record or some fields of the record. Nothing is perfect from the first iteration and the use cases may change.

With everything private/getters/setters, you can more easily change the internals while keeping the (old) api in an usable state and (eventually) add a new api.

There was a vote to allow dot-notation on all types recently; I dissented, and honestly would rather remove the syntax altogether, going back to Ada95-style notation.(There are several reasons for this, but that would be besides the point.) — Given the problem, it’s not exactly doable as-stated, with current Ada.

Now, I do have a proposal on abstracted types and abstracted interfaces that could help, see: this, which would allow for a definition of “point” on the abstract-datatype level (CS dfn. ADT) and/or on the abstract-interface level (language-construct/-usage), but this, too, doesn’t address the issue now.

There are several methods you can use, though they will require some modification, simply because you are altering the design/type-definition/structure at a fundamental level; the one that will cause the least modification is the generic-bridge:

Generic
  Value : in out Point;
Package Test.Generic_Point is
  X : Integer renames Value.X;
  Y : Integer renames Value.Y;
-- You may have to use Import+address-overlay; this depends on visibility
-- rules for child packages; which I may be misremembering, if so then:
-- FOR OVERLAY, remove the renamings. and uncomment the following
--Private
-- Pragma Import( X );
-- Pragma Import( Y );
-- For X.Address use Value.X'Address;
-- For Y'Address use Value.Y'Address;
End Test.Generic_Point;

Once you make Point a private type, X and Y become invisible, which would cause compiler errors on lines like P.X := 1 – you may be tempted to fix this error, Wait!, the solution is to go to the declarative region and alter P to something like Data_P and add package P is new Test.Generic_Point(Data_P), then all your operations on P in the block become valid again.

In some cases you can use this interfacing/adaptor trick to reduce all needed modification to an added instantiation and a renames. I use this trick “going the other way”, using the generic’s defaults, in EVIL’s File and Strings to that switching between (e.g.) Character/String and Wide_Character/Wide_String is as simple as changing the instantiation of:
Package String_Package is new EVIL.Util.Strings(Character, String, ' ', others => <>);
to
Package String_Package is new EVIL.Util.Strings(Wide_Character, Wide_String, ' ', others => <>);
and letting the defaults do the rest of the work; if I’m using EVIL.Util.Files and supplying String_Package to its instantiation while the appropriate Ada.[Wide_[Wide_]]Text_IO package are visible, those defaults take up the new subprograms and allow the entire subsystem to change (from String to Wide_String) based on just that alteration. (The EVIL.Utils.Files package provides a file-type that automatically closes and has an easy/integrated stream function.)

TL;DR — When you are doing fundamental design-alterations, the code should break: to do otherwise is to invite subtle errors as the foundations mutate away from the design they were originally founded on.

Well, OP you are fighting against the very spirit of the language here. What is the point of the private keyword if you want read/write access to individual fields ? There are two solutions if you really want to hide only part of your type.
Either from an abstract and private tagged type, and extend the type. Or compose it, aka include the hidden fields as a (non-tagged) type as a component of another record, with visible fields.

package essai is
	type A is private;
	const : constant A;
	type B is record
		Hidden : A := const;
		Int: Integer;
		C : Character;
	end record;
private
	type A is record
		hidden_field1: character;
		hidden_field2: integer;
	end record;
	const : constant A := ('C', 3);
end essai;

Unfortunately you can’t put an indefinite type as a component, so that’s an advantage tagged types do have. “abstract” keeps your user from declaring a value with just the hidden fields, which would be either/or undesirable or nonsensical.

Not necessarily.
Having types with visible implementation may be perfectly fine… they might not, though. It’s a design-consideration, just like whether or not to use limited. Just like whether or not to use unknown discriminants. — One reason to use both limited and unknown discriminants is to force access-control; consider:

Package Example is
  Type Key(<>) is limited private;
  Function  Get_Key return Key;
  Procedure Store( Control : Key; Value : String );
  Function Retrieve ( Control : Key ) return String;
  No_Key : Exception;
Private
   Number_of_Keys : Constant := 7;
   Type Key_Index is range 1..Number_of_Keys;

   Package Mailboxes is new Ada.Containers.Indefinite_Ordered_Maps
   ( Key_Type => Key_Index, Element_Type => String );
   Boxes : Mailboxes.Map:= Mailboxes.Empty_Map;

   Type Key( Index : not null access Key_Index ) is
     new Ada.Finalization.Limited_Controlled
     with null record;

   overriding
   procedure Finalize  (Object : in out Key);
End Example;
-- ...
Package body Example is
  procedure Finalize  (Object : in out Key) is
    type Temp_Access is access all Key_Index;
    procedure Free is new Ada.Unchecked_Deallocation( Key_Index, Temp_Access );
    -- Disgusting hack because anon. accesses are disgusting.
    Temp : Temp_Access with Import, Address => Object.Index'Address;
  begin
    Boxes.Delete( Object.Index.All );
    Free( Temp );
  end Finalize;

  Function  Get_Key return Key is
    use all type Ada.Containers.Count_Type;
  Begin
    if Mailboxes.Length(Boxes) = Number_of_Keys then
      raise No_Key with "No available key.";
    end if;

    declare
      Function First_Free return Key_Index is
      begin
        for X in Key_Index when not Boxes.Contains(X) loop
          return X;
        end loop;
        raise Program_Error with "Impossible!";
      end First_Free;
      Value  : Key_Index renames Key_Index'(
        (if Mailboxes.Is_Empty(Boxes) then Key_Index'First
         elsif Mailboxes.Last_Key(Boxes) = Key_Index'Last then First_Free
         else Key_Index'Succ(Mailboxes.Last_Key(Boxes)))
                                            );
    begin
      Return Result : constant Key := ( Ada.Finalization.Limited_Controlled
               with Index => new Key_Index'(Value)
             ) do
          Mailboxes.Include( Key => Result.Index.All, New_Item => "", Container => Boxes );
       End return;
    end;
  End Get_Key;
   
  Procedure Store( Control : Key; Value : String ) is
  begin
    Mailboxes.Replace( Key => Control.Index.All, New_Item => Value, Container => Boxes );
  end Store;

  Function Retrieve ( Control : Key ) return String is
  ( Boxes.Element(Key => Control.Index.All) );
End Example;

The above models a controlled “mailbox” of texts, there is one key per ‘box’ which is only obtainable via the Get_Key function — note how this forces the use of initialization; you can use this technique to (e.g) force SQL-escaping on user-text, thus avoiding SQL-injection.

This is a design-issue; how much of the internals are visible is a bit of an art, and there are ways that you can “do both”, say, by having a library for use, but another for internal use; this sort of “layering” could be useful for making a good API-library, using Ada’s features, with a ‘thin’ one interfacing some C-drivers.

1 Like