[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