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
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
...
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
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
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?
ReplyDeleteDavid 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.
ReplyDeleteThat every single enum has its own helper.
ReplyDeleteSince 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.
ReplyDeleteCould you exemplify the generic alternative?
Something along these lines
ReplyDeletetype
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;
That is a useful class, but it doesn't fill the need for us, since the ToString is not the only method.
ReplyDeleteShortDisplayName, 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?
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.
ReplyDeletefunction 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;
T n T - In your opinion - what are the issues with helpers?
ReplyDeleteT n T What is the rest of your specification? Specifically, how are multple matches resolved?
ReplyDeleteI'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).
ReplyDeleteIt'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))
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.
ReplyDeleteHelpers (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.
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.
ReplyDeleteDavid 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.
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.
ReplyDeleteT n T Which ever one was most recently declared? Not very intuitive.
ReplyDelete+Lars Fosdal No more tricky than using of the global routines today. Thank the gods Delphi is a strongly typed language.
ReplyDelete+David Heffernan Same as using of overloaded global routines. It's funny that Shaun Roselt posted a video with a demonstration of overloaded functions.
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?
ReplyDeleteIt'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.
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).
ReplyDeleteDavid 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.
ReplyDeleteLars Fosdal The same problem with helpers. The code:
ReplyDeleteunit 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.