Anonymous Methods in Delphi

From RAD Studio
Jump to: navigation, search

Go Up to Procedures and Functions Index


As the name suggests, an anonymous method is a procedure or function that does not have a name associated with it. An anonymous method treats a block of code as an entity that can be assigned to a variable or used as a parameter to a method. In addition, an anonymous method can refer to variables and bind values to the variables in the context in which the method is defined. Anonymous methods can be defined and used with simple syntax. They are similar to the construct of closures defined in other languages.

Note: This topic covers handling Delphi anonymous method in Delphi code. For C++ code, see How to Handle Delphi Anonymous Methods in C++.

Syntax

An anonymous method is defined similarly to a regular procedure or function, but with no name.

For example, this function returns a function that is defined as an anonymous method:

function MakeAdder(y: Integer): TFuncOfInt;
begin
  Result := { start anonymous method } function(x: Integer) : Integer
  begin
    Result := x + y;
  end; { end anonymous method }
end;

The function MakeAdder returns a function that it declares with no name: an anonymous method.

Note that MakeAdder returns a value of type TFuncOfInt. An anonymous method type is declared as a reference to a method:

type
  TFuncOfInt = reference to function(x: Integer): Integer;

This declaration indicates that the anonymous method:

  • is a function
  • takes one integer parameter
  • returns an integer value.

In general, an anonymous function type is declared for either a procedure or function:

type
  TType1 = reference to procedure (parameterlist);
  TType2 = reference to function (parameterlist): returntype;

where (parameterlist) are optional.

Here are a couple of examples of types:

type
  TSimpleProcedure = reference to procedure;
  TSimpleFunction = reference to function(x: string): Integer;

An anonymous method is declared as a procedure or function without a name:

// Procedure
procedure (parameters)
begin
  { statement block }
end;
// Function
function (parameters): returntype
begin
  { statement block }
end;

where (parameters) are optional.

Using Anonymous Methods

Anonymous methods are typically assigned to something, as in these examples:

myFunc := function(x: Integer): string
begin
  Result := IntToStr(x);
end;

myProc := procedure(x: Integer)
begin
  Writeln(x);
end;

Anonymous methods may also be returned by functions or passed as values for parameters when calling methods. For instance, using the anonymous method variable myFunc defined just above:

type
  TFuncOfIntToString = reference to function(x: Integer): string;

procedure AnalyzeFunction(proc: TFuncOfIntToString);
begin
  { some code }
end;

// Call procedure with anonymous method as parameter
// Using variable:
AnalyzeFunction(myFunc);

// Use anonymous method directly:
AnalyzeFunction(function(x: Integer): string
begin
  Result := IntToStr(x);
end;)

Method references can also be assigned to methods as well as anonymous methods. For example:

type
  TMethRef = reference to procedure(x: Integer);
TMyClass = class
  procedure Method(x: Integer);
end;

var
  m: TMethRef;
  i: TMyClass;
begin
  // ...
  m := i.Method;   //assigning to method reference
end;

However, the converse is not true: you cannot assign an anonymous method to a regular method pointer. Method references are managed types, but method pointers are unmanaged types. Thus, for type-safety reasons, assigning method references to method pointers is not supported. For instance, events are method pointer-valued properties, so you cannot use an anonymous method for an event. See the section on variable binding for more information on this restriction.

Anonymous Methods Variable Binding

A key feature of anonymous methods is that they may reference variables that are visible to them where they were defined. Furthermore, these variables can be bound to values and wrapped up with a reference to the anonymous method. This captures state and extends the lifetime of variables.

Variable Binding Illustration

Consider again the function defined above:

function MakeAdder(y: Integer): TFuncOfInt;
begin
  Result := function(x: Integer): Integer
  begin
    Result := x + y;
  end;
end;

We can create an instance of this function that binds a variable value:

var
  adder: TFuncOfInt;
begin
  adder := MakeAdder(20);
  Writeln(adder(22)); // prints 42
end.

The variable adder contains an anonymous method that binds the value 20 to the variable y referenced in the anonymous method's code block. This binding persists even if the value goes out of scope.

Anonymous Methods as Events

