Recent

Author Topic: Create inherited class  (Read 2119 times)

SymbolicFrank

  • Hero Member
  • *****
  • Posts: 1313
Create inherited class
« on: April 26, 2022, 08:56:13 am »
I have nodes, which are basically:

Code: Pascal  [Select][+][-]
  1. TNode = class
  2.   MyParent: TNode;
  3.   MyName: string;
  4.   MyValue: Variant;
  5.   MyChildren: TObjectList<TNode>
  6.   function Duplicate: TNode;
  7. end;

They are for serializing / deserializing things like JSON. Most actions are done recursively, on all children as well.

Depending on the usage, I inherit a new class from them. So, I end up with node trees, filled with all kinds of different nodes.

As it is, much is virtual and overloaded in the child classes, which isn't optimal. Simple case: the function Duplicate. It should create a copy of the correct node and children, and copy the corresponding fields. And overloading virtual functions which have different parameters or results (say, TSpecialNode instead of TNode) doesn't work all that great. And I should probably turn every function that returns a TNode into a virtual constructor and override that when inheriting a new class.

My next try was generics, but that basically requires something like this (I think):

Code: Pascal  [Select][+][-]
  1. TNode<T: TNode> = class;

Next up: RTTI.

Code: Pascal  [Select][+][-]
  1. function TNode.Duplicate: TNode;
  2. var
  3.   n: TNode;
  4. begin
  5.   Result := ((Self.ClassType).Create as TNode);
  6.   // copy fields
  7.   for n in MyChildren do Result.MyChildren.Add(n.Duplicate);
  8. end;
  9.  

But I'm probably making it far too complex, it's probably quite simple, I just don't get it. So, who can tell me how to do it?
« Last Edit: April 26, 2022, 09:29:41 am by SymbolicFrank »

PascalDragon

  • Hero Member
  • *****
  • Posts: 5481
  • Compiler Developer
Re: Create inherited class
« Reply #1 on: April 26, 2022, 09:07:17 am »
Just look at how TPersistent does it (simplified):

Code: Pascal  [Select][+][-]
  1. type
  2.   TPersistent = class
  3.     procedure Assign(aOther: TPersistent); virtual;
  4.   end;
  5.  
  6.   TSomeSubClass = class(TPersistent)
  7.     SomeField: Integer;
  8.     procedure Assign(aOther: TPersistent); override;
  9.   end;
  10.  
  11. procedure TPersistent.Assign(aOther: TPersistent);
  12. begin
  13.   { empty for TPersistent, but for your TNode you'd copy its fields here }
  14. end;
  15.  
  16. procedure TSomeSubClass.Assign(aOther: TPersistent);
  17. var
  18.   other: TSomeSubClass absolute aOther;
  19. begin
  20.   if aOther is TSomeSubClass then begin
  21.     SomeField := other.SomeField;
  22.   end;
  23.   inherited;
  24. end;

With that you can easily implement a Duplicate with the appropriate type:

Code: Pascal  [Select][+][-]
  1. type
  2.   TSomeSubClass = class
  3.     { ... }
  4.     function Duplicate: TSomeSubClass;
  5.   end;
  6.  
  7. function TSomeSubClass.Duplicate: TSomeSubClass;
  8. begin
  9.   Result := TSomeSubClass.Create;
  10.   Result.Assign(Self);
  11. end;

If you're happy with casting you can also simply add a Duplicate to the base type as long as you ensure that the constructor is virtual:

Code: Pascal  [Select][+][-]
  1. type
  2.   TNode = class
  3.     constructor Create; virtual;
  4.     function Duplicate: TNode;
  5.   end;
  6.  
  7. function TNode.Duplicate: TNode;
  8. begin
  9.   // if you call this on a child class it will create an instance of that class
  10.   // please note that this will always call the parameterless constructor
  11.   Result := TNode(ClassType).Create;
  12.   Result.Assign(Self);
  13. end;

The heavy lifting is always done by Assign.

This should give you pointers in the right direction.

SymbolicFrank

  • Hero Member
  • *****
  • Posts: 1313
Re: Create inherited class
« Reply #2 on: April 26, 2022, 09:19:31 am »
Thanks!

egsuh

  • Hero Member
  • *****
  • Posts: 1292
Re: Create inherited class
« Reply #3 on: April 26, 2022, 09:25:59 am »
I believe virtual methods and overriding them would work. Not overloading.

SymbolicFrank

  • Hero Member
  • *****
  • Posts: 1313
Re: Create inherited class
« Reply #4 on: April 26, 2022, 10:26:45 am »
@PascalDragon: well, I changed it as you said, but that's basically what I already had (with the method CopyValues instead of Assign). The thing is, a "basic" TNode has 53 constructors and methods, of which at least eleven have to be overridden in any inherited class. Simply because they do something with "TNode".

The next node type is the TTypedNode (like a Variant), which will roughly double that. That made me wonder if it wouldn't be easier to put everything in the base class. And there are more sub-classes on top of that.

Most of the time, the code is the same, only the type has changed. It's a trade-off between that and casting everything in the code that uses them. Which is mostly why generics were invented.

In case you're wondering: it is to encapsulate WEB API's (REST and such) by parsing the description files. With a TDataSet on top, when it's done.
« Last Edit: April 26, 2022, 10:29:27 am by SymbolicFrank »

BeniBela

  • Hero Member
  • *****
  • Posts: 906
    • homepage
Re: Create inherited class
« Reply #5 on: April 26, 2022, 10:46:50 am »
You can make  the return type of Duplicate generic: https://blog.grijjy.com/2022/01/25/crgp/


SymbolicFrank

  • Hero Member
  • *****
  • Posts: 1313
Re: Create inherited class
« Reply #6 on: April 26, 2022, 11:07:19 am »
You can make  the return type of Duplicate generic: https://blog.grijjy.com/2022/01/25/crgp/

Yes, that seems to be what I was looking for :) Let's see if that does the trick!

