Jump to content
PeterPanettone

Open Type Arrays?

Recommended Posts

Today in the shower I had an idea - wouldn't Open Type Arrays be a useful thing?

 

What if we could write:

 

procedure DoSomething(const AParam: [string, Integer]);
begin
  if AParam is string then
    ShowMessage(AParam)
  else if AParam is Integer then
    ShowMessage(IntToStr(AParam));
end;

One could argue that if we want flexibility we could use overloaded procedures.

 

But if we have procedures with many different parameters then we had to write a lot of overloaded procedures - Open Type Arrays would simplify things a lot.

 

What do you think?

Share this post


Link to post

This seems worse than method overloading to me. There you have compile time branching. Here you have to perform runtime type checking.

  • Like 5

Share this post


Link to post
56 minutes ago, David Heffernan said:

This seems worse than method overloading to me. There you have compile time branching. Here you have to perform runtime type checking.

Not necessarily. The compiler could generate a set of overloaded procedures for this.

  • Like 1

Share this post


Link to post

For this case will work with Variant.

procedure DoSomething(const AParam: Variant);
begin
  ShowMessage(AParam);
end;

 

  • Like 3

Share this post


Link to post

For dynamic language like typescript ... sure they're designed to work like that. Elsewhere (Delphi), this is going to be awful because you're checking the type at runtime, moreover you are mixing reference(string) with integer ... how are you expecting from the compiler to pass argument for your call (i.e 8 bytes and 4 bytes) ? Using union may solve the problem but still there is a drawback to check for the type. So I don't think it is a good idea at all.

 

Share this post


Link to post
3 hours ago, dummzeuch said:

Not necessarily. The compiler could generate a set of overloaded procedures for this.

You mean with the code from OP? 

  • Like 1

Share this post


Link to post
1 minute ago, David Heffernan said:

You mean with the code from OP? 

Yes

Share this post


Link to post
1 minute ago, dummzeuch said:

Yes

I would absolutely not want syntax like that. If that's the way you want to go you can do it today with generics.

 

In an ideal world I'd like to see the generality offered by C++ templates though. That would allow algorithmic programming that is just too painful with generics. 

 

 

Share this post


Link to post

 

3 hours ago, dummzeuch said:

Not necessarily. The compiler could generate a set of overloaded procedures for this.

And copy the method body in each?

Well, it could split that code and put the pieces into the appropriate overload.

 

On the other hand, that is exactly what a developer would do - and that is way more readable and maintainable (at least IMHO).

  • Like 3

Share this post


Link to post
Posted (edited)
4 hours ago, Cristian Peța said:

For this case will work with Variant.

Variant will accept everything and their dog which is hardly what is wanted here.

 

@PeterPanettone This is not in any form an array but as @david berneda rightly said a union. See https://en.wikipedia.org/wiki/Union_type

We already have such things in Delphi: TVarRec, Variant, TValue. However none of them can be specified to only accept a certain amount of types.

 

If you want something like that then write a custom record type with implicit operators and a TValue as internal storage (saves you from taking care of mixing unmanaged and managed types in a proper way)

 

Just for the lulz:

 

{$APPTYPE CONSOLE}

uses
  SysUtils, Rtti;

type
  Union = record
    fvalue: TValue;
    class operator Implicit(const value: string): Union;
    class operator Implicit(const value: Integer): Union;
    class operator Implicit(const value: Union): string;
    class operator Implicit(const value: Union): Integer;
    function IsType<T>: Boolean;
  end;

procedure DoSomething(const AParam: Union);
begin
  if AParam.IsType<string> then
    Writeln('string: ', string(AParam))
  else if AParam.IsType<Integer> then
    Writeln('integer: ', IntToStr(AParam));
end;

class operator Union.Implicit(const value: Integer): Union;
begin
  Result.fvalue := value;
end;

class operator Union.Implicit(const value: string): Union;
begin
  Result.fvalue := value;
end;

class operator Union.Implicit(const value: Union): string;
begin
  Result := value.fvalue.AsString;
end;

class operator Union.Implicit(const value: Union): Integer;
begin
  Result := value.fvalue.AsInteger;
