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
https://quality.embarcadero.com/browse/RSP-16799

Comments

  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?

    ReplyDelete
  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.

    ReplyDelete
  3. That every single enum has its own helper.

    ReplyDelete
  4. 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?

    ReplyDelete
  5. 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;

    ReplyDelete
  6. 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?

    ReplyDelete
  7. 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;

    ReplyDelete
  8. T n T - In your opinion - what are the issues with helpers?

    ReplyDelete
  9. T n T  What is the rest of your specification? Specifically, how are multple matches resolved?

    ReplyDelete
  10. 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))

    ReplyDelete
  11. 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.

    ReplyDelete
  12. 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.

    ReplyDelete
  13. 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.

    ReplyDelete
  14. T n T  Which ever one was most recently declared? Not very intuitive.

    ReplyDelete
  15. +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.

    ReplyDelete
  16. 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.

    ReplyDelete
  17. 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).

    ReplyDelete
  18. 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.

    ReplyDelete
  19. 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.

    ReplyDelete

Post a Comment