SymbolicFrank

  • Hero Member
  • *****
  • Posts: 1313
Re: Create inherited class
« Reply #7 on: April 26, 2022, 11:37:49 am »
I tried it out (mode Delphi):

Code: Pascal  [Select][+][-]
  1.   TBaseNode = class abstract
  2.   protected
  3.     MyName: string;
  4.     MyValue: Variant;
  5.     MyParent: TBaseNode;
  6.     MyChildren: TObjectList<TBaseNode>;
  7.   end;
  8.  
  9.   TBaseNode<T: class> = class abstract(TBaseNode)
  10.   protected
  11.     function GetNode(Index: SizeInt): T;
  12.   public
  13.     class constructor Create;
  14.     property Child[Index: SizeInt]: T read GetNode; default;
  15.   end;
  16.  
  17.   TNode = class(TBaseNode<TNode>)
  18.  
  19.   end;

But unfortunately, the declaration of the TNode fails with:

nodes.pas(57,27) Error: Illegal expression
nodes.pas(57,33) Error: Class type expected, but got "<erroneous type>"

avk

  • Hero Member
  • *****
  • Posts: 752
Re: Create inherited class
« Reply #8 on: April 26, 2022, 01:50:57 pm »
Maybe a forward declaration would help?
Code: Pascal  [Select][+][-]
  1.   TBaseNode = class abstract
  2.   protected
  3.     MyName: string;
  4.     MyValue: Variant;
  5.     MyParent: TBaseNode;
  6.     MyChildren: TObjectList<TBaseNode>;
  7.   end;
  8.  
  9.   TBaseNode<T: class> = class abstract(TBaseNode)
  10.   protected
  11.     function GetNode(Index: SizeInt): T;
  12.   public
  13.     class constructor Create;
  14.     property Child[Index: SizeInt]: T read GetNode; default;
  15.   end;
  16.  
  17.   TNode = class;
  18.  
  19.   TNode = class(TBaseNode<TNode>)
  20.  
  21.   end;
  22.  

SymbolicFrank

  • Hero Member
  • *****
  • Posts: 1313
Re: Create inherited class
« Reply #9 on: April 26, 2022, 02:37:54 pm »
That was indeed missing.

Code: Pascal  [Select][+][-]
  1. type
  2.  
  3.   TNode = class;
  4.   TNodeArray = array of TNode;
  5.   TNodeList = TObjectList<TNode>;
  6.  
  7.   TBaseNode = class abstract
  8.   protected
  9.     MyName: string;
  10.     MyValue: Variant;
  11.     MyParent: TBaseNode;
  12.     MyChildren: TNodeList;
  13.   end;
  14.  
  15.   { TBaseNode }
  16.  
  17.   TBaseNode<T: class> = class abstract(TBaseNode)
  18.   protected
  19.     function GetChild(Index: SizeInt): T;
  20.   public
  21.     class constructor Create;
  22.     constructor Create; virtual;
  23.     property Child[Index: SizeInt]: T read GetChild; default;
  24.     function Add: T; virtual;
  25.   end;
  26.  
  27.  
  28.   { TNodeComparer }
  29.  
  30.   TNodeComparer<TNode> = class(TComparer<TNode>)
  31.   public
  32.     function Compare(constref Left, Right: TNode): Integer; override;
  33.   end;
  34.  
  35.   { TNode }
  36.  
  37.   TNode = class(TBaseNode<TNode>)
  38.  
  39.   end;
  40.  
  41.   { TBaseTypedNode }
  42.  
  43.   TBaseTypedNode<T: class> = class(TBaseNode<T>)
  44.   protected
  45.     MyType: Integer;
  46.     procedure SetType(ThisType: Integer);
  47.   public
  48.     property Kind: Integer read MyType write SetType;
  49.     constructor Create; override;
  50.     function Add: T; override;
  51.   end;
  52.  
  53.   { TTypedNode }
  54.  
  55.   TTypedNode = class;
  56.   TTypedNode = class(TBaseTypedNode<TTypedNode>)
  57.  
  58.   end;

