Sign in Join

Live chat for Free Pascal, Lazarus, Delphi and every Pascal programmer.

Mailing lists / fpc-pascal / 2022-05

Feature Announcement: Function References and Anonymous Functions

22 messages · 10 people · –

Reply by email Subscribe to this list Your subscription

Read-only archive. Use j and k to move between messages.

Sven Barth

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 begin ===

type
   TProcLongInt = reference to procedure(aArg: LongInt); stdcall;
   TFuncTObject = reference to function(aArg: TObject): TObject;

=== code end ===

Like other function pointer types function references can also be declared as generic:

=== code begin ===

type
   generic TGenericProc<T> = reference to procedure(aArg: T);

=== code end ===

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 begin ===

type
   someref = reference; // will fail
   someref = &reference; // correct fix
var
   somevar: reference; // will fail
   somevar: &reference; // correct fix

=== code end ===

A function reference variable can then be called like any other function pointer type:

=== code begin ===

var
   p: TProcLongInt;
begin
   p := @SomeLongIntProc;
   p(42);
end.

=== code 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 begin ===

type
   TProc = reference to procedure;
var
   p: TProc;
begin
   p := @SomeProcedure;
   p(); // required
   p; // this can be used e.g. in mode Delphi
end.

=== code end ===

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 begin ===

var
   f: reference to function: LongInt;
type
   TTest = class
     f: reference to procedure;
   end;

=== code 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 begin ===

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;

=== code 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 begin ===

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.

=== code 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 begin ===

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.

=== code end ===

This is for example described by Stefan Glienke on his blog ( https://delphisorcery.blogspot.com/2015/06/anonymous-method-overloading.html ). 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 RESULT variable 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 begin ===

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.

=== code end ===

= Anonymous Functions 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 begin ===

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 21
end;

=== code end ===

Changes will those also be persistent across calls and different anonymous functions as long as they capture the same symbols:

=== code begin ===

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 168
end;

=== code end ===

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 begin ===

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;

=== code 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 begin ===

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;

=== code end ===

Delphi essentially generates the following:

=== code begin ===

procedure Test;
var
   p: reference to procedure;
   p2: procedure;
begin
   p2 := Foo;
   p := procedure
        begin
          p2();
        end;
   p();
   p2 := Bar;
   p();
end;

=== code end ===

This will result in the following output:

=== output begin ===

Foo
Bar

=== output end ===

However FPC will generate the following:

=== code begin ===

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;

=== code end ===

This will result in the following output:

=== output begin ===

Foo
Foo

=== output end ===

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!

Regards,
Sven

Michael Van Canneyt
to Sven Barth

Hello !

Nice job, kudos both for Blaise and Sven, and special thanks to Zoë Peterson for donating the code to FPC, as Scooter Software sponsored the initial work by Blaise.

I'm glad to see that the FPC implementation makes the distinction between function references and anonymous functions very explicitly :-)

Michael.

Sven Barth wrote 564 lines
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 begin === type   TProcLongInt = reference to procedure(aArg: LongInt); stdcall;   TFuncTObject = reference to function(aArg: TObject): TObject; === code end === Like other function pointer types function references can also be declared as generic: === code begin === type   generic TGenericProc<T> = reference to procedure(aArg: T); === code end === 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 begin === type   someref = reference; // will fail   someref = &reference; // correct fix var   somevar: reference; // will fail   somevar: &reference; // correct fix === code end === A function reference variable can then be called like any other function pointer type: === code begin === var   p: TProcLongInt; begin   p := @SomeLongIntProc;   p(42); end. === code 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 begin === type   TProc = reference to procedure; var   p: TProc; begin   p := @SomeProcedure;   p(); // required   p; // this can be used e.g. in mode Delphi end. === code end === 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 begin === var   f: reference to function: LongInt; type   TTest = class     f: reference to procedure;   end; === code 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 begin === 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; === code 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 begin === 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. === code 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 begin === 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. === code end === This is for example described by Stefan Glienke on his blog ( https://delphisorcery.blogspot.com/2015/06/anonymous-method-overloading.html ). 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 RESULT variable 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 begin === 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. === code end === = Anonymous Functions 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 begin === 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 21 end; === code end === Changes will those also be persistent across calls and different anonymous functions as long as they capture the same symbols: === code begin === 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 168 end; === code end === 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 begin === 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; === code 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 begin === 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; === code end === Delphi essentially generates the following: === code begin === procedure Test; var   p: reference to procedure;   p2: procedure; begin   p2 := Foo;   p := procedure        begin          p2();        end;   p();   p2 := Bar;   p(); end; === code end === This will result in the following output: === output begin === Foo Bar === output end === However FPC will generate the following: === code begin === 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; === code end === This will result in the following output: === output begin === Foo Foo === output end === 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! Regards, Sven _______________________________________________ fpc-pascal maillist - https://lists.freepascal.org/cgi-bin/mailman/listinfo/fpc-pascal
Mattias Gaertner
to Sven Barth: The Free Pascal Developer team is pleased to finally announce the addition of a long…
Show quoted text 5 lines
[...] 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.

\O/

Show quoted text 5 lines
[...] 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

First!

[...]

Mattias

Thomas Kurz
to Sven Barth: The Free Pascal Developer team is pleased to finally announce the addition of a long…

Great!!! Thank you very very much :)))

