Generalized private extension ("is record with private")

There was here an interesting discussion here Moving a record to private - #3 by Shuihuzhuan about setters and getters, and choosing between hiding or exposing data at record type level.

I have sometimes the in between situation.
If I want to create a type Person, exposing all fields except the age, I have to either make a bunch of setters and getters, or make two types, one being private (with other annoying constraints if they are in the same package due to the “premature” use of the type).

Let’s give an example in a language that have made the (wrong ) choice “Everything is a class” :grinning: :

class Person {
  public name: string; // The name is publicly accessible
  private age: number; // The age is accessible only within the Person class

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  public isAdult(): boolean {
    return this.age >= 18;
  }
}

What would be nice in Ada to have the ability to complete a type in the private part, something like:

package Person is

   type Person is record with private
      Name : Unbounded_String;
   end record;
   -- "record with private" is clear enough

   -- or
   type Person is record 
      Name : Unbounded_String;
   with private; 
   -- More natural syntax to me, but possible confusion with predicates,
   -- and taboo "end record" dropping :-) 

   function Is_Adult (P : Person) return boolean is (P.Age >= 18);

private
   type Person private record is
      Age : Natural;
   end record;
   -- It would be strange to have here a normal record type declaration
   -- that wouldn't tell to the reader that other fields are already declared.
   -- This is why there is an explicit "private record is", 
   -- close enough to what it actually means ("private additional part of the record is").
   -- But you could object that it is already the case (not having all the fields at hand)
   -- with tagged type, and that it not worth creating a new syntax for that reason. 

end Persons;

From the semantic point of vue, it would be a normal private type, with the same constraints applying on the partial vue, except that public fields are in direct access, like if Setters and Getters where provided.

And for the sake of unification, this could also apply to tagged type private extension.

Does this proposal make sense?

Yes it makes sense conceptually, but no, I wouldn’t want it and don’t ever remember a case where I’ve done something like what you’ve shown.

Forcing types to be fully exposed or private forces a distinct separation between data-only types without behavior and opaque types which maintain invariants. Ada conceptually makes this simpler by making it such that you can tell from a single line of declaration whether a type is data or an opaque type only manipulated through subprograms.

Is this it what you’re looking for? What’ is it that you don’t like in this design and want to improve?

with Ada.Strings.Unbounded;
use  Ada.Strings.Unbounded;

package Mixture is

  type Hidden is private;
  
  type Person is record
    Name: Unbounded_String;
    More: Hidden;
  end record;

  function is_Adult (P: Person) return Boolean;
  
private

  type Hidden is record
    Age: Natural;
  end record;

  function is_Adult (P: Person) return Boolean is (P.More.Age >= 18);
  
end Mixture;

OK, I agree.

This is clear, short, almost perfect :slight_smile:

I have oversimplified my case, and today I can’t remember how this proposal could have help with my problem!
I have a self referential structure, fully exposed in a spec (bbt/src/bbt-documents.ads at 1061e62626caeeb5511e7643360c580927083ffb · LionelDraghi/bbt · GitHub).
Jeffrey wrote (Papers/Self_Ref_No_Access.pdf at b579a6550bdbaa30bd3b7fe008f6c485dfa9f9dc · jrcarter/Papers · GitHub) and talk about that problem.

The solutions seems to be:

  1. Status Quo, I let the details exposed, no private part;
  2. Using Vector of access type instead of a Vector of the type works : I wrote the code, it works, I was able to split this big package in a child hierarchy by using limited and private with.
    But I’m not a big fan because of the access type;
  3. Using an Ada.Containers.Indefinite_Holders instance to hold the Vectors is more complex than needed;
  4. Using tagged record instead of record for the nodes is OK (and actually make sense in my case, because all nodes share some common properties), but, first, having Vectors of Node’Class instead of Vector of Node make the code kind of lying on my design intents, and I have the impression of inviting possible errors…

None of the solution is really nice, but I’m now experimenting with the last one.

Anyway, the night as gone, and I can’t remember how this proposal could have help with my problem.

I’ve done this often in the past, and I think a lot of it has to do with how fancy I try to be. If I leave to be just a simple public type with no operations and just fields, I don’t tend to run into those problems. I just don’t like it because it ups name pollution, but I still use it from time to time, something like:

    package Something is
		
		type Public is tagged record
			Value_1 : Integer := 0;
		end record;
		
		type Instance is new Public with private;
		procedure Primitive_Op1(Self : Instance) is null;
		procedure Primitive_Op2(Self : Instance) is null;
		
	private
	
		type Instance is new Public with record
			Value_2 : Integer := 10;
		end record;
	
	end Something;