The code tools really don't like it, but it does compile :D

I'll have to play around with this for a bit, to see how it works and if it has the intended result.

SymbolicFrank

  • Hero Member
  • *****
  • Posts: 1313
Re: Create inherited class
« Reply #10 on: April 29, 2022, 08:54:59 am »
Well, it might work in Delphi, but it doesn't really work in Free Pascal. I have to cast everything all the time and even then sometimes the compiler cannot find fields. The code tools don't work, so I have to figure it out by guessing and compiling. I'll have to think of something else.

I'll try the RTTI way next.

But it would be awesome if generics allowed using the class itself as the type.

Martin_fr

  • Administrator
  • Hero Member
  • *
  • Posts: 9871
  • Debugger - SynEdit - and more
    • wiki
Re: Create inherited class
« Reply #11 on: April 29, 2022, 09:44:21 am »
I have partial fixes to the codetools. I do similar staff in ide/packages/idedebugger/...watchresult.....
I am away, but I'll try to have a look to get some of it into the main branch when I am back home.


I don't understand
Code: Pascal  [Select][+][-]
  1.     function GetChild(Index: SizeInt): T;
  2.     property Child[Index: SizeInt]: T read GetChild; default;

Can a node only have children of its own class (or subclasses of its own class)?

Also, properties aren't virtual. So unless you have
Code: Pascal  [Select][+][-]
  1. var n: TFooNode;
it wont work?
If you do "n.Child[1];" on an instance of TFooNode, but stored in "var n: TNode", then it will use the declaration of TNode (which does not return the type TFooNode).

And a virtual getter does not work either, because you can't change the declared return type.

Of course if all children are TFooNode, and you are calling this on "self" then it should save you the type-cast.


If you need to access children (as a specific sub-class), but you can't have the owner variable (self or other "n") declared as TFooNode, then the air get's thinner....

1) (ugly) have countless properties on the base class
Code: Pascal  [Select][+][-]
  1.     property ChildFoo[Index: SizeInt]: TFooNode read GetChildFoo;
  2.    ....
 
2) Sugar coat the typecasts into a type helper
Code: Pascal  [Select][+][-]
  1. TBaseNodeHelper = class helper for TBaseNode
  2.   function AsFooNode: TFooNode; // do the typecast here
  3. end;
Code: Pascal  [Select][+][-]
  1.    BaseNode.Child[1].AsFooNode.FooSpecific;

3) use absolute

4) Have "function FooSpecific: ..; virtual;" on the BaseNode as empty function => doing nothing (or even assert as failure).
Then you can always call it.

4a) you can put "FooSpecific" into a type helper (helper for TBaseNode). Saves the call of "AsFooNode". But essentially the same as having it on the BaseNode.
Only you can build the type helper by adding methods for each sub-class in individual blocks

Code: Pascal  [Select][+][-]
  1. TBaseNodeHelper = class helper for TBaseNode
  2.   function AsFooNode: TFooNode; // optional, but still useful
  3. end;
  4. TFooNodeHelper = class helper (TBaseNodeHelper) for TBaseNode // inherit / I don't have the syntax in my head right now...
  5.   function GetFooChild(idx: sizeint): TFooNode;
  6.   property FooChild[idx].....
  7. end;
  8. TBarNodeHelper = class helper (TFooNodeHelper) for TBaseNode // inherit, but from TFooNodeHelper
  9. ...
  10. end;
  11. TFinalNodeHelper = class helper (TBarNodeHelper) ....
  12.  

Then if TFinalNodeHelper is in scope, all methods are avail.
But if you add a new helper, you need to make sure not to break the inheritance chain....




That are all the ideas I have right now.

SymbolicFrank

  • Hero Member
  • *****
  • Posts: 1313