end;

function Union.IsType<T>: Boolean;
begin
  Result := fvalue.IsType<T>;
end;

begin
  DoSomething(42);
  DoSomething('Hello world');
end.

 

If you want to go funky then make it a generic type with 2, 3, 4, ... generic type parameters 😉

Edited by Stefan Glienke
  • Like 2

Share this post


Link to post
2 hours ago, Uwe Raabe said:

Well, it could split that code and put the pieces into the appropriate overload.

Why make double work? The employer would like it if you need only half the time.

Share this post


Link to post
53 minutes ago, PeterPanettone said:

Why make double work? The employer would like it if you need only half the time.

It's not double the work. 

Share this post


Link to post

Just use Generics for this (see Testing the type of a generic in delphi), eg:

type
  TSomeClass = class
    class procedure DoSomething<T>(const AParam: T);
  end;

class procedure TSomeClass.DoSomething<T>(const AParam: T);
begin
  case GetTypeKind(T) of
    tkUString: begin
      ShowMessage(PUnicodeString(@AParam)^);
    end;
    tkInteger: begin
      ShowMessage(IntToStr(PInteger(@AParam)^));
    end;
  end;
end;

 

Share this post


Link to post
Posted (edited)
6 minutes ago, Remy Lebeau said:

Just use Generics for this

Even worse than Variant - you can pass anything to DoSomething now and it simply wont get handled (or raise an exception if you add that as else case)

Edited by Stefan Glienke
  • Like 1

Share this post


Link to post
54 minutes ago, Remy Lebeau said:

Just use Generics for this (see Testing the type of a generic in delphi), eg:


type
  TSomeClass = class
    class procedure DoSomething<T>(const AParam: T);
  end;

class procedure TSomeClass.DoSomething<T>(const AParam: T);
begin
  case GetTypeKind(T) of
    tkUString: begin
      ShowMessage(PUnicodeString(@AParam)^);
    end;
    tkInteger: begin
      ShowMessage(IntToStr(PInteger(@AParam)^));
    end;
  end;
end;

 

Nice professional code, compliments!

 

But why making something so complicated and prone to errors, if it could be SO EASY with Open Type Arrays?

Share this post


Link to post

This all seems like an over complication for little gain. Perhaps the OP could explain better what he is trying to achieve by this? 

 

As @David Heffernan said, this is worse than method overloading, and the need for that could be removed in many cases by allowing named parameter passing. 

 

IMHO, most uses of GetTypeKind in generic code is just making up for the lack of constraint types on delphi's generics. 

  • Like 4

Share this post


Link to post

@Stefan Glienke IsType<x> does not distinguish between distinct-type. And it could be a source of error. Using Overloaded function in some how can distinct at least if you specify the type explicitly:

type TMyString = type string;
procedure DoSomething2(const AParam: Integer); overload;
begin
  Writeln('this is a integer :', AParam);
end;

procedure DoSomething2(const AParam: string); overload;
begin
  Writeln('this is a string :', AParam);
end;

procedure DoSomething2(const AParam: TMyString); overload;
begin
  Writeln('this is a TMyString :', AParam);
end;

var
  a: Integer;
  s: string;
  m: TMyString;

begin
  s := 'string';
  m := 'mystring';
  DoSomething(a);
  DoSomething(s);
  DoSomething(m);

  DoSomething2(a);
  DoSomething2(s);
  DoSomething2(m);
  Readln;
end.

 

 

Share this post


Link to post
18 minutes ago, Mahdi Safsafi said:

@Stefan Glienke IsType<x> does not distinguish between distinct-type. And it could be a source of error. Using Overloaded function in some how can distinct at least if you specify the type explicitly:


type TMyString = type string;
procedure DoSomething2(const AParam: Integer); overload;
begin
  Writeln('this is a integer :', AParam);
end;

procedure DoSomething2(const AParam: string); overload;
begin
  Writeln('this is a string :', AParam);
end;

procedure DoSomething2(const AParam: TMyString); overload;
begin
  Writeln('this is a TMyString :', AParam);
end;