I haven’t run into premature use issues using a template like that, but I may not be trying the same things you are.

1 Like

Perhaps, slightly abusing/higjacking the intention of record discriminants (i.e. parameterising a record) but would the following work:

type Person (Name : access String) is private;
function Is_Adult (P : Person) return Boolean;

private
   type Person (Name : access String) is
      record
         Age: Natural;
      end record;

And then in the body:

function Is_Adult (P : Person) return Boolean is (P.Age >= 18);

(Note, this can’t be implemented as an expression function in the spec due to linear elaboration issues)

1 Like

Ok, I understand what you’re getting at, but you need to understand Ada is NOT other languages.
There is a fundamental disconnect between what you think private (on a type) means, as compared/contrasted with other languages.

To illustrate, let’s take a step back to another classification of types: the limited type.
This type has no predefined assignment (:=) operator; this is something that most other languages don’t even have a concept for, so what is its purpose in Ada? — Simple! It is for interfacing, in particular hardware. Consider:

  -- Type's record definition.
  Type Internal_Timer is limited record
     Time : Some_HW_Time;
   end record;
  -- Type's operations
  Function "-"( Now : Internal_Timer; Then : Some_HW_Time ) return Duration;

  -- Actual Instance, Internal_Address is the memory-mapped location of the timer.
  Embedded_Timer : Internal_Clock
    with Import, Address => Internal_Address;

What would “Other_Timer : Internal_Clock;Other_Timer := Embedded_Timer;” even do? You cannot duplicate the hardware-timer that this is modeling, so eliminating the assignment operator perfectly models this direct-interfacing situation.

Likewise private is not about fields, but about the type itself, namely forcing the visible-part to use visible-subprograms, thus limiting access, or else completely visible implementation — this is a starkly different notion than languages like Delphi or C++ take: it is about the entire type’s implementation being visible or not. And this design decision has some good consequences in (a) forcing you to think about the interface that your type is presenting, and (b) making the move between private and non-private into something that introduces wide breakage: because this is a huge design-change: from visible implementation to non-visible implementation.

The above also forces you to be more mindful of your data-type’s, but this can be ‘enhanced’ into forcing control of the type altogether — consider the problem of having some user-input data that will be passed into a database-query (the semi-famous “Bobby Tables” problem) — and you can see exactly how this could be useful: to completely solve the Bobby Tables problem we need some method whereby we can force escaping on the user-string, and restrict our subprogram that acts on this to use that forced-escape-string.

Package Safe_Query_Strings with Pure is
  Type Safe_String(<>) is private;
  Function Create( Unsafe : String ) return Safe_String;
Private
  -- Import the escape function:
  Function Escape( Unsafe : String ) return String
   with Convention => Ada, Import, Link_Name => "SQL_Escape";

  Type Safe_String is new String;

  -- Force the call to Escape, convert the result.
  Function Create( Unsafe : String ) return Safe_String is
   ( Safe_String(Escape(Unsafe)) );
End Safe_Query_Strings;

As you can see, the unknown discriminant prohibits the auto-initialization, forcing an initializer; the private means that anything that’s not part of Safe_Query_Strings or its children cannot “see” the implementation is just a plain old String.

What you are proposing (field-level visibility) is far more granular than what Ada offers, yes… but by altering Ada’s semantics for private, you fundamentally alter these properties — instead of worrying about the implementation-visibility of the type-as-a-whole, now you are worrying about every field’s visibility. This increases the cognitive load on a programmer, both in design and in maintenance, but also increases the complexity of the compiler fairly significantly as you now have to track all accesses to all fields, rather than just checking some access against (a) if the type has private, and (b) if the location of this access is not in any of: the defining-scope’s private-area, its implementation, the private-area of any children, or the implementation area of its children.

You misinterpret Ada painting it much lower-level than it was thought and is. Ada abstracts as much as possible at least attempts to do so. Like functions and literals are conceptually same, so, in a better design the record members could be. You can make a type operation private. That is the original intent of Ada design. There is nothing altering in making record members = operations private. And indeed tagged types, protected objects, formal generic types allow this with little efforts. There is no “cognitive load” for the programmer not to look into implementation details. In fact the programmer should refrain doing so.