Re: Create inherited class
« Reply #12 on: April 29, 2022, 11:35:27 am »
As far as I understand it, it is the difference between messaging and inheritance. With inheritance, all descendants of TNode fit in an "array of TNode", but are thereafter treated like that by the compiler, unless you cast them to another type. It is up to the calling function to know that class type.

While with messaging, you send the instance a message and it sees if it can handle it (has a new field or method for that). If it doesn't, it sends the message to its parent and/or superclass, depending. In that case, they don't even have to have the same ancestor, only a compatible message handler. (Which probably IS that common ancestor.)

I can remember I implemented a hybrid of that, long ago, before classes were a thing. With linked lists. I don't know if I used strings or some kind of enumeration, but I would check the method index (VTable) of that instance to see if it could do that. Same with fields.

At the other hand, there are the classes that provide methods that require a minimum descendant level ("InheritsFrom(TTypedNode)" instead of "is TNode"), like the serializers.

Generics don't really fit this mold, because they require a container and have a fixed type as well. You still need your code to handle all cases. It just saves you the trouble of making a separate class for each case and overriding those methods in all descendants.

I made something like this in an early version of C#, when ".docx" appeared. It had the same problems, so I made a whole new base class (like TObject) and class hierarchy, that allowed "lazy typing" and serialization/deserialization as well. That heavily depended on Reflection, the .NET RTTI variant. I should probably do that here as well.

Web APIs aren't as complex as a docx document, but about as standardized as the components of that ;)

Interesting enough, this is the same problem you see in a lot of web frameworks and micro-services, where the URL is parsed as function names and parameters. Most often in scripting-languages, or at least ones that don't have strict types and full-blown classes.

So, now for version 0.3: the RTTI variant. :)

SymbolicFrank

  • Hero Member
  • *****
  • Posts: 1313
Re: Create inherited class
« Reply #13 on: May 03, 2022, 11:32:35 am »
I decided to start with a class that contains all the fields and base methods, inherit a new class from it that contains the properties and some extended methods. But that still doesn't work.

Code: Pascal  [Select][+][-]
  1. type
  2.   TSNode = class
  3.   protected
  4.     MyParent: TSNode;
  5.     MyChildren: TObjectList<TSNode>;
  6.   end;
  7.  
  8.   TNode = class(TSNode)
  9.   protected
  10.     function GetChild(Index: SizeInt): TNode; virtual;
  11.   public
  12.     property Child[Index: SizeInt]: TNode read GetChild; default;
  13.     function GetEnumerator: TNodeEnumerator; virtual;
  14.     function Serialize: string; virtual;
  15.   end;
  16.  
  17. implementation
  18.  
  19. function TNodeEnumerator.GetCurrent: TNode;
  20. begin
  21.   Result := TNode(MyNode.MyChildren[MyIndex]);
  22. end;
  23.  
  24. function TNode.GetChild(Index: SizeInt): TNode;
  25. begin
  26.   Result := TNode(MyChildren[Index]);
  27. end;
  28.  
  29. var
  30.   Node, n: TNode;
  31. begin
  32.   Node := TNode.Create;
  33.   // Add some children here
  34.   for n in Node do WriteLn(n.Serialize); // n is actually a TSNode
  35.   Node.Free;
  36. end;

"n.Serialize" gives an error, as it is a TSNode, which doesn't have that method. using

Code: Pascal  [Select][+][-]
  1. MyChildren[i].Serialize;

has the same problem. Adding a virtual, abstract Serialize method to the TSNode doesn't help and gives an abstract error. You have to keep casting it. Ok, obvious in hindsight. You can insist that it's a TNode, but the compiler knows better.

In short: it won't work. I have to put everything inside a single class. RTTI isn't going to help with that. A lot less dynamic than I wanted it, but it cannot be helped.

PascalDragon

  • Hero Member
  • *****
  • Posts: 5481
  • Compiler Developer
Re: Create inherited class
« Reply #14 on: May 03, 2022, 01:21:52 pm »
"n.Serialize" gives an error, as it is a TSNode, which doesn't have that method. using

Code: Pascal  [Select][+][-]
  1. MyChildren[i].Serialize;

has the same problem. Adding a virtual, abstract Serialize method to the TSNode doesn't help and gives an abstract error. You have to keep casting it. Ok, obvious in hindsight. You can insist that it's a TNode, but the compiler knows better.

Did you keep your TNode.Serialize as virtual or did you change it to override? Cause if the former you really shouldn't be surprised that it doesn't work cause you're essentially introducing a new virtual method unrelated to the one of the parent and the compiler will even warn about that. If you did change it to override then please provide a complete example, cause then it doesn't make sense.

 

TinyPortal © 2005-2018