AdaCore / ada-spark-rfcs

Platform to submit RFCs for the Ada & SPARK languages
63 stars 28 forks source link

[RFC] Lightweight finalization #65

Closed Roldak closed 5 months ago

Roldak commented 3 years ago

Full text

Lucretia commented 3 years ago

"substantial" is spelt incorrectly as "substential."

Lucretia commented 3 years ago

As already mentioned, today's controlled objects allocated on the heap through an access type T must be finalized when T goes out of scope. First of all, we propose to completely drop this guarantee for libary-level access types

Do you mean for tagged types as well or just these types? It's unclear.

Lucretia commented 3 years ago

Re constants, Ada has the concept of deferred constants, would it be better to generalise this concept for all constant types? I've actually wanted to be able to define a constant without an initialiser and somewhere later on, assign a value, after this assignment, the constant is frozen and cannot be changed. It's not always convenient to initialise a constant at the place of creation.

QuentinOchem commented 3 years ago

I am personally strongly in favor of removing the need for tagged types when dealing with initialization & finalization, and I think this proposal make steps to the right direction. I think however that it needs to be considered together with questions around traditional OO construction destruction, which can lead to to spin Ada-related initialization / destruction concept to something closer to what people are accustomed to. These can be used with non tagged and tagged types.

I have a pending proposal (not submitted yet) that considers OO as a whole, but the specific construction / destruction can be taken out and merged here. The syntax isn't necessary important and this proposal could easily be extended to incorporate above concepts.

In a nutshell, I would suggest that:

I would also suggest to spin adjust, which is doing a post-copy analysis, into something akin to a copy constructor which allows to actually perform the copy, which will have obvious performance impact (e.g. you may not need to do the initial copy in the first place) and also closer to other languages.

pat-rogers commented 3 years ago

In a nutshell, I would suggest that:

  • we get rid of the Ada notion of initialization. The fact that it doesn't provide the same level of guarantees of a constructor (ie you can avoid calling it) is very confusing and actually breaches some of the safety constants one might want to enforce. The fact that it doesn't allow for parameters is also an issue.

I don't understand the above.

You can certainly force a call to a constructor, if that's required, using an indefinite limited view. Here's a real-world example:

type STM32_Ethernet_Device (<>) is new Network_Interface
  with private;
--  The compiler will require a call to a constructor function when
--  declaring an object of this type. See the function New_Device in
--  package STM32_Ethernet.NetIf.Ctors

And the ctor functions can have whatever parameters you want:

function New_Device
  (Transceiver : not null Any_PHY_Transceiver;
   Destination_Address_Filtering : Boolean;
   Time_Stamping : Boolean := False;
   Checksums_By_Hardware : Boolean := True;
   PHY_Auto_Negotiation : Boolean := True)
return STM32_Ethernet_Device;
jere-software commented 3 years ago