I think you’re reading what I said backwards: the cognitive load (and complexity) is increased where you must be cognizant of implementation-details outside the realm of the implementation, also they are increased when the details leak out. Ada does this through abstraction (i.e. information-hiding), but this does not negate certain fundamental operations being within consideration, and because these are fundamental they appear to you as “low level” — the example I provided of using limited to define a type whereby hardware interfacing occurs is merely a practical illustration thereof. (And, let us not forget: Ada was initially designed with the control of non-standard hardware in view [eg missiles], and so needed such low-level aspects.)

In some sense, Ada’s abstraction raises the “low-level” of hardware interfacing by allowing both the specificity of e.g. representation clauses, while allowing (even encouraging) abstractions thereof with e.g. enumerations and tasks. — An illustration of this would be an enumeration which has representations of particular bit-patterns, once these bit-patterns are specified (with the type) they can be dismissed from your cognitive-space, using the enumeration-names within your program; likewise using a Task to map a protocol (which is possible in certain circumstances) frees you from thinking of the details of that particular protocol, having abstracted it away.

This was exactly what you advocated for - to expose implementation = hidden record members (getters/setters). Not all language bugs are features. Ada does not need that sort of advocacy.

Limited has nothing to do with hardware. It is a small hack Ada deployed to avoid interfaces. Normally it should be a “copyable” interface all non-limited types inherit to.

It was much simpler to use a reserved keyword for the opposite thing since types with instances having identity are rare. It has nasty consequences for distorting the notion of the function returned value and lacking proper initialization/finalization, which led in turn to other semantic bugs like mix-ins, Initialize/Finalize hooks and enforcing access types on task members etc. No design fault remains unpunished!

Furthermore regarding hardware and more general cases, identity can be easily attached to an object without identity, e.g.by assigning an address to the object.

I feel it to be the reverse of what you say in terms of cognitive load. With type only visibility, I spend more time worrying over trying to remember which fields were intended to be mutable and which were not. With languages that provide field visibility mechanics, the compiler catches any mistakes (you can’t modify constant fields). And there often times where I want a mix of mutable and immutable fields. In Ada I have to be very careful not accidentally mutate a field I originally intended to be a constant. Easier to do in small projects, but in larger projects and long lasting projects it can be a pain, especially when other developers ignore comments and mutate fields meant to be immutable in their own code updates..

I do agree it is easier for the compiler to implement type visibility vs field visibility.

I’m can’t speak for compiler specialists (and I m pretty sure there is a lot of hidden complexity) : but my guess was that this proposal could be internally considered like two record, one being a private extension of the other. This is a purely compile time effort in my view, I don’t think there is need to track access on a “per field” basis, or that there will be an execution cost respect to the two records model.

It does have to do with interfaces, albeit tangentially; limited has nothing to do with hardware precisely because it is an abstraction.

Over the totality of all code, sure; but it’s not exactly uncommon… the OOP-world even made a big deal about the even-more-constrained (i.e. proper subset) notion of “singleton”.

This only makes sense if you’re talking about implementing the type alone; how much time do you spend using it? (Unless you’re a library implementer/maintainer, you shouldn’t be operating in the spaces where you can see the internals of the type, assuming you are using private types. This assessment indicates that you probably aren’t spending enough time designing the type and, in particular, its interface. [It could also be that you are changing designs mid-implementation.]) — The whole point of private types is that they are only interactable with the publicly-visible interface to clients of the package wherein they are defined.

Ada should be supporting the implementer as well as the user. The implementer can use help too from time to time. There are plenty of times where I have wanted to have a mix of private and public fields in my API. In Ada I have to jump through hoops for it. I like Ada, so I’ll jump through the hoops.

I get that is kinda cool nowadays to imply that people are bad designers because they don’t design the way you do, but I spend a lot of time and consideration on my designs.

It’s not that I’m happily throwing grenades or pointing-fingers, it’s that this is my own experience of seat-of-the-pants coding when I either don’t have a clear mental image of how things fit together, or else change my mind mid-stream.

Ada’s private types mitigate this a lot compared to other languages, and you can capitalize on that by designing your types (esp. private) with that usage in mind, and then using that public-interface throughout the rest of the program.

Ada does help implementers, a lot of thought goes into that, just ask @sttaft —but there could be a little bit more— I, myself, have a proposal for smoothing out some library nastiness; see: Abstracting TYPE and INTERFACE, which aims to extend Ada with meta-language (1) enabling good description of the type-hierarchy, allowing for nicely describing how the type is used; and (2) enabling factoring out properties of a type, to enable something akin to the Computer Science notion of Abstract Data Type and consolidating proof for/on that abstraction.

Surely not for an API. (Why would you want to expose any non-public information on the interface? Doesn’t that encourage dependency on those details, thus defeating the whole point?)

