[fpc-pascal] Traits Proposal
Sven Barth
pascaldragon at googlemail.com
Sat Feb 13 20:38:03 CET 2021
Am 10.02.2021 um 03:18 schrieb Ryan Joseph via fpc-pascal:
> We had talked about this some time ago and it's been rattling around in my brain so I wanted to write it down into a formal proposal where we can discuss it and hopefully agree upon a syntax. Everything is preliminary and tentative but this is a syntax which allows a "composition over inheritance" model, also known as "mix-ins" in some languages. That idea is similar to multiple inheritance except you have a concrete reference to the trait being implemented so you can resolve conflicts easily.
>
> Here's what I have so far. Please feel free to look at it and give any feedback.
>
> https://github.com/genericptr/freepascal/wiki/Traits-Proposal
Time for me to tackle this topic as well. First of, I might incorporate
answers to other mails of this thread here (directly or indirectly),
just so you know.
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:
Allow the extension of a class' declaration with preexisting behavior
AND state through the use of composition instead of inheritance,
providing this in a convenient and possible even transparent way to the
user of the class while allowing this to be implemented conveniently by
the author of the class.
Let's break this down and let's see whether we can naturally evolve
existing language features to cover this instead of trying to shoehorn
concepts of other languages for this. Obviously I know where I'm going
with this, but bear with me.
So the first part: "Allow the extension of a class' declaration with
preexisting behavior AND state through the use of composition instead of
inheritance". Obviously this is already possible in Object Pascal,
namely by using the interface delegation feature. For those that aren't
aware of the full capabilities of this feature, here is a small
excursion (if you think you already know all there is to know, jump
ahead to "end of the delegate excursion"):
The interface delegation feature allows the developer of a class to
redirect the implementation of a specific interface to a class or
interface instance provided in the class. A simple case looks like this:
=== code begin ===
type
ITest = interface
procedure Test;
end;
TTest = class(TObject, ITest)
private
fTest: ITest;
public
property TestImpl: ITest read fTest implements ITest;
end;
=== code end ===
This is however not the full ability of this feature. The property
implementing this interface does not need to be an interface. It can
also be a class instance and this class instance does *not* need to
declare that it implements this interface. So the following is a valid
interface delegation as well (Note: FPC does not support this currently,
that is still an outstanding Delphi compatibility problem):
=== code begin ===
type
ITest = interface
procedure Test;
end;
TTestImpl = class
procedure Test;
end;
TTest = class(TInterfacedObject, ITest)
private
fTest: TTestImpl;
public
property TestImpl: TTestImpl read fTest implements ITest;
end;
=== code end ===
In this case the methods introduced by IInterface are implemented by
TTest's parent class and ITest.Test is implemented by the TTestImpl
instance. If TTestImpl inherits from e.g. TInterfacedObject then TTest
does not need to inherit from TInterfacedObject itself (Note: the
reference counting in relation to interface delegation and aggregation
is a whole topic in and of itself and will be ignored here).
One can even control which methods should be implemented by the property:
=== code begin ===
type
ITest = interface
procedure Test1;
procedure Test2;
end;
TTestImpl = class
procedure Test1;
end;
TTest = class(TInterfacedObject, ITest)
private
fTest: TTestImpl;
public
procedure ITest.Test2 = MyTest2; // name can be chosen freely, can
also be "Test2"
property TestImpl: TTestImpl read fTest implements ITest;
procedure MyTest2;
end;
=== code end ===
So far this only covered behavior, but as interfaces can also provide
properties it's also possible to provide state through them (though they
*have* to be implemented through explicit Getters and Setters):
=== code begin ===
type
ITest = interface
function GetTestProp: LongInt;
procedure SetTestProp(aValue: LongInt);
property TestProp: LongInt read GetTestProp write SetTestProp;
end;
TTestImpl = class
function GetTestProp: LongInt;
procedure SetTestProp(aValue: LongInt);
end;
TTest = class(TInterfacedObject, ITest)
private
fTest: TTestImpl;
public
property TestImpl: TTestImpl read fTest implements ITest;
end;
=== code end ===
For using a delegated interface you need to cast the class instance to
the desired interface type:
=== code begin ===
var
t: TTest;
i: ITest;
begin
t := TTest.Create;
try
i := t as ITest;
i.Test1;
finally
t.Free;
end;
end.
=== code end ===
In all cases the compiler generates interface thunks that ensure that
the correct methods are executed (this is what needs to be implemented
in FPC to allow the use of class types instead of only interface types).
With this we have the end of the delegate excursion.
Thus the part of adding state and behavior can already be done through
interface delegation.
The next part: "providing this in a convenient and possible even
transparent way to the user of the class"
Now as we've seen above this is not the case right now with interface
delegation as one needs to cast to the interface type to allow this.
However there already is a way to allow for a more convenient use of a
property, namely the "default" modifier for indexed properties. Thus it
would be a logical extension to allow the "default" modifier for
"implements" as well. In this way the compiler would "hoist" the
methods/properties of the specified interface into the namespace of the
implementing class as well (while making sure that there is no ambigious
case of course).
=== code begin ===
type
ITest = interface
procedure Test;
end;
TTest = class(TObject, ITest)
private
fTest: ITest;
public
property Test: ITest 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 ===
This way we can easily provide even a transparent way for the user to
use a class with a delegated interface.
Now the last part: "while allowing this to be implemented conveniently
by the author of the class."
As seen above the class needs to declare that it provides an interface
and the "implements" property either needs to be a interface or class
instance.
Now the later could be solved rather easily thanks to class instances
already not having to implement the interface: one can easily allow
objects and records to "implement" such an interface through a
"implements" property as well:
=== 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.
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.
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.
So there you have it, my two cents (well, more like a few euros :P )
regarding the trait proposal.
Regards,
Sven
More information about the fpc-pascal
mailing list