Recent

Author Topic: Functional type with both 'is nested' and 'of object'  (Read 2030 times)

trexet

  • New Member
  • *
  • Posts: 37
Functional type with both 'is nested' and 'of object'
« on: June 19, 2024, 06:01:08 pm »
Hi. Is it possible to make a functional type for a function nested inside a class method?

{$modeswitch nestedprocvars} is on.

Code: Pascal  [Select][+][-]
  1. TWaitForConditionNested = function(): Boolean of object is nested;
Quote
uwait.pas(13,61) Error: Syntax error, ";" expected but "is" found

Code: Pascal  [Select][+][-]
  1. TWaitForConditionNested = function(): Boolean of object; is nested;
Quote
uwait.pas(13,62) Error: Syntax error, "IMPLEMENTATION" expected but "is" found

Code: Pascal  [Select][+][-]
  1. TWaitForConditionNested = function(): Boolean is nested of object;
Quote
uwait.pas(13,61) Error: Syntax error, ";" expected but "OF" found

« Last Edit: June 19, 2024, 06:03:38 pm by trexet »

Khrys

  • Full Member
  • ***
  • Posts: 104
Re: Functional type with both 'is nested' and 'of object'
« Reply #1 on: June 20, 2024, 08:26:10 am »
According to the syntax diagram in the reference guide's section on procedural types (https://www.freepascal.org/docs-html/ref/refse17.html) they are mutually exclusive modifiers.



The entire reason these two modifiers exist in the first place is to capture the implicit changes to the function signature of methods and nested procedures.
Namely, object methods have the  Self  pointer as their actual first argument, making the following technically equivalent:
Code: Pascal  [Select][+][-]
  1. function TFoo.Bar(Alpha: Integer): Boolean;                     // Method
  2. function TFoo_Bar(const Self: TFoo; Alpha: Integer): Boolean;   // Free-standing function

The special thing about nested procedures is that they can access the enclosing function's local variables & parameters. This is made possible by passing the parent function's frame pointer (describing the general location of local variables) as the first parameter:
Code: Pascal  [Select][+][-]
  1. function BarOuter(Alpha: Integer): Boolean;
  2.   function BarInner(): Boolean;                                 // Nested;        Can access "Alpha"
  3. function BarOuter_BarInner(const ParentFP: Pointer): Boolean;   // Free-standing; Resolves "Alpha" by adding an offset to "ParentFP"

