Forum > Free Pascal

Feature announcement: Function References and Anonymous Functions

(1/21) > >>

PascalDragon:
Dear Free Pascal Community,

The Free Pascal Developer team is pleased to finally announce the addition of a long awaited feature, though to be precise it's two different, but very much related features: Function References and Anonymous Functions. These two features can be used independantly of each other, but their greatest power they unfold when used together.

These features are based on the work by Blaise.ru, so thank you very much and I hope you're doing well considering the current situation.

In the following we'll highlight both features separately and then we'll take a look at using them together.

Function References

Function References (also applicable names are Procedure References and Routine References, in the following only Function References will be used) are types that can take a function (or procedure or routine), method, function variable (or procedure variable or routine variable), method variable, nested function (or nested procedure or nested routine) or an anonymous function (or anonymous procedure or anonymous routine) as a value. The function reference can then be used to call the provided function just like other similar routine pointer types. In contrast to these other types nearly all function-like constructs can be assigned to it (the only exception are nested function variables (or nested procedure variables or nested routine variables), more about that later on) and then used or stored.

Function references are enabled with the modeswitch FUNCTIONREFERENCES (the following examples will assume that this modeswitch is provided).

A function reference is declared as follows:

REFERENCE TO FUNCTION|PROCEDURE [(argumentlist)][: resulttype;] [directives;]

Examples:


--- Code: Pascal  [+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i < x.length; i++) { x[i].style.maxHeight='none'; x[i].style.height = Math.min(x[i].clientHeight+15,306)+'px'; x[i].style.resize = "vertical";}};} ---type  TProcLongInt = reference to procedure(aArg: LongInt); stdcall;  TFuncTObject = reference to function(aArg: TObject): TObject;
Like other function pointer types function references can also be declared as generic:


--- Code: Pascal  [+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i < x.length; i++) { x[i].style.maxHeight='none'; x[i].style.height = Math.min(x[i].clientHeight+15,306)+'px'; x[i].style.resize = "vertical";}};} ---type  generic TGenericProc<T> = reference to procedure(aArg: T);
As you can see, once function references are enabled you can't use the identifier "REFERENCE" as part of an alias declaration without using "&":


--- Code: Pascal  [+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i < x.length; i++) { x[i].style.maxHeight='none'; x[i].style.height = Math.min(x[i].clientHeight+15,306)+'px'; x[i].style.resize = "vertical";}};} ---type  someref = reference; // will fail  someref = &reference; // correct fix  var  somevar: reference; // will fail  somevar: &reference; // correct fix
A function reference variable can then be called like any other function pointer type:


--- Code: Pascal  [+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i < x.length; i++) { x[i].style.maxHeight='none'; x[i].style.height = Math.min(x[i].clientHeight+15,306)+'px'; x[i].style.resize = "vertical";}};} ---var  p: TProcLongInt;begin  p := @SomeLongIntProc;  p(42);end.
If a function reference has no parameters then you need to use "()" nevertheless in the FPC/ObjFPC modes like for other function pointer types:


--- Code: Pascal  [+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i < x.length; i++) { x[i].style.maxHeight='none'; x[i].style.height = Math.min(x[i].clientHeight+15,306)+'px'; x[i].style.resize = "vertical";}};} ---type  TProc = reference to procedure;var  p: TProc;begin  p := @SomeProcedure;  p(); // required  p; // this can be used e.g. in mode Delphiend.
Like other function pointer types they can also be declared anonymously as part of a variable, field declaration (but not as part of a paramater declaration):


--- Code: Pascal  [+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i < x.length; i++) { x[i].style.maxHeight='none'; x[i].style.height = Math.min(x[i].clientHeight+15,306)+'px'; x[i].style.resize = "vertical";}};} ---var  f: reference to function: LongInt;  type  TTest = class    f: reference to procedure;  end;
They get their great power from a point that is for once not considered an implementation detail: function references are in fact internally declared as reference counted interfaces with a single Invoke() method of the provided signature. So the above examples are in fact declared like this:


