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
_______________________________________________
fpc-pascal maillist  -  fpc-pascal@lists.freepascal.org
https://lists.freepascal.org/cgi-bin/mailman/listinfo/fpc-pascal

Reply via email to