This single requirement (the ability to access the enclosing function's local variables and parameters) is enough to make nested procedures work inside methods, because  Self  is just another parameter/local variable of the parent function; there's no need to explicitly combine these two modifiers -  is nested;  is enough.



Knowing all this, we can demonstrate why  {$modeswitch nestedprocvars}  can be a bad idea if one isn't careful enough when passing around such function pointers. The crux lies in the loss of type & memory safety when accessing parent local variables (implicitly through  ParentFP); the following code compiles without any warnings and demonstrates why nested procedure variables aren't closures - surrounding variables are not captured:
Code: Pascal  [Select][+][-]
  1. program NestedProcedureVariableAbuse;
  2.  
  3. {$mode objfpc}
  4. {$modeswitch nestedprocvars}
  5.  
  6. type
  7.   TNestedBar = function(X: Integer): Integer is nested;
  8.  
  9. function GoodBar(): TNestedBar;
  10.   function Bar(X: Integer): Integer;  // Does not use parent's local variables - OK
  11.   begin
  12.     Exit(X + 8);
  13.   end;
  14. begin
  15.   Result := @Bar;
  16. end;
  17.  
  18. function BadBar(): TNestedBar;
  19. var
  20.   Alpha: Integer = 8;
  21.   function Bar(X: Integer): Integer;  // Uses parent's local variables - DANGEROUS
  22.   begin                               // "Alpha" is equivalent to PInteger(ParentFP)[ARBITRARY_OFFSET]
  23.     Exit(X + Alpha);                  // Optimizations WILL break this in most other contexts
  24.   end;
  25. begin
  26.   Result := @Bar;
  27. end;
  28.  
  29. procedure Main();
  30. begin
  31.   WriteLn(GoodBar()(42)); // Outputs "50" as expected
  32.   WriteLn(BadBar()(42));  // Undefined behaviour; in fact doesn't even work when there's a local Integer variable defined
  33. end;

In essence, procedural function variables are fine to be passed around and called as long as they don't use parent local variables - otherwise they become incredibly fragile and dangerous. Nowadays function references (https://forum.lazarus.freepascal.org/index.php?topic=59468.0) are the way to go in these cases.

PascalDragon

  • Hero Member
  • *****
  • Posts: 5750
  • Compiler Developer
Re: Functional type with both 'is nested' and 'of object'
« Reply #2 on: June 23, 2024, 05:05:28 pm »
What exactly is it that you're trying to solve? Can you give a simple example? 🤔

trexet

  • New Member
  • *
  • Posts: 37
Re: Functional type with both 'is nested' and 'of object'
« Reply #3 on: June 24, 2024, 01:05:46 am »
What exactly is it that you're trying to solve? Can you give a simple example? 🤔

Consider a simple wait_until implementation:
Code: Pascal  [Select][+][-]
  1. type
  2.     TWaitForCondition = function(): Boolean of object;
  3.  
  4. procedure WaitFor(F: TWaitForCondition);
  5. begin
  6.     while (not F()) do begin
  7.         Application.ProcessMessages();
  8.         Sleep(SleepInterval);
  9.     end;
  10. end;

Being used like this:
Code: Pascal  [Select][+][-]
  1. procedure TListControlFrame.WaitRefresh();
  2. begin
  3.     Screen.Cursor := crHourGlass;
  4.     ListBoxFilter.Items.Assign(PStringList^);
  5.     ListBoxFilter.InvalidateFilter();
  6.     WaitFor(@fFilterWaitCondition); // <---- HERE
  7.     Screen.Cursor := crDefault;
  8. end;

Where wait condition is

Code: Pascal  [Select][+][-]
  1. function TListControlFrame.fFilterWaitCondition(): Boolean;
  2. begin
  3.     Result := not ListBoxFilter.IdleConnected;
  4. end;

ListBoxFilter.InvalidateFilter() starts an async operation, which I should synchronize to by checking property ListBoxFilter.IdleConnected.

What I'm trying to achieve is to make fFilterWaitCondition() nested inside WaitRefresh(), because it's the only user of fFilterWaitCondition().
However, by being nested it becomes both nested and a (hidden?) class method.

I suppose solving this would be easier by using function references in 3.3.
« Last Edit: June 24, 2024, 01:07:24 am by trexet »

PascalDragon

  • Hero Member
  • *****
  • Posts: 5750
  • Compiler Developer
Re: Functional type with both 'is nested' and 'of object'
« Reply #4 on: June 25, 2024, 09:11:26 pm »
What I'm trying to achieve is to make fFilterWaitCondition() nested inside WaitRefresh(), because it's the only user of fFilterWaitCondition().
However, by being nested it becomes both nested and a (hidden?) class method.

With nested functions it will work correctly with nested functions as well as global functions. If you add an additional overload for methods you can cover all your bases:

Code: Pascal  [Select][+][-]
  1. program tnested;
  2.  
  3. {$mode objfpc}{$H+}
  4. {$modeswitch nestedprocvars}
  5.  
  6. type
  7.   TWaitForConditionFunc = function: Boolean is nested;
  8.   TWaitForConditionMethod = function: Boolean of object;
  9.  
  10.   TTest = class
  11.     k: LongInt;
  12.     function DoSomethingObj: Boolean;
  13.     procedure Test;
  14.   end;
  15.  
  16. procedure WaitFor(aArg: TWaitForConditionMethod);
  17. begin
  18.   while not aArg() do begin
  19.     Writeln('Spinning');
  20.   end;
  21.   Writeln('Done');
  22. end;
  23.  
  24. procedure WaitFor(aArg: TWaitForConditionFunc);
  25. begin
  26.   while not aArg() do begin
  27.     Writeln('Spinning');
  28.   end;
  29.   Writeln('Done');
  30. end;
  31.  
  32. var
  33.   j: LongInt;
  34.  
  35. function DoSomethingElse: Boolean;
  36. begin
  37.   Dec(j);
  38.   Result := j = 0;
  39. end;
  40.  
  41. function TTest.DoSomethingObj: Boolean;
  42. begin
  43.   Dec(k);
  44.   Result := k = 0;
  45. end;
  46.  
  47. procedure TTest.Test;
  48. var
  49.   i: LongInt;
  50.  
  51.   function DoSomething: Boolean;
  52.   begin
  53.     Dec(i);
  54.     Result := i = 0;
  55.   end;
  56.  
  57. begin
  58.   i := 3;
  59.   WaitFor(@DoSomething);
  60.   j := 5;
  61.   WaitFor(@DoSomethingElse);
  62.   k := 1;
  63.   WaitFor(@DoSomethingObj);
  64. end;
  65.  
  66. var
  67.   t: TTest;
  68. begin
  69.   t := TTest.Create;
  70.   try
  71.     t.Test;
  72.   finally
  73.     t.Free;
  74.   end;
  75. end.

I suppose solving this would be easier by using function references in 3.3.

Function references allow the usage of global functions, nested functions as well as methods.

trexet

  • New Member
  • *
  • Posts: 37
Re: Functional type with both 'is nested' and 'of object'
« Reply #5 on: July 01, 2024, 03:23:25 pm »
Thanks, works fine:

Code: Pascal  [Select][+][-]
  1. TWaitForCondition = function(): Boolean of object;
  2. TWaitForConditionNested = function(): Boolean is nested;
  3.  
  4. procedure WaitFor(F: TWaitForCondition); overload;
  5. procedure WaitFor(F: TWaitForConditionNested); overload;

Code: Pascal  [Select][+][-]
  1. procedure TListControlFrame.WaitRefresh();
  2.     function fFilterWaitCondition(): Boolean;
  3.     begin
  4.         Result := not ListBoxFilter.IdleConnected;
  5.     end;
  6. begin
  7.     {...}
  8.     WaitFor(@fFilterWaitCondition);

I think it would be nice to have it clarified in docs that is nested works for functions nested inside methods and of object is not needed (as nesting inside method is confusing about of object).

 

TinyPortal © 2005-2018