Recent

Author Topic: Nested Generic (or generic collection of generics)  (Read 604 times)

nothratal

  • Newbie
  • Posts: 3
Nested Generic (or generic collection of generics)
« on: September 16, 2022, 05:06:05 pm »
Hello together,

if the subject ob this topic is really unclear, then probably because I haven't any clues about how to name this problem I have - which also is my excuse for not finding it in the forum  :P
I'm doing some coding exercises and I came up with the following scenario:
In the future I would like to store data of unkown simple types in an unkown data collection. To make the work easier, I would like to create a function to print them in an easy way.

I had this here in mind:
Code: Pascal  [Select][+][-]
  1. type
  2.   TNumbersL = specialize TFPGList<integer>;
  3.   TNumbersVec = specialize TVector<integer>;
  4.  
  5.   generic function prettyFormat<Iterable, T>(vec: specialize Iterable<T>): string;
  6.   var
  7.     i: T;
  8.  
  9.   begin
  10.     Result := '[';
  11.  
  12.     for i in vec do
  13.     begin
  14.       Result := Result + i.ToString() + ', ';
  15.     end;
  16.     Result := Result.remove(Result.length - 2);
  17.  
  18.     Result := Result + ']';
  19.   end;
  20.  
  21. var
  22.   numbersL: TNumbersL;
  23.   numbersVec: TNumbersVec;
  24.   n: integer;
  25. begin
  26.   numbersL := TNumbersL.Create();
  27.   numbersVec := TNumbersVec.Create();
  28.  
  29.   for n := 0 to 10 do
  30.   begin
  31.     numbersL.Add(n);
  32.     numbersVec.PushBack(n);
  33.   end;
  34.  
  35.   writeln(specialize prettyFormat<TNumbersL, integer>(numbersL));
  36.  
  37. end.    
  38.  

The issue I have is, that the compiler doesn't allow/understand Iterable<T>. I can work with only one generic type, but not with two...or I didn't get how. The error message I get is:
project1.lpr(18,72) Error: Identifier not found "Iterable$1"

Any ideas?

PascalDragon

  • Hero Member
  • *****
  • Posts: 5481
  • Compiler Developer
Re: Nested Generic (or generic collection of generics)
« Reply #1 on: September 16, 2022, 05:36:12 pm »
The compiler has no way of knowing that Iterable can be a generic thus you can't use it like a generic. Also since you pass TNumbersL where you call prettyFormat<,> that would in the result in specialize TNumbersL<integer> which is obviously wrong. You'll need to pass the list as is:

Code: Pascal  [Select][+][-]
  1. generic function prettyFormat<TList, TElem>(vec: TList): string;
  2. // …

However this will then result in the compiler complaining about the i.ToString, because it won't know that a helper might be applicable. So you'll need to find a different solution for that.

Arioch

  • Sr. Member
  • ****
  • Posts: 421
Re: Nested Generic (or generic collection of generics)
« Reply #2 on: September 16, 2022, 07:46:56 pm »
Sounds like what Visitor pattern was designed for, in pure pre-generic OOP ?  like this Delphi series - https://www.uweraabe.de/Blog/2010/08/16/the-visitor-pattern-part-1/

I'd separate concerns here and try make some generic syntax sugar on top of it later