Earlier message 566 lines
----- Original Message ----- From: Sven Barth via fpc-pascal To: Sent: Thursday, May 26, 2022, 21:47:06 Subject: [fpc-pascal] Feature Announcement: Function References and Anonymous Functions 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 begin === type TProcLongInt = reference to procedure(aArg: LongInt); stdcall; TFuncTObject = reference to function(aArg: TObject): TObject; === code end === Like other function pointer types function references can also be declared as generic: === code begin === type generic TGenericProc<T> = reference to procedure(aArg: T); === code end === 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 begin === type someref = reference; // will fail someref = &reference; // correct fix var somevar: reference; // will fail somevar: &reference; // correct fix === code end === A function reference variable can then be called like any other function pointer type: === code begin === var p: TProcLongInt; begin p := @SomeLongIntProc; p(42); end. === code 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 begin === type TProc = reference to procedure; var p: TProc; begin p := @SomeProcedure; p(); // required p; // this can be used e.g. in mode Delphi end. === code end === 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 begin === var f: reference to function: LongInt; type TTest = class f: reference to procedure; end; === code 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 begin === 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; === code 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 begin === 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. === code 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 begin === 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. === code end === This is for example described by Stefan Glienke on his blog ( https://delphisorcery.blogspot.com/2015/06/anonymous-method-overloading.html ). 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 RESULT variable 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 begin === 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. === code end === = Anonymous Functions 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 begin === 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 21 end; === code end === Changes will those also be persistent across calls and different anonymous functions as long as they capture the same symbols: === code begin === 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 168 end; === code end === 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 begin === 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; === code 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 begin === 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; === code end === Delphi essentially generates the following: === code begin === procedure Test; var p: reference to procedure; p2: procedure; begin p2 := Foo; p := procedure begin p2(); end; p(); p2 := Bar; p(); end; === code end === This will result in the following output: === output begin === Foo Bar === output end === However FPC will generate the following: === code begin === 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; === code end === This will result in the following output: === output begin === Foo Foo === output end === 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! Regards, Sven
Hairy Pixels
to Sven Barth: The Free Pascal Developer team is pleased to finally announce the addition of a long…
Show quoted text 5 lines
On May 27, 2022, at 2:47 AM, Sven Barth via fpc-pascal wrote: 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!

Huge news, I’m so excited about this. Thank you both.

As discussed over the last years, now that we’ve implemented the anonymous functions part can we consider an optimization for closures that don’t leave the current scope? I explored the possibility of using the existing nested function feature and I think that would work great with the anonymous functions syntax.

The open questions are:

1) How does the compiler know the closure will not leave the scope?
2) The actual implementation, leverage the nested functions which are stacked based objects or change the closure capture object to be a record instead of an interface (much more work I believe).

Regards,
Ryan Joseph

Marco van de Voort
to Mattias Gaertner: On Thu, 26 May 2022 21:47:06 +0200 \O/ First! Mattias
Mattias Gaertner wrote 5 lines
> 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 First!

Ditto! https://gitlab.com/freepascal.org/lazarus/lazarus/-/issues/39774

This ticket also contains an experimental patch for anonymous method queue and synchronize. (required to reproduce)

Sven Barth
to Marco van de Voort