But, what I would propose [in the above link] would be something more akin to the property feature of Delphi: the ability to expose some possibly-virtual field, with write-only, read-only, or read-write access which may be backed by an actual field, a procedure (for writing) or a function (for reading).

This is perhaps the worst aspect of “also does” designing: taking a feature from another language and not capitalizing on the underlying concept, instead copying the implementation: Ada 2005 gave us interface, cribbed from Java, but didn’t consider property from Delphi… and also made them tied to tagged-types rather than possibly being untagged. — A design mistake, IMO.

To me, an API is more than the public facing parts. The public facing parts are for the users, the rest for the programmers, both the initial and those to follow (especially in Ada where someone can just make a child package of your package and leak private section details that way). I consider the whole package specification (.ads in GNAT) essentially an API, and I find it very advantageous (through experience) to ensure even the private parts are properly designed so the person after me is less likely to make mistakes updating it later on (though sometimes you can’t avoid it easily). There are definitely times when I am designing a type, that I want both some public fields for the users, but I also want to use some private fields for internal use.

As far as the dependency side of it, I actually prefer to move as much as I can to the body to limit dependencies (barring some design related goal to the contrary), but some of Ada’s rules and features unfortunately necessitate at least having it in the private section of the spec file, and sometimes even force it into the public section (The required implementation for variable indexing and iteration are big ones here come to mind).

Then you should really look into the link I provided: it addresses this issue completely. (Though the proposal is fledging right now, not even syntax — but that is purposeful, I want the underlying model to be complete and well-enginered before even touching syntax.)

C++ has this, it’s an uncopyable type:

struct Foo {
    Foo(const Foo&) = delete;
    Foo& operator=(const Foo&) = delete;
    //...
};

It’s important outside hardware interfacing because it’s used quite often for types which should never be copied because they’re heavyweight. Limited types get passed by reference automatically, so it perfectly fits within this similar notion.

the ability to expose some possibly-virtual field, with write-only, read-only, or read-write access which may be backed by an actual field

In general, I’m not a proponent of getters/setters or mixing publicly and privately visible state, this does strike a neuron that something similar to Ruby’s attr_reader/attr_writer/attr_accessor or Objective C’s @property system for shorthand could be a middle ground. In general, when I find I need public/private state in a type it signals I screwed up somewhere and Ada’s prohibition on this seems to fit with the rest of the language’s, “Nah, we don’t let your design mistakes compile.”

tied to tagged-types rather than possibly being untagged

Ada seems like it’s missing a way to describe a set of subprograms which a type must implement (or override) such as for easier specification of generics, I’m thinking of something along the line of C++ concepts or Rust’s traits, not for 'class style dynamic dispatching. This would generalize the notion of Constant_Indexing and Variable_Indexing aspects for other behavior – those being fulfilled by whatever this concept would be. @OneWingedShark’s proposal sounds sort of along these lines, a non-virtual interface description a set of required subprograms – perhaps closer to Go interfaces which are structurally typed, not nominally typed.

I agree with the first part of the sentence, but I don’t get the “bad design” point also made by @OneWingedShark.

There is no obvious design mistake in @Nordic_Dogsledding proposal, isn’t it?

 package Mixture is

  type Hidden is private;
  
  type Person is record
    Name: Unbounded_String;
    More: Hidden;
  end record;

  function is_Adult (P: Person) return Boolean;
  
private
  type Hidden is record
    Age: Natural;
  end record;

  function is_Adult (P: Person) return Boolean is (P.More.Age >= 18);
  
end Mixture;

The proposed

 package Mixture is

  type Person is record with private
    Name: Unbounded_String;
  end record;

  function is_Adult (P: Person) return Boolean;
  
private
  type Person is record
    Age: Natural;
  end record;

  function is_Adult (P: Person) return Boolean is (P.More.Age >= 18);
  
end Mixture;

expose the exact same design intent.
Why would it be qualified as “bad design”, or rejected by the compiler?

It’s the same design with just no boiler plate code.

Actually, the distinction between public and private is even more immediate, because you dont waste neurons on reading the “Hidden” type.

(And also, as you said Paul, in real life we don’t hit the bullseye every time.
So we sometimes have to move fields from private to public and vice versa.
In the current version, it sometimes means creating or removing a type. In the proposed one, it’s just moving a line.)

I can ear the “insufficient benefit to justify a change” point, but I don’t get the “this code result form a bad design” one.
But if someone give me a couter example, I will have no problem changing my mind!

1 Like