--- Code: Pascal  [+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i < x.length; i++) { x[i].style.maxHeight='none'; x[i].style.height = Math.min(x[i].clientHeight+15,306)+'px'; x[i].style.resize = "vertical";}};} ---type  TProcLongInt = interface(IInterface)    procedure Invoke(aArg: LongInt); stdcall; overload;  end;    TFuncTObject = interface(IInterface)    procedure Invoke(aArg: TObject): TObject; overload;  end;    generic TGenericProc<T> = interface(IInterface)    procedure Invoke(aArg: T); overload;  end;
This has a few implications:

* in the RTTI this will appear like a normal interface
* it reacts to the $M directive like a normal interface
* it is a managed type
* it has no valid GUID
* it can be implemented by a class
* it can be inherited from
Especially the last two points are important.

That the interface can be implemented means that much more functionality and state can be added to a function reference:


--- Code: Pascal  [+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i < x.length; i++) { x[i].style.maxHeight='none'; x[i].style.height = Math.min(x[i].clientHeight+15,306)+'px'; x[i].style.resize = "vertical";}};} ---type  TFunc = reference to function: LongInt;   TSomeImpl = class(TInterfacedObject, TFunc)    f: LongInt;    function Invoke: LongInt;  end;  function TSomeImpl.Invoke: LongInt;begin  Result := f;end; var  t: TSomeImpl;  f: TFunc;  begin  t := TSomeImpl.Create;  f := t;  Writeln(f()); // will write 0  t.f := 42;  Writeln(f()); // will write 42  f := Nil; // the usual warnings about mixing classes and interface apply!end.
As function references don't have valid GUIDs you can't however use QueryInterface() or the as-operator to retrieve it. Using the low level interface related functions of TObject however will work.

An interface that inherits from a function reference is still considered invokable by the compiler, so it can still be used like an ordinary function reference could, but you can also add additional methods including overloads for Invoke itself:


--- Code: Pascal  [+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i < x.length; i++) { x[i].style.maxHeight='none'; x[i].style.height = Math.min(x[i].clientHeight+15,306)+'px'; x[i].style.resize = "vertical";}};} ---type  TTest = reference to procedure(aArg: TObject);    TTestEx = interface(TTest)    function Invoke: TObject; overload;  end;  var  f: TTestEx;  o: TObject;begin  f := TSomeImplEx.Create;  o := f();  f(o);end.
This is for example described by Stefan Glienke on his blog. His linked example won't work as-is however due to missing functionality in Rtti.TValue.

As mentioned initially you can assign a nested function to a function reference, but not a nested function variable. There is no real technical reason for this, but it's instead a design choice based on how function references are assumed to behave: they are assumed to be valid beyond their scope (this will become clearer when combined with anonymous functions in the third part), so they can for example be returned from a function or stored in some class instance and can still be considered valid. However a nested function variable is no longer useable once the function frame it was retrieved has been left (for a nested function the compiler can safely convert it in a way that this is no problem, but for a nested function variable it simply can't).
One could argue that the same is true for method pointers and method variables as they aren't callable anymore once their class instance is freed however these are much more common in the Object Pascal world while nested function variables are very seldom used, thus the dangers of the former are much more apparent than the dangers of the later.
For this reason assigning nested function variables to function references is prohibited.

Anonymous Functions

Anonymous Functions (or Anonymous Procedures or Anonymous Routines, in the following simply Anonymous Functions) are routines that have no name associated with them and are declared in the middle of a code block (for example on the right side of an expression or as a parameter for a function call). However they can just as well be called directly like a nested function (or nested procedure or nested routine) would.

Anonymous functions are enabled with the modeswitch ANONYMOUSFUNCTIONS (the following examples will assume that this modeswitch is provided).

An anonymous function is declared as follows:

FUNCTION|PROCEDURE [(argumentlist)][[resultname]: resulttype;] [directives;]
[[VAR|TYPE|CONST section]|[nested routine]]*
BEGIN
[STATEMENTS]
END