Marco van de Voort via fpc-pascal schrieb
am Fr., 27. Mai 2022, 13:31:

Show quoted text 13 lines
On 27-5-2022 00:46, Mattias Gaertner via fpc-pascal wrote: > >> 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 > First! Ditto! https://gitlab.com/freepascal.org/lazarus/lazarus/-/issues/39774 This ticket also contains an experimental patch for anonymous method queue and synchronize. (required to reproduce)

You couldn't have written a test that's independent of TThread? 🥺

Regards,
Sven

Marco van de Voort
to Sven Barth
Sven Barth wrote
You couldn't have written a test that's independent of TThread? 🥺

When I saw no reactions on core this morning, I dumped everything I had to the respective issue trackers. I'm not sure if I can get all of this reproduced on my private laptop when I'm away this weekend.

If it is about the overload case: well, you said it would be easy because objfpc already did it right :-)

Ondrej Pokorny
to Zoë Peterson: I'm absolutely thrilled to see this finally merged.  I sincerely appreciate all of the…
Zoë Peterson wrote 7 lines
Sven, I'm absolutely thrilled to see this finally merged.  I sincerely appreciate all of the work that you and Blaise put into it over such a long time.  I can't wait to see what the community is able to do with it.  Thank you, and thank you to the entire Free Pascal team for everything you've all built.

Sven & Blaise,

I thank you for this enormous achievement as well!

Ondrej

Hairy Pixels
to Sven Barth: The Free Pascal Developer team is pleased to finally announce the addition of a long…

I’ve had some time to play with this now and my first piece of feedback is that given my experience with other languages, the most common usage of closures is by passing them as arguments to functions.

Compared to the other languages I’m using now I’d say that we should be inferring more of the context of the receiving function type and not requiring the programmer to type out the full function header as in the example below (especially in the case there are no local variables declared).

Sort(function(left, right: Double): integer
                  begin
                    if left < right then
                      result := -1
                    else if left > right then
                      result := 1
                    else
                      result := 0;
                  end);

It’s hard to say what the best policy is for Pascal but some combination of the function/procedure keyword, parameter type names and return type could be omitted or shortened in various ways.

Given we know the function type from the parameter list we could infer most of the information and provide a more limited set of syntax, for example like this (Swift like):

Sort((left, right) begin
                    if left < right then
                      result := -1
                    else if left > right then
                      result := 1
                    else
                      result := 0;
                  end);

There is even the most limited shorthand (from Swift again) which uses token to represent the arguments as they are ordered.

Sort(begin
      if $0 < right then
        result := -1
      else if $0 > $1 then
        result := 1
      else
        result := 0;
    end);

Regards,
Ryan Joseph

Michael Van Canneyt
to Hairy Pixels
Hairy Pixels wrote 15 lines
I’ve had some time to play with this now and my first piece of feedback is that given my experience with other languages, the most common usage of closures is by passing them as arguments to functions. Compared to the other languages I’m using now I’d say that we should be inferring more of the context of the receiving function type and not requiring the programmer to type out the full function header as in the example below (especially in the case there are no local variables declared). Sort(function(left, right: Double): integer begin if left < right then result := -1 else if left > right then result := 1 else result := 0; end); It’s hard to say what the best policy is for Pascal but some combination of the function/procedure keyword, parameter type names and return type could be omitted or shortened in various ways.

Ah...

The desire to make a programming language terse and unreadable as a consequence. If you want that, use C#, Javascript or one of the ubiquitous languages for bracket fetishists. Scala & Rust top the bill in terms of unreadability.

So no, that's a "No pasarán".

Pascal is verbose and explicit, it enhances readability & clarity of code. Any proposal to undo that, is not acceptable.

If it was not for Delphi compatibility, I would kick out anonymous functions outright. Local functions can fulfill the need for closures perfectly, which results in more readable code as far as I am concerned.

Michael.

Hairy Pixels
to Michael Van Canneyt
Show quoted text 9 lines
On May 28, 2022, at 2:06 PM, Michael Van Canneyt via fpc-pascal wrote: Ah... The desire to make a programming language terse and unreadable as a consequence. If you want that, use C#, Javascript or one of the ubiquitous languages for bracket fetishists. Scala & Rust top the bill in terms of unreadability. So no, that's a "No pasarán".

