Am 14.02.2021 um 02:45 schrieb Ryan Joseph via fpc-pascal:
On Feb 13, 2021, at 12:38 PM, Sven Barth <pascaldra...@googlemail.com> wrote:

Right now, Ryan, your suggestion looks like a solution in search of a problem, or a 
"hey, look at that feature in language X, I so must have that in Pascal as 
well". Those concepts more likely then not tend to end in problems or should be 
rejected. So let's first define what we're trying to solve here:
What I'm trying to solve is the dilemma of OOP where we want to extend a class 
but the addition does not fit cleanly into an existing hierarchy. I know we've 
all had this problem and we were forced to settle on injecting some base class 
into a hierarchy even though other classes higher up don't need this 
functionality. Things like class helpers and interface delegation have already 
been added into the language so clearly there is a need not being addressed. 
We've all had this problem haven't we?

First off lets go summarize the differences/limitations of interface delegation 
(as it is now) when compared to good, old fashion normal inheritance and OOP:

1) Does not support fields, class operators, properties

Wrong, properties are supported as I showed, though they need to be with getters and setters right now.

And supporting the hoisting of class operators would be *wrong*, because they could only work on the delegated type as it would not know about the parent class.

3) Requires you to define an interface with duplicate methods, so boilerplate 
code

One can argue whether this is really boilerplate code or just an expression of Pascal's declarativeness (also see below).

=== code begin ===

type
   ITest = interface
     procedure Test;
   end;

   TTestImpl = record
     procedure Test;
   end;

   TTest = class(TObject, ITest)
   private
     fTest: TTestImpl;
   public
     property Test: TTestImpl read fTest implements ITest; default;
   end;

var
   t: TTest;
begin
   t := TTest.Create;
   try
     t.Test; // calls t.fTest.Test
   finally
     t.Free;
   end;
end.

=== code end ===

As the compiler needs to generate corresponding thunks anyway whether it needs 
to do this for a record or object is not really much more effort either.

Whether the class needs to declare the interface in its interface clause can be 
argued about.

But all in all one can see that with a few extensions of existing features one 
can easily provide a mixin-like, convenient functionality.
Great, this is getting closer to inheritance. It requires a default property 
(which is basically what traits were anyways) and a lifting of restrictions on 
the object being implemented. What we solved here is:

1) Default properties merge the namespaces, which is the perhaps the most important part of OOP, 
that is the class is "one thing". If there is one thing to accomplish it's this. Imagine 
how annoying OOP would be if you you have to do "sphere.circle.shape.Draw()" because we 
had TSphere which inherited from TCircle and in turn inherited from TShape.

To be fair, that *is* how inheritance works for example if you try to use OOP in C... (though it would be more like "sphere.parent.parent.Draw()")

What it leaves desired:

1) Making a dummy interface is annoying boiler plate but not a deal breaker.

Again, I see this is part of Pascal's declarativeness. Also this way you can make sure that only those methods/properties that you need are hoisted into the parent object allowing for easier reuse of existing classes/records/objects.

Take this for example:

=== code begin ===

type
  TMyRec = record
    procedure DoX;
    procedure DoY;
  end;

  IMyIntf = interface
    procedure DoX;
  end;

  TMyClass = class(TInterfacedObject, IMyIntf)
  private
    fMyRec: TMyRec;
  public
    property MyRec: TMyRec read fMyRec implements IMyIntf; default;
  end;

var
  t: TMyClass;
begin
  t := TMyClass.Create;
  try
    t.DoX;
    // this won't work however
    //t.DoY;
  finally
    t.Free;
  end;
end.

=== code end ===

Also it provides you with the possibility to really only pass on the interface to something that only expects the interface (though with the introduction of duck typed interfaces that wouldn't be that much of a problem either, albeit here the mapping to the interface would be determined at the time of the declaration of the class instead of when the cast happens)

2) No fields or properties although you have some solution below which will 
probably require some additional boiler plate. Class operators would are kind 
of sad to lose too but not a deal breaker.

No class operators. As mentioned above they wouldn't work anyway.

Properties *are* supported though they need getters and setters.

I stand by that using some concept of traits/mixins does all the stuff we want 
with less boiler plate but I'm happy to explore any other ideas.

One can explore further improvements down the road (e.g. fields), but we should provide a solid base first.


Of course this does not provide any mechanism to directly add fields, however 
the compiler could in theory optimize access through property getters/setters 
if they're accessed through the surrounding class instance instead of the 
interface.
How does this look?

Assume the following:

=== code begin ===

type
  TMyRecord = record
  private
    function GetX; inline;
  public
    fX: LongInt;
    property X: LongInt read GetX;
  end;

  IMyIntf = interface
    function GetX;
    property X: LongInt read GetX;
  end;

  TMyClass = class(TInterfacedObject, IMyIntf)
  private
    fMyRecord: TMyRecord;
  public
    property MyRecord: TMyRecord read fMyRecord implements IMyIntf; default;
  end;

var
  t: TMyClass;
begin
  t := TMyClass.Create;
  try
    // here the compiler knows that X is called through a hoisted interface property
    // thus it can optimize away the interface related stuff
    // and thus inline the GetX
    Writeln(t.X);
  finally
    t.Free;
  end;
end.
begin
end.

=== code end ===

One could argue that this contains quite some boilerplate code, but one should see this in relation: you're writing IMyIntf and TMyRecord *once* and use it *multiple* times. Thus the only code that you repeat multiple times is the one in TMyClass and here an IDE like Lazarus could help you.
Also this does not address the point of whether these delegates are able to 
access functionality of the surrounding class. In my opinion however this can 
be explicitely modelled by providing the class instance through a constructor 
or property or whatever.
Indeed but this can be solved by more boiler plate. :) In AfterConstruction you 
can set references as desired. I had other ideas on this as they related to 
traits but that wouldn't make sense if we were using an existing type like 
records or classes.
That is what I meant by explicitely modeling, though it doesn't matter if you do it inside the parent classes' constructor or its AfterConstruction. You simply need to make sure that your delegator is set up correctly so that it can do its work.

Regards,
Sven
_______________________________________________
fpc-pascal maillist  -  fpc-pascal@lists.freepascal.org
https://lists.freepascal.org/cgi-bin/mailman/listinfo/fpc-pascal

Reply via email to