1. make different functions to enumerate, to convert to string (HTML? RTF?, and to print the resulting string (or save to file, or upload to server, or copy to clipboard)
2. make some registry of methods, like LCL's RegisterClass
3. make plumbing funcitons to "bring thi all together" in easy to call way

When all types are known in advance you do not even need .ToString, you have class operator implicit (in delphi terms) or operator := (in FPC terms) and then just typecast to string

You can register those in class constructor for classes, but for other types there seem to be no unified place, sans initialization section of units

Code: Pascal  [Select][+][-]
  1. type
  2.    // simulating TPrettyFormatter<T> = reference to function(const Value: T): string;
  3.    TPrettyFormatter<T> = class
  4.       public class function Format(const Value: T): string; virtual; abstract;
  5.    end;
  6.  
  7. Var
  8.    GlobalFormatters: TDictionary<Pointer, TClass>;
  9.  
  10. funciton GetGlobalFormatter(const ValueTypeInto: Pointer): TClass;
  11.  
  12. // pointer should be TypeInfo(T), classes and interfaces should be manually traversed back to parent types
  13.  

Having it so, you then can have the same API for items and the container, because the container is also someone's item

Code: Pascal  [Select][+][-]
  1. type
  2.   TMyListFormatter = class( TPrettyFormatter<TMyList<TMyItem>>)
  3.       public class function Format(const Value: TMyList): string; override;
  4.    end;
  5.  
  6. function TMyListFormatter.Format(const Value: TMyList<TMyItem>): string;
  7. var i: TMyItem; sl: iJclStringList;
  8.       if: TPrettyFormatter<TMyItem>;  
  9.       if_C: TClass aboslute if;
  10. begin
  11.    Result := ''; // Result is var parameter and already has the value from callee
  12.    if Count <= 0 then exit;
  13.  
  14.    if_C := GetGlobalFormatter(TypeInto(TMyItem));
  15.    if nil = if_C then exit;
  16.  
  17.    sl := JclStringList();
  18.    for i in Self do
  19.      sl.Add( if.Format(i) );
  20.  
  21.    Result := sl.Join( #13#10 );
  22. end;
  23.  

Now, on top of that you can make some

Code: Pascal  [Select][+][-]
  1. // should it be overload-marked to avoidlash with generic functions in future?
  2. // implementation is to be about like the above foir the list
  3. function PrettyFormatValue(var Value; Const ValueTypeInfo: pointer): string; forward;
  4.  
  5. // and then one day also
  6.  
  7. function PrettyFormatValue<T>(const Value: T): string; inline;
  8. begin
  9.    Result := PrettyFormatValue(Value, TypeInfo(T));
  10. end;
  11.  

and then you can have any number of consumers

Quote
type
   TValueConsumer = reference to procedure(var Value; Const ValueTypeInfo: pointer);
   TValueConsumer<T> = TProc<T>;


procedure PrintToShowMessage(var Value; Const ValueTypeInfo: pointer); ...
procedure CopyToClipboard(var Value; Const ValueTypeInfo: pointer); ...
procedure DumpToTextFile(var Value; Const ValueTypeInfo: pointer); ...

And then you can make, for example, TObject class helper "adding" a method TObject.PrintToShowMessage that would equally work called on TMyItem and on TMyList<TMyItem>

Granted, such a pattenr works much better in "rooted" type systems like in Java, where "everything is object".

nothratal

  • Newbie
  • Posts: 3
Re: Nested Generic (or generic collection of generics)
« Reply #3 on: September 20, 2022, 12:29:06 pm »
@Arioch:
I cannot say that I've ever knowingly heard of the visitor pattern, but the java version looks more familiar. As I understood I still need to  "register" a Formatter for each class I want to visit, right? It looks a bit to much for what I wanted to achieve. Maybe I didn't understood it right.

@PascalDragon
You were right with the parameter. After fixing it, fpc did compile and execute it properly.

So now it looks like this:
Code: Pascal  [Select][+][-]
  1. type
  2.   TNumbersL = specialize TFPGList<integer>;
  3.   TNumbersVec = specialize TVector<integer>;
  4.  
  5.   generic function prettyFormat<Iterable, T>(vec: Iterable): string;
  6.   var
  7.     i: T;
  8.  
  9.   begin
  10.     Result := '[';
  11.  
  12.     for i in vec do
  13.     begin
  14.       Result := Result + i.ToString + ', ';
  15.     end;
  16.     Result := Result.remove(Result.length - 2);
  17.  
  18.     Result := Result + ']';
  19.   end;  
  20.  
  21. var
  22.   numbersL: TNumbersL;
  23.   numbersVec: TNumbersVec;
  24.   n: integer;
  25.  
  26. begin
  27.   numbersL := TNumbersL.Create();
  28.   numbersVec := TNumbersVec.Create();
  29.  
  30.   for n := 0 to 10 do
  31.   begin
  32.     numbersL.Add(n);
  33.     numbersVec.PushBack(n);
  34.   end;
  35.  
  36.   writeln(specialize prettyFormat<TNumbersL, integer>(numbersL));
  37.   writeln(specialize prettyFormat<TNumbersVec, integer>(numbersVec));
  38.  
  39. end.
  40.  
  41.  

For me this is the most attractive solution. But somehow it's still a bit unsatisfying. If I pass TNumbersL/TNumbersVec it's certain that items are of type integer, so passing them again via <> seems a bit unnecessary. But for now it's ok :)


Arioch

  • Sr. Member
  • ****
  • Posts: 421
Re: Nested Generic (or generic collection of generics)
« Reply #4 on: September 20, 2022, 12:46:12 pm »
@Arioch: As I understood I still need to  "register" a Formatter for each class I want to visit, right?

In "visitor" yes, because the idea of it is exactly avoiding coding in implementations into the class itself.

if you do - you can have TObject.ToString virtual - and then just use it. But that is exactly what you seek to avoid for a number of reasons.

then, if you create formatters outside of the target class (or other data type) - you indeed have to register them some way.
if nothing else - because otherwise Smart Linker would probably eliminate them like a code no one calls :-)

May You hope for type inferenece to do job for you. Personally i gave up on that in Delphi after trying to run VTV forks working without "record helpers" abomination. Delphi gave up on enhancing it and perhaps so did i. Maybe FPC is noticeably better in this department, dunno.

But i really have issue with your initial
Code: Pascal  [Select][+][-]
  1.     for i in vec do
  2.     begin
  3.       Result := Result + i.ToString() + ', ';

I believe if you do such an infrastructure - it better be self-sufficient and not rely on TObject.ToString which you seek to replace.
You have to call prettyFormat on "i" too.
And then "i" can, inside itself, call prettyFormat on it's inner memeber, etc, in uniform way.

Also to me the need to write boilerplate like "writeln(specialize prettyFormat<TNumbersL, integer>(numbersL));" on every call looks worse than need to write boilerplate of "RegisterFormatter" once.
To me it is equal to have ad hoc register at eveyr call site.
To me you should just call Writeln(prettyFormat(numbersL)); and the rest had to be inferred by the compiler (including "specialize", yes :-) ). And if it won't - to me that kills the whole idea.