Well that’s been my experience using both Swift and C# frequently over the last couple years that you don’t actually need to see the full function declaration in most instances because it’s inferred from the context. I’m actually finding it harder to read the Pascal syntax because there’s so much slammed in the function call.

Regards,
Ryan Joseph

Michael Van Canneyt
to Hairy Pixels
Hairy Pixels wrote 15 lines
> On May 28, 2022, at 2:06 PM, Michael Van Canneyt via fpc-pascal wrote: > > Ah... > > The desire to make a programming language terse and unreadable as a consequence. > If you want that, use C#, Javascript or one of the ubiquitous languages for bracket fetishists. > Scala & Rust top the bill in terms of unreadability. > > So no, that's a "No pasarán". Well that’s been my experience using both Swift and C# frequently over the last couple years that you don’t actually need to see the full function declaration in most instances because it’s inferred from the context.

I'm happy for the computer that it can infer things from the context in milliseconds. Really, I stand in awe...

And I'm sure it finds these very understandable (live examples):

[s: string]: ((s: string, cb: (done: any) => void) => void) & ((cb: (done: any) => void) => void) & {only: any, skip: any};
load: (url: string, onLoad: (object3D: THREE.Object3D) => void, onProgress?: (progress: ProgressEvent) => void, onError?: (event: ErrorEvent) => void) => void;
function comp<A, B, C, D>(c: (c: C) => D, b: (b: B) => C, a: (a: A) => B): (a: A) => D;
function comp<A, B, C, D, E>(d: (d: D) => E, c: (c: C) => D, b: (b: B) => C, a: (a: A) => B): (a: A) => E;
singleton = _.memoize(<T>(classInstance: new () => T) => new classInstance());
function cacheGetter(judgeFunc: () => boolean, returnCacheValueFunc: () => any, setCacheFunc: (returnVal: any) => void): (target: any, name: any, descriptor: any) => any;

However, I as a human have a brain that works at lower clock cycles, but even so I like to see at a glance what is intended without having to think too long on 'what on earth could missus X have meant here ?'.