As can be seen an anonymous function looks like a regular function (or procedure or routine) with the most important differences being that it does not have a name and that it isn't terminated by a semicolon (because it's essentially an expression). Because it doesn't have a name for modes that don't have the implicit RESULTvariable it's allowed to explicitely name the result variable (even in modes that do have the RESULT variable) like is the case with operator overloads.

It's possible to directly call an anonymous function in which case it essentially behaves like a nested function.

Like nested functions anonymous functions have access to the symbols (variables, functions, etc.) of the surrounding scope including Self if the surrounding scope is a method. Accessing such a symbol is named “capturing” and is one of the core concepts of anonymous functions.

Their main use however is when assigning them to one of the various function pointer types: function variables, method variables, nested function variables and function references. However not every anonymous function is assignable to every function pointer type as it depends on which symbols (if any) are captured from the surrounding scope. Unlike for non-anonymous function or method identifiers this assignment is however always done without the "@"-operator, because aside from calling one can't do much else with anonymous functions.
An anonymous function that captures no symbols at all (except for global symbols or static symbols) is assignable to all four function pointer types. If the anonymous function captures Self then it is no longer assignable to function variables, but still to the other three. And if it captures any local symbol then it's only assignable to nested function variables or function references.
In case of function variables, method variables and nested function variables anonymous functions behave just like their non-anonymous counterparts. The differences appear when they're used with function references which will be highlighted in the next part.

But first some examples:


--- Code: Pascal  [+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i < x.length; i++) { x[i].style.maxHeight='none'; x[i].style.height = Math.min(x[i].clientHeight+15,306)+'px'; x[i].style.resize = "vertical";}};} ---type  TFunc = function: LongInt; var  p: TProcedure;  f: TFunc;  n: TNotifyEvent;begin  procedure(const aArg: String)  begin    Writeln(aArg);  end('Hello World');    p := procedure       begin             Writeln('Foobar');           end;  p();    n := procedure(aSender: TObject);       begin             Writeln(HexStr(Pointer(aSender));           end;  n(Nil);    f := function MyRes : LongInt;       begin             MyRes := 42;           end;  Writeln(f());end.
Anonymous Function References

As mentioned above the greatest power of the two new features comes when the two are combined: like a nested function an anonymous function can access symbols from the surrounding scope, however unlike for nested functions a anonymous function that has been assigned to a function reference can leave the scope where it has been declared in and it will then take the captured symbols with it.
For this purpose any variable or parameter that is captured by an anonymous function will become part of the implicitely created object instance (which shall be considered opaque) that will be assigned to the function reference instead of belonging to the original function. The original function will then reference these symbols using the object instance instead of its stack frame. This has the implication that changes to the symobls will be reflected in all anonymous function that capture that symbol.

For example:


--- Code: Pascal  [+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i < x.length; i++) { x[i].style.maxHeight='none'; x[i].style.height = Math.min(x[i].clientHeight+15,306)+'px'; x[i].style.resize = "vertical";}};} ---type  TProc = reference to procedure; procedure Test;var  i: LongInt;  p: TProc;begin  i := 42;  p := procedure       begin             Writeln(i);           end;             p(); // will print 42    i := 21;    p(); // will print 21end;
Changes will those also be persistent across calls and different anonymous functions as long as they capture the same symbols:


--- Code: Pascal  [+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i < x.length; i++) { x[i].style.maxHeight='none'; x[i].style.height = Math.min(x[i].clientHeight+15,306)+'px'; x[i].style.resize = "vertical";}};} ---type  TProc = reference to procedure;  procedure Test;var  i: LongInt;  p1, p2: TProc;begin  i := 42;  p1 := procedure        begin              Writeln(i);                  i := i * 2;            end;             p1(); // will print 42    p2 := procedure        begin                  Writeln(i);                end;    p1(); // will print 84  p2(); // will print 168end;
The lifetime of managed types captured by anonymous function references will be handled accordingly (they will stay alive as long as at least one anonymous function that has captured them is alive as well), however special care needs to be taken regarding manual memory management:


--- Code: Pascal  [+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i < x.length; i++) { x[i].style.maxHeight='none'; x[i].style.height = Math.min(x[i].clientHeight+15,306)+'px'; x[i].style.resize = "vertical";}};} ---type  TProc = reference to procedure; function Test: TProc;var  o: TObject;begin  o := TObject.Create;  Result := procedure            begin                          Writeln(o.ClassName);                        end;  o.Free;end;
Calling the function reference returned by Test will essentially result in use-after-free. And not freeing “o” at all will result in a memory leak.

Compatibility

The two features are by and large compatible to Delphi's Anonymous Methods. However FPC allows the assignment of anonymous functions to various function pointer types while Delphi restricts them to function references.
Also FPC handles the assignment of function, method and nested function variables to function variables slightly differently. Take the following code:


--- Code: Pascal  [+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i < x.length; i++) { x[i].style.maxHeight='none'; x[i].style.height = Math.min(x[i].clientHeight+15,306)+'px'; x[i].style.resize = "vertical";}};} ---procedure Foo;begin  Writeln('Foo');end; procedure Bar;begin  Writeln('Bar');end; procedure Test;var  p: reference to procedure;  p2: procedure;begin  p2 := Foo;  p := p2;  p();  p2 := Bar;  p();end;
Delphi essentially generates the following:


--- Code: Pascal  [+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i < x.length; i++) { x[i].style.maxHeight='none'; x[i].style.height = Math.min(x[i].clientHeight+15,306)+'px'; x[i].style.resize = "vertical";}};} ---procedure Test;var  p: reference to procedure;  p2: procedure;begin  p2 := Foo;  p := procedure       begin             p2();           end;  p();  p2 := Bar;  p();end;
This will result in the following output:


--- Code: ---Foo
Bar
--- End code ---

However FPC will generate the following:


--- Code: Pascal  [+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i < x.length; i++) { x[i].style.maxHeight='none'; x[i].style.height = Math.min(x[i].clientHeight+15,306)+'px'; x[i].style.resize = "vertical";}};} ---procedure Test;var  p: reference to procedure;  p2, tmp: procedure;begin  p2 := Foo;  tmp := p2;  p := procedure       begin             tmp();           end;  p();  p2 := Bar;  p();end;
This will result in the following output:


--- Code: ---Foo
Foo
--- End code ---

This is more consistent with assignments of other function pointer types to function pointer types.

The Function References feature is available on all platforms which have the Classes feature available (so essentially everything except AVR) and Anonymous Functions themselves are available on all platforms (excluding the assignment to function references on platforms where these are missing). Yes, this includes platform like DOS where directives like “far” and “near” are handled accordingly (which means that these need to be compatible as well when assigning).

As these two features are rather complicated there might still be a huge bundle of bugs lurking around so I ask you to test them to year heart's content and report found bugs to the issues on GitLab so that we can fix as many of them as possible before the next major version (which is not yet planned, so don't worry ;) ).

Further RTL enhancements like the declaration of TProc<> or the addition of a TThread.Queue() that takes a function reference will come in the near future now that the basics on the compiler side are done. Maybe we can now also tackle ports of libraries like Spring4D and OmniThreadLibrary. There's also the idea to introduce a syntax to control whether symbols are captured by-reference (as currently) or by-value.

Enjoy!

Okoba:
Thank you so much to the team!

marcov:
Are the modeswitches already always on for $mode Delphi ?

PascalDragon:

--- Quote from: marcov on May 26, 2022, 10:09:52 pm ---Are the modeswitches already always on for $mode Delphi ?

--- End quote ---

Not yet, I first want to spot any mature hurdles with more testers and then we can make them default before the next major release (though at least they shouldn't affect exisiting code except for the point I had mentioned in my announcement post regarding reference, so we can make them default sooner as well *shrugs*).

bytebites:
Oh, it happened in our lifetime.  :D  Thank you.

Navigation

[0] Message Index

[#] Next page

Go to full version