A motivation for using method references is to have a type that can contain bound variables, also known as closure values. Since closures close over their defining environment, including any local variables referenced at the point of definition, they have state that must be freed. Method references are managed types (they are reference counted), so they can keep track of this state and free it when necessary. If a method reference or closure could be freely assigned to a method pointer, such as an event, then it would be easy to create ill-typed programs with dangling pointers or memory leaks.

Delphi events are a convention for properties. There is no difference between an event and a property, except for the kind of type. If a property is of a method pointer type, then it is an event.

If a property is of a method reference type, then it should logically be considered an event too. However the IDE does not treat it as an event. This matters for classes that are installed into the IDE as components and custom controls.

Therefore, to have an event on a component or custom control that can be assigned to using a method reference or a closure value, the property must be of a method reference type. However, this is inconvenient, because the IDE does not recognize it as an event.

Here is an example of using a property with a method reference type, so it can operate as an event:

type
  TProc = reference to procedure;
  TMyComponent = class(TComponent)
  private
    FMyEvent: TProc;
  public
    // MyEvent property serves as an event:
    property MyEvent: TProc read FMyEvent write FMyEvent;
    // some other code invokes FMyEvent as usual pattern for events
  end;

// …

var
  c: TMyComponent;
begin
  c := TMyComponent.Create(Self);
  c.MyEvent := procedure
  begin
    ShowMessage('Hello World!'); // shown when TMyComponent invokes MyEvent
  end;
end;

Variable Binding Mechanism

To avoid creating memory leaks, it is useful to understand the variable binding process in greater detail.

Local variables defined at the start of a procedure, function or method (hereafter "routine") normally live only as long as that routine is active. Anonymous methods can extend these variables' lifetimes.

If an anonymous method refers to an outer local variable in its body, that variable is "captured". Capturing means extending the lifetime of the variable, so that it lives as long as the anonymous method value, rather than dying with its declaring routine. Note that variable capture captures variables--not values. If a variable's value changes after being captured by constructing an anonymous method, the value of the variable the anonymous method captured changes too, because they are the same variable with the same storage. Captured variables are stored on the heap, not the stack.

Anonymous method values are of the method reference type, and are reference-counted. When the last method reference to a given anonymous method value goes out of scope, or is cleared (initialized to nil) or finalized, the variables it has captured finally go out of scope.

This situation is more complicated in the case of multiple anonymous methods capturing the same local variable. To understand how this works in all situations, it is necessary to be more precise about the mechanics of the implementation.

Whenever a local variable is captured, it is added to a "frame object" associated with its declaring routine. Every anonymous method declared in a routine is converted into a method on the frame object associated with its containing routine. Finally, any frame object created because of an anonymous method value being constructed or variable being captured is chained to its parent frame by another reference--if any such frame exists and if necessary to access a captured outer variable. These links from one frame object to its parent are also reference counted. An anonymous method declared in a nested, local routine that captures variables from its parent routine keeps that parent frame object alive until it itself goes out of scope.

For example, consider this situation:

type
  TProc = reference to procedure;
procedure Call(proc: TProc);
// ...
procedure Use(x: Integer);
// ...

procedure L1; // frame F1
var
  v1: Integer;

  procedure L2; // frame F1_1
  begin
    Call(procedure // frame F1_1_1
    begin
      Use(v1);
    end);
  end;

begin
  Call(procedure // frame F1_2
  var
    v2: Integer;
  begin
    Use(v1);
    Call(procedure // frame F1_2_1
    begin
      Use(v2);
    end);
  end);
end;

Each routine and anonymous method is annotated with a frame identifier to make it easier to identify which frame object links to which:

  • v1 is a variable in F1
  • v2 is a variable in F1_2 (captured by F1_2_1)
  • anonymous method for F1_1_1 is a method in F1_1
  • F1_1 links to F1 (F1_1_1 uses v1)
  • anonymous method for F1_2 is a method in F1
  • anonymous method for F1_2_1 is a method in F1_2

Frames F1_2_1 and F1_1_1 do not need frame objects, since they neither declare anonymous methods nor have variables that are captured. They are not on any path of parentage between a nested anonymous method and an outer captured variable either. (They have implicit frames stored on the stack.)