That means providing a little more context/structure, and keeping the door firmly closed for syntax that allows absurd monstrosities as the things above. (and preferably lock it, take out the key and throw it away while we're at it)

This is - incidentally - also why I think pascal should never allow type definitions in function arguments.

For similar reasons I deplore the popularity of YAML or markdown, which all too often provide too little structure to grasp what is meant. By contrast XML is too much cruft, but e.g. JSON strikes a good balance.

Programming languages are meant for humans, not for computers.

If it were up to the computer, we'd probably simply write out programs as sequences of 0-s and 1-s. Guaranteed to be the shortest notation, but good luck trying to make sense of that as a human. I suspect punched cards are a thing of the past just for this reason.

The downside is that every human has his likes and dislikes.

But, if you actually prefer the above syntax to the more verbose pascal, you're in luck: There are many languages that will allow you to knock yourself out =-)

Michael.

Benito van der Zander
to Hairy Pixels: I’ve had some time to play with this now and my first piece of feedback is that given…

Hi,

Show quoted text 8 lines
Sort((left, right) begin if left < right then result := -1 else if left > right then result := 1 else result := 0; end);

One could introduce the @ operator to get a reference to a block of code.

Sort( @(left, right) begin
                     if left < right then
                       result := -1
                     else if left > right then
                       result := 1
                     else
                       result := 0;
                   end);

The "result" variable is also ugly. You could make "if" an expression:

Sort( @(left, right) begin
                     result :=
                       if left < right then -1
                       else if left > right then 1
                       else 0;
                   end);
Then the begin end could be removed too. Make @:= an operator to turn an
expression to a function
Sort( @(left, right) :=
                        if left < right then -1
                        else if left > right then 1
                        else 0;
                   );

Benito

Hairy Pixels wrote 44 lines
I’ve had some time to play with this now and my first piece of feedback is that given my experience with other languages, the most common usage of closures is by passing them as arguments to functions. Compared to the other languages I’m using now I’d say that we should be inferring more of the context of the receiving function type and not requiring the programmer to type out the full function header as in the example below (especially in the case there are no local variables declared). Sort(function(left, right: Double): integer begin if left < right then result := -1 else if left > right then result := 1 else result := 0; end); It’s hard to say what the best policy is for Pascal but some combination of the function/procedure keyword, parameter type names and return type could be omitted or shortened in various ways. Given we know the function type from the parameter list we could infer most of the information and provide a more limited set of syntax, for example like this (Swift like): Sort((left, right) begin if left < right then result := -1 else if left > right then result := 1 else result := 0; end); There is even the most limited shorthand (from Swift again) which uses token to represent the arguments as they are ordered. Sort(begin if $0 < right then result := -1 else if $0 > $1 then result := 1 else result := 0; end); Regards, Ryan Joseph _______________________________________________ fpc-pascal maillist https://lists.freepascal.org/cgi-bin/mailman/listinfo/fpc-pascal
Hairy Pixels
to Michael Van Canneyt: I'm happy for the computer that it can infer things from the context in milliseconds.…
Show quoted text 11 lines
On May 28, 2022, at 6:39 PM, Michael Van Canneyt via fpc-pascal wrote: And I'm sure it finds these very understandable (live examples): [s: string]: ((s: string, cb: (done: any) => void) => void) & ((cb: (done: any) => void) => void) & {only: any, skip: any}; load: (url: string, onLoad: (object3D: THREE.Object3D) => void, onProgress?: (progress: ProgressEvent) => void, onError?: (event: ErrorEvent) => void) => void; function comp<A, B, C, D>(c: (c: C) => D, b: (b: B) => C, a: (a: A) => B): (a: A) => D; function comp<A, B, C, D, E>(d: (d: D) => E, c: (c: C) => D, b: (b: B) => C, a: (a: A) => B): (a: A) => E; singleton = _.memoize(<T>(classInstance: new () => T) => new classInstance()); function cacheGetter(judgeFunc: () => boolean, returnCacheValueFunc: () => any, setCacheFunc: (returnVal: any) => void): (target: any, name: any, descriptor: any) => any;

Yes I see your point here but this can happen to any language with closures and is something FPC needs to reckon with now. Having verbose function declarations could make these kind of nested closures even worse than this.

I’m not up to date on how languages like JavaScript are dealing with this problem in their inherently concurrent code but I think they’re moving away from callbacks and closures. See http://callbackhell.com.

My point has to do with the best case scenario where adding a function which mean the user needs to scroll up dozens of lines to see what the function does even though it may be only a single expression. This is very common for filter, map etc… container functions. Adding in a fat function declaration doesn’t help the programmer since they already know what the TList.Filter function does and the extra lines make it hard to see what the actual code does.

For example you want to filter out items from a list with a value of less than 10. The function is only a single expression so wrapping it up in a 3 extra lines of code is just obscuring the code in my opinion.

Swift even has a convention for if the last parameter is a function you can call it with something like this:

	list.Filter begin
			  result := $0 < 10;
		      end;

I really don’t see how the extra "function (item: integer): boolean” makes that easier to read or adds more information. Maybe if you started doing stupid nesting things it will make a difference?

list.Filter begin
              result := $0.PeformCheck begin
                                           result := $0 < 10;
                                         end;
            end;
list.Filter(function (item: integer): boolean
            begin
              result := item.PeformCheck(function: boolean
                                         begin
                                           result := self < 10;
                                         end);
            end);

They both look not great to me but I think this is more with the nesting aspect than the function declaration.

What about plain procedures with no parameters?

timer.Finished(procedure
               begin
                 DoTheNextThing;
               end);

Or this:

timer.Finished(procedure begin DoTheNextThing end);

I’m just not seeing how it helps to write in the procedure keyword in these cases.

Anyways, it’s too early to know how this is going to work and I don’t do much concurrent code in Pascal so I likely won’t ever have a chance to write deeply nested functions but lets see what other people end of doing and how it looks.

Regards,
Ryan Joseph

Hairy Pixels
to Benito van der Zander: One could introduce the @ operator to get a reference to a block of code. Sort( @(left,…
Show quoted text 10 lines
On May 28, 2022, at 7:04 PM, Benito van der Zander via fpc-pascal wrote: The "result" variable is also ugly. You could make "if" an expression: Sort( @(left, right) begin result := if left < right then -1 else if left > right then 1 else 0; end);

Swift, C# and JavaScript etc… allow you to omit the return statement if the function body is a single expression. I don’t think any languages I’m aware of break the rules of expression though like this is proposing but you could use exit() and it would look a little better.

Show quoted text 10 lines
Then the begin end could be removed too. Make @:= an operator to turn an expression to a function Sort( @(left, right) := if left < right then -1 else if left > right then 1 else 0; );
I don’t think you need the @ operator here since the ():= is enough to denote a function. This is like the “arrow” syntax in JavaScript and it makes sense to me. It’s a good idea I think.

Come to think of it is FPC going to support capture lists? Right now the entire scope is captured but this is not always ideal. Right now there’s no syntax currently available that would support that.

Regards,
Ryan Joseph

Hairy Pixels
to Marco van de Voort: Ditto! https://gitlab.com/freepascal.org/lazarus/lazarus/-/issues/39774 This ticket also…
Show quoted text 5 lines
On May 27, 2022, at 6:31 PM, Marco van de Voort via fpc-pascal wrote: Ditto! https://gitlab.com/freepascal.org/lazarus/lazarus/-/issues/39774 This ticket also contains an experimental patch for anonymous method queue and synchronize. (required to reproduce)

Yeah code tools is basically broken right now. I don’t think I can even use it right now from my language server because I get the error: invalid mode switch “functionreferences”. Hopefully this can be fixed soon.

Regards,
Ryan Joseph

Thomas Kurz
to Benito van der Zander: One could introduce the @ operator to get a reference to a block of code. Sort( @(left,…

This is horrendous code, especially the last example. Looks like PHP or JavaScript, which I hate because their code is hard to read. I'm using pascal because I like its clean and easy-to-read syntax.

"-1" is a statement that should give a compiler error. "Result" and "Exit(x)" are established constructs and I don't see any need for changing, or even worse: omitting, them.

What should the data type of "left, right" be?

What should "@" mean?

Earlier message 53 lines
----- Original Message ----- From: Benito van der Zander via fpc-pascal To: Sent: Saturday, May 28, 2022, 14:04:46 Subject: [fpc-pascal] Feature Announcement: Function References and Anonymous Functions Hi, > Sort((left, right) begin > if left < right then > result := -1 > else if left > right then > result := 1 > else > result := 0; > end); One could introduce the @ operator to get a reference to a block of code. Sort( @(left, right) begin if left < right then result := -1 else if left > right then result := 1 else result := 0; end); The "result" variable is also ugly. You could make "if" an expression: Sort( @(left, right) begin result := if left < right then -1 else if left > right then 1 else 0; end); Then the begin end could be removed too. Make @:= an operator to turn an expression to a function Sort( @(left, right) := if left < right then -1 else if left > right then 1 else 0; ); Benito
Sven Barth
to Benito van der Zander: One could introduce the @ operator to get a reference to a block of code. Sort( @(left,…
Benito van der Zander wrote 21 lines
Hi, > Sort((left, right) begin > if left < right then > result := -1 > else if left > right then > result := 1 > else > result := 0; > end); One could introduce the @ operator to get a reference to a block of code. Sort( @(left, right) begin if left < right then result := -1 else if left > right then result := 1 else result := 0; end);

The sequence "@(identifier" is already a valid sequence (because "@(SomeVar)" is valid) and I'm definitely and absolutely against introducing another ambigous syntax as I already have enough headaches with Delphi's generic syntax.

Show quoted text 9 lines
The "result" variable is also ugly. You could make "if" an expression: Sort( @(left, right) begin result := if left < right then -1 else if left > right then 1 else 0; end);

Allowing if (and case) to be an expression, too, is something I'm still toying with independantly of this...

Show quoted text 10 lines
Then the begin end could be removed too. Make @:= an operator to turn an expression to a function Sort( @(left, right) := if left < right then -1 else if left > right then 1 else 0; );

I personally would have a bit more verbose syntax though it would have the same problem regarding type inference which would be an absolute PITA to implement (not to mention the opposition from the other core devs):

=== code begin ===

Sort(lamba (left, right) as
  if left < right then -1
  else if left > right then 1
  else 0);

=== code end ===

Regards,
Sven

Bart
to Sven Barth
AM Sven Barth wrote 7 lines
=== code begin === Sort(lamba (left, right) as if left < right then -1 else if left > right then 1 else 0);

This doesn't look like definitively hope this is not the future of our beautiful language.

Bart