default properties

From Lazarus wiki
Revision as of 11:45, 17 December 2018 by Ryan joseph (talk | contribs) (Created page with "Default properties allow "hoisting" or otherwise exposing of object members into the caller name space to facilitate wrapper types and (possibly) delegation patterns. This fe...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigationJump to search

Default properties allow "hoisting" or otherwise exposing of object members into the caller name space to facilitate wrapper types and (possibly) delegation patterns.

This feature is under developement and everything is subject to change.

Download development fork:

https://github.com/genericptr/freepascal/tree/defaultprops

Supports:

Precedence rules:

Precedence order for names is as follows: base, default (last to first).

 type
   THelper = class
     num: integer;
     procedure DoThis; overload;
     procedure DoThis (param: integer); overload;
     procedure DoThis (param: string); overload;
   end;
 
 procedure THelper.DoThis;
 begin
   writeln('THelper.DoThis:',num);
 end;
 
 procedure THelper.DoThis (param: integer);
 begin
   writeln('THelper.DoThis:',param,':',num);
 end;
 
 procedure THelper.DoThis (param: string);
 begin
   writeln('THelper.DoThis:',param,':',num);
 end;
 
 type
   TParent = class
     m_helper: THelper;
     property helper: THelper read m_helper; default;
     procedure DoThis (param:single);overload;
   end;
 
 procedure TParent.DoThis (param:single);
 begin
   writeln('TParent.DoThis:',param:1:1);
 end;
 
 type
   TMyObject = class (TParent)
     procedure Call;
     procedure DoThis;overload;
   end;
 
 procedure TMyObject.DoThis;
 begin
   writeln('TMyObject.DoThis');
 end;
 
 procedure TMyObject.Call;
 begin
   DoThis(100);      // THelper.DoThis
   DoThis(10.5);     // TParent.DoThis
   DoThis('hello');  // THelper.DoThis
   DoThis;           // TMyObject.DoThis
 end;
 
 var
   obj: TMyObject;
 begin
   obj := TMyObject.Create;
   obj.m_helper := THelper.Create;
   obj.Call;
 end.

Operator overloads are possible also. Precedence can get complicated considering it's possible to have overloads in the current unit for the default type as well as overloads in the structure itself.

 operator + (l : integer; r : ansistring): integer;
 begin  
   result := l + StrToInt(r);  
 end;
 
 type
   TWrapper = record
     m_value: integer;
     property value: integer read m_value write m_value; default;
   end;
 
 var
   wrapper: TWrapper;
 begin
   wrapper += 1;               // default property takes precedence
   wrapper += '128';           // unit + operator takes precedence because default property type (integer) doesn't support string  overloads
   writeln(wrapper.value);     // 129
 end.

Type helpers behave as normal on the default type.

 type
   TIntegerHelper = type helper for integer
     function Str: string;
   end;
 
 function TIntegerHelper.Str: string;
 begin
   result := 'string->'+IntToStr(self);
 end;
 
 type
   TWrapper = record
     m_value: integer;
     property value: integer read m_value write m_value; default;
   end;
 
 var
   wrapper: TWrapper;
 begin
   wrapper := 100;
   writeln(wrapper.str);
 end.

Assignments:

Default properties introduce an ambiguity with assignments.

 type
   TWrapper = class
     m_value: integer;
     m_string: string;
     property value: integer read m_value write m_value; default;
     property str: string read m_string write m_string; default;
   end;
 
 var
   wrapper: TWrapper;
 begin
   wrapper := TWrapper.Create;   // TWrapper is same type so assignment is as normal
   wrapper := 100;               // integer matches "value"
   wrapper := 'hello world';     // string matches "str"
   writeln(wrapper.value);
   writeln(wrapper.str);
 end.

Multidimensional:

Currently during development while the concept is being explored default properties are "multidimensional", meaning you can declare more than one per type.

They have been developed this way from the outset because the concept of exposing namespaces is inherently a multidimensional concept. Limiting a structure to only one would be an articial limit impossed by the compiler so at least for development I wanted to keep the option available while the concept is being explored.

This is of course a highly contested idea given the capacity to introduce difficult to understand precednece rules and method calling.

Examples of usages:

Nullable types:

 type
  generic TNullable<T>= record
    private
      FValue : T;
    public
      isAssigned : boolean;
      function IsNull: boolean;
      procedure SetValue (newValue: T); 
      Property Value : T read FValue write SetValue; default;
      class operator Initialize(var a: TNullable);
    end;
 
 procedure TNullable.SetValue (newValue: T); 
 begin
   FValue := newValue;
   isAssigned := true;
 end;
 
 function TNullable.IsNull: boolean;
 begin
   result := not isAssigned;
 end;
 
 class operator TNullable.Initialize(var a: TNullable);
 begin
   a.isAssigned := Default(T);
 end;
 
 type
   TBoolean = specialize TNullable<boolean>;
 Var
   bool: TBoolean;
 begin
   bool := true;
   if bool then
     writeln('bool is true');
 end.

Auto managed classes using management operators:

 type
   generic TAuto<T> = record
     private
       m_object: T;
       class operator Initialize(var a: TAuto);
       class operator Finalize(var a: TAuto);
     public
       property obj: T read m_object; default;
   end;
 
 type
   TStringList = specialize TFPGList<String>;
   TStringListAuto = specialize TAuto<TStringList>;
 
 class operator TAuto.Initialize(var a: TAuto);
 begin
   a.m_object := T.Create;
 end;
 
 class operator TAuto.Finalize(var a: TAuto);
 begin
   a.m_object.Free;
 end;
 
 var
   list: TStringListAuto;
   str: string;
 begin
   list.Add('foo');
   list.Add('bar');
   for str in list do
     writeln(str);
 end.

Array indexing

Because it is technically possible arrays are exposed using default properties. This however doesn't conflict with the existing [] indexer properties.

 type
   TIntArray = array of integer;
   TWrapper = record
     m_value: TIntArray;
     property value: TIntArray read m_value write m_value; default;
   end;
 
 var
   wrapper: TWrapper;
   i: integer;
 begin
   wrapper := TIntArray.Create(100,200,300);  // NOTE: we can't use array constructors [] due to bug in compiler
   wrapper[0] += 50;
   wrapper[1] += 1;
   for i in wrapper do
     writeln(i);
 end.

Modules/Delegation

If delegate properties are allowed be multidimensional in the final version they can be used for delegation patterns that would otherwise only be possible with multiple inheritance (in languages such as C++, Java).

 
 // Stats:
 
 type
   TStats = record
     hp: integer;
     exp: integer;
     procedure InitStats;
   end;
 
 // Base Module:
 
 type
   TEntity = class;
   TBaseHandler = class
     entity: TEntity;
     constructor Create (inEntity: TEntity);
     // NOTE: this is a dangerous idea because it will be convered
     procedure Clear;
   end;
 
 // Physics Module:
 
   TPhysicsHandler = class (TBaseHandler)
     pos: TVec3;
     acc: TVec3;
     vel: TVec3;
     procedure Integrate; virtual;
   end;
 
 // Renderer Module:
 
   TRendererHandler = class (TBaseHandler)
     procedure Render; virtual;
   end;
 
 // Entity:
 
   TEntity = class
     private
       m_physics: TPhysicsHandler;
       m_renderer: TRendererHandler;
       m_stats: TStats;
     public
       property physics: TPhysicsHandler read m_physics; default;
       property renderer: TRendererHandler read m_renderer; default;
       property stats: TStats read m_stats; default;
     public
       procedure Update;
   end;
 
 // Implemention:
 
 type
   TMonster = class (TEntity)
     public
       procedure AfterConstruction; override;
   end;
 
 type
   TMonsterRenderer = class (TRendererHandler)
     procedure Render; override;
   end;
 
 procedure TStats.InitStats;
 begin
   hp := 100;
   exp := 0;
 end;
 
 procedure TBaseHandler.Clear;
 begin
   writeln(classname,' clear');
 end;
 
 constructor TBaseHandler.Create (inEntity: TEntity);
 begin
   entity := inEntity;
 end;
 
 procedure TPhysicsHandler.Integrate;
 begin
   pos.x += vel.x;
   pos.y += vel.y;
   pos.z += vel.z;
 end;
 
 procedure TRendererHandler.Render;
 begin
 end;
 
 procedure TEntity.Update;
 begin
   Integrate;
   Render;
 end;
 
 procedure TMonster.AfterConstruction;
 begin
   m_physics := TPhysicsHandler.Create(self);
   m_renderer := TMonsterRenderer.Create(self);
 
   InitStats;
 
   vel.x := 80;
   vel.y := 20;
   vel.z := 0;
 end;
 
 procedure TMonsterRenderer.Render;
 begin
   writeln('render monster at ', entity.pos.str, ' hp:', entity.hp);
 end;
 
 var
   entity: TMonster;
   i: integer;
 begin
   entity := TMonster.Create;
   entity.Clear;
   for i := 0 to 2 do
     entity.Update;
 end.

Default Implements Properties

"implements" properties are more complete by exposing their namespace using "default". If multiple defaults are allowed one could imagine this being a possible method to implement multiple inheritence in Pascal.

 type
   IWrapper = interface ['IWrapper']
     procedure DoThis;
   end;
 
 type
   TWrapper_Handler = class (IWrapper)
     procedure DoThis;
   end;
 
 procedure TWrapper_Handler.DoThis;
 begin
   writeln('TWrapper_Handler.DoThis');
 end;
 
 type
   TWrapper = class (IWrapper)
     m_wrapper: TWrapper_Handler;
     property handler: TWrapper_Handler read m_wrapper implements IWrapper; default;
     procedure AfterConstruction; override;
   end;
 
 procedure TWrapper.AfterConstruction;
 begin
   m_wrapper := TWrapper_Handler.Create;
 end;
 
 procedure HandleWrapper (wrapper: IWrapper);
 begin
   wrapper.DoThis;
 end;
 
 var
   wrapper: TWrapper;
 begin
   wrapper := TWrapper.Create;
   HandleWrapper(wrapper);
 end.