Wish: Interface support for record/class helpers

Wish: Interface support for record/class helpers

Scenario: We are adding record helpers for all our enumerated types to reduce code clutter and enhance readability

type

TMyEnum = (A, B C);

TMyEnumHelper = record helper for TMyEnum

function ToString:string;

function Description: string;

end;

The old pattern was to create

function TMyEnumValueToString(const Value: TMyEnum):String;

function TMyEnumDescription(const Value: TMyEnum):String;

in addition to the enumerated type.

Rather than having to write

var

Code: TMyEnum;

begin

Writeln(TMyEnumValueToString(Code), ‘ ‘, TMyEnumDescription(Code));

We can now write

var

Code: TMyEnum;

begin

Writeln(Code.ToString, ‘ ‘, Code.Description);

which reads a lot better.

But – For unit testing, this creates another predicament

We have used an RTTI trick to create a loop for each value in anon.type T for enumerations, and hence had a

type

TEnumTester

type

reference to function EnumToString(const Value: T):String;

end;

But there is no such way to “genericize” a helper method.

type

IEnumHelper = interface

function ToString:string;

function Description: string;

end;

I can’t declare an interface which a record helper have to adher to, as there is no syntax to do so, and I can’t constrain the generic type to match that interface

My suggestion would be to allow something like

type

TMyEnumHelper = record helper for (TMyEnum, IEnumHelper)

function ToString:string;

function Description: string;

end;

TEnumTester = class

end;

and be able to declare

var

TestMyEnum: TEnumTester;

Today the interface constraint fails with

[dcc32 Error] : E2514 Type parameter ‘T’ must support interface ‘IEnumHelper’

since the enumerated type is not recognized as having support for the interface.

I think that would have been helpful – as would a generics constraint for enumerated types be.

Quality Portal Issue RSP-16799

https://quality.embarcadero.com/browse/RSP-16799