Your boilerplate has the advantage of being computed in compile-time, rather than runtime dictionary look-ups. But then with code like "Result := Result.remove(Result.length - 2);" you clearly do not optimize for speed anyway, so that is moot point too.

So, personally, all in all, i'd stick with something Visitor-like, in spirit at least.

> but the java version looks more familiar

AFAIR, Java, like C++, lacked TClass concept and had to workaround it inventing CoClasses (COM terms) and Factory classes (Java terms).
As in every languages, patterns emerged to overcome those shortcomings. So many Java patterns when copied to Pascal verbatim indeed look overengineered, because Pascla has different set of limitations.
But "the spirit", the reasoning behind the pattern remain valid.
« Last Edit: September 20, 2022, 12:50:46 pm by Arioch »

nothratal

  • Newbie
  • Posts: 3
Re: Nested Generic (or generic collection of generics)
« Reply #5 on: September 20, 2022, 02:18:28 pm »
Quote
Your boilerplate has the advantage of being computed in compile-time, rather than runtime dictionary look-ups. But then with code like "Result := Result.remove(Result.length - 2);" you clearly do not optimize for speed anyway, so that is moot point too.
you are right  :D
at the moment it's just about exercising FPC. What I really like about Free Pascal is the readability of the code.
Quote
Also to me the need to write boilerplate like "writeln(specialize prettyFormat<TNumbersL, integer>(numbersL));" on every call looks worse than need to write boilerplate of "RegisterFormatter" once.
yes that really hurts. It really doesn't improve the readability if I always have to call writeln(specialize prettyFormat<TNumbersL, integer>(numbersL))
In delphi mode I could spare the specialize, but still, not optimal. But if there isn't any type helper for generics, I don't see any other opportunities.

 

TinyPortal © 2005-2018