var
  a: Integer;
  s: string;
  m: TMyString;

begin
  s := 'string';
  m := 'mystring';
  DoSomething(a);
  DoSomething(s);
  DoSomething(m);

  DoSomething2(a);
  DoSomething2(s);
  DoSomething2(m);
  Readln;
end.

 

 

Really nice code!

 

However, your employer has to pay for the time you need to write overloaded code. (Though I assume you're a fast writer).

Share this post


Link to post
Posted (edited)
6 hours ago, PeterPanettone said:

However, your employer has to pay for the time you need to write overloaded code. (Though I assume you're a fast writer). 

Depends. I added only 15K LOC last year to my project. A lot of time I spend thinking how to add features asked by customers to actual app logic. This doesn't have to do with old code but to old app logic and not to break old behavior. An to leave space for feature improvements because this is a 20 years old project.

Edited by Cristian Peța
  • Like 3

Share this post


Link to post
Posted (edited)

As @Stefan Glienke has pointed out, we have TVarRec, Variant and TValue to deal with situation of:

 

  • The type of the parameter is uncertain

 

But in addition to the above situation, open array parameters are designed to deal with even more complex situation:

  • Both the types and number of parameters are uncertain. The most well-known case is the Format function.

 

However, I think the OP is heading to a wrong direction to use open array parameters for simplifying the example code he's given. There is at least one better solution:

procedure DoSomething(const AParam: string); overload;
begin
  // procedure body here
end;

procedure DoSomething(const AParam: Integer); overload;
begin
  DoSomething(IntToStr(aParam));
end;

The overloaded procedures eliminate the use of the `if` statement and make the code simpler and easier to read.

Just my 2 cents.

 

Edit 1: Please note, 'less lines of code' <> simple, 'more lines of code' <> complexity

Edit 2: One more benefit of my above alternative method - More user friendly to the clients (that uses the DoSomething procedures). Because the clients can see exactly the type of the parameters to provide, and this is exactly the advantage of statically typed languages over dynamically typed languages such as JS .

Edited by Edwin Yip
  • Like 5

Share this post


Link to post

I am trying to see a proper use case for this, as the example is too simplistic to make sense.

What is the problem that needs to be solved?

What is the duplicate work that needs to be eliminated?

  • Like 1

Share this post


Link to post
8 hours ago, PeterPanettone said:

However, your employer has to pay for the time you need to write overloaded code. (Though I assume you're a fast writer).

In the OP you also write an implementation for each different type. So where is the saving? 

  • Like 4

Share this post


Link to post
10 hours ago, Mahdi Safsafi said:

IsType<x> does not distinguish between distinct-type. And it could be a source of error. Using Overloaded function in some how can distinct at least if you specify the type explicitly:

Then make it do a strict type check comparing with TValue.TypeInfo... also method overloading and distinct types is a source of errors in itself because they are always assignment compatible with their base type so that does not count as argument. I can also pass a TFileName, a TCaption or whatever to your DoSomething2 and it will happily go into the string overload.

 

BTT if Delphi was more functional I could very well see more usecases for this in the form of monads like the maybe or either type because they can very well solve a lot of unnecessary cyclomatic complexity

  • Like 1

Share this post


Link to post

I wonder if there is trick(s) to make this works :classic_biggrin:

type
  TRecSI = record
    FInteger: Integer;
    FString: string;
    constructor Create(const s: string; i: Integer);
  end;

constructor TRecSI.Create(const s: string; i: Integer);
begin
  FInteger := i;
  FString := s;
end;

procedure DoSomething(var Anything);
var
  T: TTypeKind;
begin
  T := GetTypeKind(Anything);	// ??????????
  case T of
    tkInteger:
      Writeln(Integer(Anything));
    ///
  end;
end;

var
  A: Integer;
  B: string;
  C: array of Byte;
  D: array of Integer;
  E: array of string;

begin
  DoSomething(A);
  DoSomething(B);
  DoSomething(C);
  DoSomething(D);
  DoSomething(E);
  //DoSomething(TRecSI.Create('Hello',1234));    // or make this work

  Readln;
end.

 

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×