19 thoughts on “Wish: Interface support for record/class helpers


  1. Every single enumerated type has identical boilerplate? That doesn’t sound very good. Why don’t you just use an abstract class with generic methods to operate on enumerated types. Then you can throw away all the boilerplate?


  2. David Heffernan Which boilerplate are you referring to? The conversions or the testing of the conversions? The example has been simplfied a bit, since there also is the complexity of language and translations involved.


  3. Since every enum needs its own translations, and performance is an issue, this was what we went for. It also helps that it gives you code completion.


    Could you exemplify the generic alternative?


  4. Something along these lines


    type


    TEnumeration = class


    strict private


    class function TypeInfo: PTypeInfo; inline; static;


    class function TypeData: PTypeData; inline; static;


    public


    class function IsEnumeration: Boolean; static;


    class function ToOrdinal(Enum: T): Integer; inline; static;


    class function FromOrdinal(Value: Integer): T; inline; static;


    class function ToString(Enum: T): string; inline; static;


    class function FromString(const Value: string): T; inline; static;


    class function MinValue: Integer; inline; static;


    class function MaxValue: Integer; inline; static;


    class function InRange(Value: Integer): Boolean; inline; static;


    class function EnsureRange(Value: Integer): Integer; inline; static;


    end;


    { TEnumeration }


    class function TEnumeration.TypeInfo: PTypeInfo;


    begin


    Result := System.TypeInfo(T);


    end;


    class function TEnumeration.TypeData: PTypeData;


    begin


    Result := TypInfo.GetTypeData(TypeInfo);


    end;


    class function TEnumeration.IsEnumeration: Boolean;


    begin


    Result := TypeInfo.Kind=tkEnumeration;


    end;


    class function TEnumeration.ToOrdinal(Enum: T): Integer;


    begin


    Assert(IsEnumeration);


    Assert(SizeOf(Enum)<=SizeOf(Result));


    Result := 0; // because we may have SizeOf(Enum) < SizeOf(Result)


    Move(Enum, Result, SizeOf(Enum));


    Assert(InRange(Result));


    end;


    class function TEnumeration.FromOrdinal(Value: Integer): T;


    begin


    Assert(IsEnumeration);


    Assert(InRange(Value));


    Assert(SizeOf(Result)<=SizeOf(Value));


    Move(Value, Result, SizeOf(Result));


    end;


    class function TEnumeration.ToString(Enum: T): string;


    begin


    Assert(IsEnumeration);


    Result := GetEnumName(TypeInfo, ToOrdinal(Enum));


    end;


    class function TEnumeration.FromString(const Value: string): T;


    var


    Ordinal: Integer;


    begin


    Assert(IsEnumeration);


    Ordinal := GetEnumValue(TypeInfo, Value);


    if not InRange(Ordinal) then begin


    raise EInvalidCast.CreateRes(PResStringRec(@SInvalidCast));


    end;


    Result := FromOrdinal(Ordinal);


    end;


    class function TEnumeration.MinValue: Integer;


    begin


    Assert(IsEnumeration);


    Result := TypeData.MinValue;


    end;


    class function TEnumeration.MaxValue: Integer;


    begin


    Assert(IsEnumeration);


    Result := TypeData.MaxValue;


    end;


    class function TEnumeration.InRange(Value: Integer): Boolean;


    var


    ptd: PTypeData;


    begin


    Assert(IsEnumeration);


    ptd := TypeData;


    Result := Math.InRange(Value, ptd.MinValue, ptd.MaxValue);


    end;


    class function TEnumeration.EnsureRange(Value: Integer): Integer;


    var


    ptd: PTypeData;


    begin


    Assert(IsEnumeration);


    ptd := TypeData;


    Result := Math.EnsureRange(Value, ptd.MinValue, ptd.MaxValue);


    end;


  5. That is a useful class, but it doesn’t fill the need for us, since the ToString is not the only method.


    ShortDisplayName, LongDisplayName, Description. Some enums have other functions as well, related to logic or subtyping (.IsAllowedWith(const v: TSomeOtherType))


    An interface would give a formalized way of confirming that a helper has the necessary methods, and write generic test wrappers without needing individual stubs to adapt to translation methods for each enumerated type.


    var


    Code: TMyEnum;


    begin


    Writeln(Code.ToString, ‘ ‘, Code.Description);


    How would the generic example look for the above snippet?


  6. I don’t think they should expand the helpers. Actually I think they should not have introduced the helpers to the language at all. The same behavior could be achieved by extending of a calling convention of global routines. For ex.


    function ToString(const Value: Integer):String; overload;


    function ToString(const Value: TMyEnum):String; overload;


    function ToString(const Value: ):String; overload;


    Allow to call as


    255.ToString;


    Code.ToString;


    Obj.ToString;


  7. I’ve done the same thing as you Lars Fosdal​. Yes it leads to some boilerplate but I prefer the .ToString syntax rather to Enumeration.ToString(value).


    It’s needed because using Rtti you can only hope to get the name of the declaration, but what if you need more like Lars pointed


    Also IRC Enumeration that you give a value don’t have Rtti (TMyEnum = (First = 3))


  8. T n T generic types are very limited in terms of available constraints and what you can do with them. Your solution would be very limited and inferior one comparing to helpers.


    Helpers (extensions) are common in many languages and they are extremely useful feature. Comparing to those, the most prominent limitation of Delphi helpers is there can be only one in scope.


  9. Lars Fosdal I have no problems with helpers. I just showed the way how to call thousands of previously written subroutines as if they are class methods. If it were implemented, there would be no need to introduce helpers.


    David Heffernan Delphi already has a mechanism of resolving of multiple matches.


    Dalija Prasnikar In some languages it’s impossible to define a global routine, only a class member.


  10. T n T That is an interesting idea, but scope control could be tricky, particularly for derivative types such as range limited integer types, array and string types.


  11. +Lars Fosdal No more tricky than using of the global routines today. Thank the gods Delphi is a strongly typed language.


    +David Heffernan Same as using of overloaded global routines. It’s funny that Shaun Roselt posted a video with a demonstration of overloaded functions.


  12. T n T You miss the point. What if I have two global functions with the same name and signature in two different units. Which one is used?


    It’s a completely different thing from overload resolution because that requires all functions with the same name to have unique signatures. You cannot impose the same thing on global functions.


    So, nice try, but back to the drawing board to work up your specification a bit further.


  13. T n T  I agree with David Heffernan. Reording the units in the uses clause could cause you to use the wrong function, and you would not even be aware that it happened (at compile time).


  14. David Heffernan “Which ever one was most recently declared” – you’ve already answered to this question. It’s how Delphi resolves ambiguities now. Perhaps they should add a warning if there are two global routines with the same name and parameters in different units.


  15. Lars Fosdal The same problem with helpers. The code:


    unit Unit2;


    interface


    type


    TIntegerHelper = record Helper for Integer


    function ToString: string;


    end;


    implementation


    uses SysUtils;


    function TIntegerHelper.ToString: string;


    begin


    Result := IntToStr(Self) + ‘1’; //!!


    end;


    unit Unit3;


    interface


    type


    TIntegerHelper = record Helper for Integer


    function ToString: string;


    end;


    implementation


    uses SysUtils;


    function TIntegerHelper.ToString: string;


    begin


    Result := IntToStr(Self) + ‘2’; //!!


    end;


    Using:


    uses Unit2, Unit3;


    //uses Unit3, Unit2;


    procedure TForm1.Button1Click(Sender: TObject);


    begin


    ShowMessage(Integer(123).ToString);


    ShowMessage(123.ToString);


    end;


    If you revert Unit2, Unit3 in the uses clause – you will get different result. And ShowMessage(123.ToString); will call helper for the Byte type.

Leave a Reply