[fpc-pascal] Traits Proposal
Sven Barth
pascaldragon at googlemail.com
Sun Feb 14 18:20:09 CET 2021
Am 14.02.2021 um 02:45 schrieb Ryan Joseph via fpc-pascal:
>> On Feb 13, 2021, at 12:38 PM, Sven Barth <pascaldragon at 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
More information about the fpc-pascal
mailing list