Given only a reference to the anonymous method F1_2_1, variables v1 and v2 are kept alive. If instead, the only reference that outlives the invocation of F1 is F1_1_1, only variable v1 is kept alive.

It is possible to create a cycle in the method reference/frame link chains that causes a memory leak. For example, storing an anonymous method directly or indirectly in a variable that the anonymous method itself captures creates a cycle, causing a memory leak.

Utility of Anonymous Methods

Anonymous methods offer more than just a simple pointer to something that is callable. They provide several advantages:

  • binding variable values
  • easy way to define and use methods
  • easy to parameterize using code

Variable Binding

Anonymous methods provide a block of code along with variable bindings to the environment in which they are defined, even if that environment is not in scope. A pointer to a function or procedure cannot do that.

For instance, the statement adder := MakeAdder(20); from the code sample above produces a variable adder that encapsulates the binding of a variable to the value 20.

Some other languages that implement such a construct refer to them as closures. Historically, the idea was that evaluating an expression like adder := MakeAdder(20); produced a closure. It represents an object that contains references to the bindings of all variables referenced in the function and defined outside it, thus closing it by capturing the values of the variables.

Ease of Use

The following sample shows a typical class definition to define some simple methods and then invoke them:

type
  TMethodPointer = procedure of object; // delegate void TMethodPointer();
  TStringToInt = function(x: string): Integer of object;

TObj = class
  procedure HelloWorld;
  function GetLength(x: string): Integer;
end;

procedure TObj.HelloWorld;
begin
  Writeln('Hello World');
end;

function TObj.GetLength(x: string): Integer;
begin
  Result := Length(x);
end;

var
  x: TMethodPointer;
  y: TStringToInt;
  obj: TObj;

begin
  obj := TObj.Create;

  x := obj.HelloWorld;
  x;
  y := obj.GetLength;
  Writeln(y('foo'));
end.

Contrast this to the same methods defined and invoked using anonymous methods:

type
  TSimpleProcedure = reference to procedure;
  TSimpleFunction = reference to function(x: string): Integer;

var
  x1: TSimpleProcedure;
  y1: TSimpleFunction;

begin
  x1 := procedure
    begin
      Writeln('Hello World');
    end;
  x1;   //invoke anonymous method just defined

  y1 := function(x: string): Integer
    begin
      Result := Length(x);
    end;
  Writeln(y1('bar'));
end.

Notice how much simpler and shorter the code is that uses anonymous methods. This is ideal if you want to explicitly and simply define these methods and use them immediately without the overhead and effort of creating a class that may never be used anywhere else. The resulting code is easier to understand.

Using Code for a Parameter

Anonymous methods make it easier to write functions and structures parameterized by code, not just values.

Multithreading is a good application for anonymous methods. if you want to execute some code in parallel, you might have a parallel-for function that looks like this:

type
  TProcOfInteger = reference to procedure(x: Integer);

procedure ParallelFor(start, finish: Integer; proc: TProcOfInteger);

The ParallelFor procedure iterates a procedure over different threads. Assuming this procedure is implemented correctly and efficiently using threads or a thread pool, it could then be easily used to take advantage of multiple processors:

procedure CalculateExpensiveThings;
var
  results: array of Integer;
begin
  SetLength(results, 100);
  ParallelFor(Low(results), High(results),
    procedure(i: Integer)                           // \
    begin                                           //  \ code block
      results[i] := ExpensiveCalculation(i);        //  /  used as parameter
    end                                             // /
    );
  // use results
  end;

Contrast this to how it would need to be done without anonymous methods: probably a "task" class with a virtual abstract method, with a concrete descendant for ExpensiveCalculation, and then adding all the tasks to a queue--not nearly as natural or integrated.

Here, the "parallel-for" algorithm is the abstraction that is being parameterized by code. In the past, a common way to implement this pattern is with a virtual base class with one or more abstract methods; consider the TThread class and its abstract Execute method. However, anonymous methods make this pattern--parameterizing of algorithms and data structures using code--far easier.

See Also