In the section "Finalized tagged types", I would actually recommend against letting derived types inherit the constructors. destructors are fine (and should be inherited so they can be destroyed though T'Class) but most OO languages don't make constructors inherited on purpose as it makes object design difficult. There's nothing more annoying than wanting to inherit off of an existing type to make a more complex version but you can't enforce a set of required initialization parameters because the old type has an inherited constructor with not enough parameters or (even worse) parameters that can break your new abstraction.

For example, when I was extending Gnoga, I wanted to create a Dialog Box type which inherited off of the View_Type (which is required to work with various GUI abstractions), however the constructor for the View_Type was primative and allowed one to supply parameters that would break the Dialog Box abstraction. My only recourse was to override it and raise an exception if used. This is not really intuitive.

Other OO languages I have worked in do not inherit the constructor (though I cannot speak for all of them).

jere-software commented 3 years ago

In a nutshell, I would suggest that: * we get rid of the Ada notion of initialization. The fact that it doesn't provide the same level of guarantees of a constructor (ie you can avoid calling it) is very confusing and actually breaches some of the safety constants one might want to enforce. The fact that it doesn't allow for parameters is also an issue.

I don't understand the above. You can certainly force a call to a constructor, if that's required, using an indefinite limited view. Here's a real-world example: type STM32_Ethernet_Device (<>) is new Network_Interface with private; -- The compiler will require a call to a constructor function when -- declaring an object of this type. See the function New_Device in -- package STM32_Ethernet.NetIf.Ctors And the ctor functions can have whatever parameters you want: function New_Device (Transceiver : not null Any_PHY_Transceiver; Destination_Address_Filtering : Boolean; Time_Stamping : Boolean := False; Checksums_By_Hardware : Boolean := True; PHY_Auto_Negotiation : Boolean := True) return STM32_Ethernet_Device;

But now if you have multiple ethernet ports, you cannot have an array of them and have to go with a much heavier container with heap allocation (not so great for embedded). It's a good work around in some cases, but not a good general work around.

EDIT: Sorry the Github interface mangled the quote.

QuentinOchem commented 3 years ago

@pat-rogers

I don't understand the above. You can certainly force a call to a constructor, if that's required, using an indefinite limited view.

 type STM32_Ethernet_Device (<>) is new Network_Interface
       with private;

This has a number of issues, in particular:

The first issue at least created a lot of distress in a few cases I've witness - including with industrial code amongst heavy users of Ada OOP.

pat-rogers commented 3 years ago

But now if you have multiple ethernet ports, you cannot have an array of them and have to go with a much heavier container with heap allocation (not so great for embedded).

Actually, physical devices like this are very limited in embedded systems, so separate objects (and a case statement to choose among them) would likely suffice. That said, I do accept your point that it is not fully general.

It's a good work around in some cases, but not a good general work around.

It isn't a workaround, that's too pejorative a term. It's the idiom for how to force a ctor call. Just as the idiom for forcing initialization in general is to present the user with an indefinite non-limited view, so that other objects will suffice for the initial value (in addition to functions).

But as I say, I agree it isn't fully general.

pat-rogers commented 3 years ago
I don't understand the above. You can certainly force a call to a
constructor, if that's required, using an indefinite limited view.

|type STM32_Ethernet_Device (<>) is new Network_Interface |

|with private; |

This has a number of issues, in particular:

  • the biggest one, you can't declare a component on that type in user code.

Well, you can, you just have to choose the function to call at the point of the enclosing type's declaration. But isn't it the same in the C++ case, in which a default ctor would necessarily be called? My C++ is rusty though, so maybe that's changed now.

  • a smaller but yet significant one - you can't force constructor functions to be called by child packages or the implementation

Meh. Child packages are part of the implementation.

The first issue at least created a lot of distress in a few cases I've witness - including with industrial code amongst heavy users of Ada OOP.

Interesting.

pat-rogers commented 3 years ago

On 01-Dec-20 2:14 PM, Pat Rogers wrote:

    I don't understand the above. You can certainly force a call to a     constructor, if that's required, using an indefinite limited view.

    |type STM32_Ethernet_Device (<>) is new Network_Interface |

|with private; |

This has a number of issues, in particular:

  * the biggest one, you can't declare a component on that type in user     code.

Well, you can, you just have to choose the function to call at the point of the enclosing type's declaration. But isn't it the same in the C++ case, in which a default ctor would necessarily be called? My C++ is rusty though, so maybe that's changed now.

No, that's wrong.

sttaft commented 3 years ago

For what it is worth, during the Ada 9X process, we understood the issue of inheriting of constructors, and it was felt that for constructors where inheritance was not desired, the function could be declared in a nested package (or a child package), or return a class-wide result. A function declared in a nested or child package has the visibility on the full type, without being inherited. A function returning a class-wide result is effectively a factory, since it makes no promise that the result is of the specific type being declared, but is guaranteed to satisfy the interface of the specific type. You can combine both ideas into a "factory" child package. Note that CodePeer uses "factory" child packages in general for SCIL creation, even though SCIL is actually defined by a variant record, so one could argue that using factory child packages has some value in and of itself.

QuentinOchem commented 3 years ago

@sttaft the issue is not about whether or not a constructor in inherited - which might have been the source of the confusion for the Ada 9X people. The key is to ensure that constructors are executed. In particular, ensuring that a child class always executes one of the parent constructors (and of course that an instantiation with always go through one of the constructor defined for the created class, which in turn will call parents). There's no provision for ensuring this in Ada - there are possible design pattern which unfortunately have been demonstrated to be difficult to use in real life projects.

reznikmm commented 3 years ago

We can try to disable a default "constructor" without introducing an unknown discriminant part, so the type can be used in components of an arrays or records. For instance we can extend this RFC with a syntax like this:

type T is private
   with Initialize => limited;

type Rec is record
   X : T;  --  Compiler time error
   Y : T := Some_Function; --  Ok
end record;

Other variants instead of limited: