Using Anonymous Methods in Delphi
The reason for writing the article was an interest in the capabilities of anonymous functions in Delphi. In different sources you can find their theoretical foundations, information about the internal structure, but examples of use are everywhere given some kind of trivial. And many ask questions: why are these references needed at all , what can be the use of their application? Therefore, I propose some options for using anonymous methods used in other languages, possibly more oriented towards a functional programming style.
To simplify and illustrate, consider operations on a numerical array, although the approach itself is applicable to any ordered containers (for example, TList) A dynamic array is not an object type, therefore, to expand its functionality, we use a helper. Type of elements, select Double:
Most of the methods described below take a function as an argument and call it for each element (or several elements) of the array. In most cases, a single argument is passed to the specified function: the value of the array element. More advanced implementations are possible, in which not only the value is transmitted, but also the index of the element and a reference to the array itself. None of the methods modify the original array, however, the function passed to these methods can do this.
The ForEach method traverses the elements of the array and calls the specified function for each of them. As mentioned above, the function is passed to the ForEach method in an argument. When this function is called, the ForEach method will pass it the value of the array element, its index, as well as the Boolean variable Done, assigning True to which will allow iterations to be terminated and exit the method (similar to the Break instruction for the usual for loop). For instance:
Implementation of the ForEach method:
The Map method passes each element of the array with respect to which it is called to the specified function and returns an array of values returned by this function. For instance:
The Map method calls the function in the same way as the ForEach method. However, the function passed to the Map method must return a value. Note that Map returns a new array: it does not modify the original array.
Implementation of the Map method:
The Filter method returns an array containing a subset of the elements of the original array. The function passed to it must be a predicate function, because should return True or False. The Filter method calls the function in the same way as the ForEach and Map methods. If True is returned, the element passed to the function is considered a member of the subset and added to the array returned by the method. For instance:
Implementation of the Filter method:
Every and Some methods are array predicates: they apply the specified predicate function to array elements and return True or False. Every method resembles the mathematical universal quantifier ∀: it returns True only if the predicate function you passed returns True for all elements of the array:
Some method resembles the mathematical existential quantifier ∃: it returns True if there is at least one element in the array for which the predicate function returns True, and False is returned by the method only if the predicate function returns False for all elements of the array:
Implementation of Every and Some methods:
Note that both methods, Every and Some, stop traversing array elements as soon as the result becomes known. The Some method returns True as soon as the predicate function returns True, and traverses all elements of the array only if the predicate function always returns False. Every method is the exact opposite: it returns False as soon as the predicate function returns False, and traverses all elements of the array only if the predicate function always returns True. Also, note that according to the rules of mathematics for an empty array, the Every method returns True, and the Some method returns False.
The Reduce and ReduceRight methods combine the elements of an array using the function you specify and return a single value. This is a typical operation in functional programming, where it is also known as “convolution”. The examples below will help to understand the essence of this operation:
The Reduce method takes two arguments. In the second, a function is passed that performs the convolution operation. The task of this function is to combine in some way or collapse the two values into one to return the minimized value. In the examples above, the functions combine the two values by adding them, multiplying and choosing the largest. The first argument passes the initial value for the function.
The functions passed to the Reduce method are different from the functions passed to the ForEach and Map methods. The value of the array element is passed to them in the second argument, and the accumulated convolution result is transferred in the first argument. The first time the function is called in the first argument, the initial value is passed, passed to the Reduce method in the first argument. In all subsequent calls, the value obtained as a result of the previous function call is transmitted. In the first example, from the above, the convolution function will first be called with arguments 0 and 1. It will add these numbers and return 1. Then it will be called with arguments 1 and 2 and return 3. Then it will calculate 3 + 3 = 6, then 6 + 4 = 10 and finally 10 + 5 = 15. This last value of 15 will be returned by the Reduce method.
In the third call, in the example above, the only argument is passed to the Reduce method: the initial value is not specified here. This alternative implementation of the Reduce method uses the first element of the array as the initial value. This means that the first and second arguments to the array will be passed the first time the convolution function is called. In the examples of calculating the sum and the product, in the same way one could apply this alternative implementation of Reduce and omit the argument with the initial value.
Calling the Reduce method with an empty array without an initial value will throw an exception. If you call a method with a single value - with an array containing a single element, and without an initial value or with an empty array and an initial value - it simply returns this single value without calling the convolution function.
Implementing Reduce Methods:
The ReduceRight method works exactly the same as the Reduce method, except that the array is processed in the reverse order, from large indexes to smaller ones (from right to left). This may be necessary if the convolution operation has associativity from right to left, for example:
Implementation of the ReduceRight Method:
It should be noted that the Every and Some methods described above are a peculiar kind of array convolution operation. However, they differ in that they seek to complete the traversal of the array as early as possible and do not always check the values of all its elements.
Consider another example of using anonymous methods. Suppose we have an array of numbers and we need to find the average value and standard deviation for these values:
Sources The
article has been improved, thanks to your comments.
And here is the continuation , which is devoted to closures and functions of higher orders.
To simplify and illustrate, consider operations on a numerical array, although the approach itself is applicable to any ordered containers (for example, TList
uses
SysUtils, Math;
type
TArrayHelper = record helper for TArray
strict private type
TForEachRef = reference to procedure(X: Double; I: Integer; var Done: Boolean);
TMapRef = reference to function(X: Double): Double;
TFilterRef = reference to function(X: Double; I: Integer): Boolean;
TPredicateRef = reference to function(X: Double): Boolean;
TReduceRef = reference to function(Accumulator, X: Double): Double;
public
function ToString: string;
procedure ForEach(Lambda: TForEachRef);
function Map(Lambda: TMapRef): TArray;
function Filter(Lambda: TFilterRef): TArray;
function Every(Lambda: TPredicateRef): Boolean;
function Some(Lambda: TPredicateRef): Boolean;
function Reduce(Lambda: TReduceRef): Double; overload;
function Reduce(Init: Double; Lambda: TReduceRef): Double; overload;
function ReduceRight(Lambda: TReduceRef): Double;
end;
Most of the methods described below take a function as an argument and call it for each element (or several elements) of the array. In most cases, a single argument is passed to the specified function: the value of the array element. More advanced implementations are possible, in which not only the value is transmitted, but also the index of the element and a reference to the array itself. None of the methods modify the original array, however, the function passed to these methods can do this.
ForEach Method
The ForEach method traverses the elements of the array and calls the specified function for each of them. As mentioned above, the function is passed to the ForEach method in an argument. When this function is called, the ForEach method will pass it the value of the array element, its index, as well as the Boolean variable Done, assigning True to which will allow iterations to be terminated and exit the method (similar to the Break instruction for the usual for loop). For instance:
var
A: TArray;
begin
A := [1, 2, 3]; // Использование литералов массивов стало возможным в XE7
// Умножить все элементы массива на 2
A.ForEach(procedure(X: Double; I: Integer; var Done: Boolean)
begin
A[I] := X * 2;
if I = 1 then
Done := True; // Досрочный выход из ForEach
end);
WriteLn(A.ToString); // => [2, 4, 3]
end;
Implementation of the ForEach method:
procedure TArrayHelper.ForEach(Lambda: TForEachRef);
var
I: Integer;
Done: Boolean;
begin
Done := False;
for I := 0 to High(Self) do
begin
Lambda(Self[I], I, Done);
if Done then Break;
end;
end;
// Вспомогательный метод: преобразование массива в строку
function TArrayHelper.ToString: string;
var
Res: TArray;
begin
if Length(Self) = 0 then Exit('[]');
ForEach(procedure(X: Double; I: Integer; var Done: Boolean)
begin
Res := Res + [FloatToStr(X)];
end);
Result := '[' + string.Join(', ', Res) + ']';
end;
Map Method
The Map method passes each element of the array with respect to which it is called to the specified function and returns an array of values returned by this function. For instance:
var
A, R: TArray;
begin
A := [1, 2, 3];
// Вычислить квадраты всех элементов
R := A.Map(function(X: Double): Double
begin
Result := X * X;
end);
WriteLn(R.ToString); // => [1, 4, 9]
end;
The Map method calls the function in the same way as the ForEach method. However, the function passed to the Map method must return a value. Note that Map returns a new array: it does not modify the original array.
Implementation of the Map method:
function TArrayHelper.Map(Lambda: TMapRef): TArray;
var
X: Double;
begin
for X in Self do
Result := Result + [Lambda(X)];
end;
Filter Method
The Filter method returns an array containing a subset of the elements of the original array. The function passed to it must be a predicate function, because should return True or False. The Filter method calls the function in the same way as the ForEach and Map methods. If True is returned, the element passed to the function is considered a member of the subset and added to the array returned by the method. For instance:
var
Data: TArray;
MidValues: TArray;
begin
Data := [5, 4, 3, 2, 1];
// Фильтровать элементы, большме 1, но меньшие 5
MidValues := Data.Filter(function(X: Double; I: Integer): Boolean
begin
Result := (1 < X) and (X < 5);
end);
WriteLn(MidValues.ToString); // => [4, 3, 2]
// Каскад
Data
.Map(function(X: Double): Double
begin
Result := X + 5; // Увеличить каждый элемент на 5.
end)
.Filter(function(X: Double; I: Integer): Boolean
begin
Result := (I mod 2 = 0); // Фильтровать элементы с четными номерами
end)
.ForEach(procedure(X: Double; I: Integer; var Done: Boolean)
begin
Write(X:2:0) // => 10 8 6
end);
end;
Implementation of the Filter method:
function TArrayHelper.Filter(Lambda: TFilterRef): TArray;
var
I: Integer;
begin
for I := 0 to High(Self) do
if Lambda(Self[I], I) then
Result := Result + [Self[I]];
end;
Every and Some Methods
Every and Some methods are array predicates: they apply the specified predicate function to array elements and return True or False. Every method resembles the mathematical universal quantifier ∀: it returns True only if the predicate function you passed returns True for all elements of the array:
var
A: TArray;
B: Boolean;
begin
A := [1, 2.7, 3, 4, 5];
B := A.Every(function(X: Double): Boolean
begin
Result := (X < 10);
end);
WriteLn(B); // => True: все значения < 10.
B := A.Every(function(X: Double): Boolean
begin
Result := (Frac(X) = 0);
end);
WriteLn(B); // => False: имеются числа с дробной частью.
end;
Some method resembles the mathematical existential quantifier ∃: it returns True if there is at least one element in the array for which the predicate function returns True, and False is returned by the method only if the predicate function returns False for all elements of the array:
var
A: TArray;
B: Boolean;
begin
A := [1, 2.7, 3, 4, 5];
B := A.Some(function(X: Double): Boolean
begin
Result := (Frac(X) = 0);
end);
WriteLn(B); // => True: имеются числа без дробной части.
end;
Implementation of Every and Some methods:
function TArrayHelper.Every(Lambda: TPredicateRef): Boolean;
var
X: Double;
begin
Result := True;
for X in Self do
if not Lambda(X) then Exit(False);
end;
function TArrayHelper.Some(Lambda: TPredicateRef): Boolean;
var
X: Double;
begin
Result := False;
for X in Self do
if Lambda(X) then Exit(True);
end;
Note that both methods, Every and Some, stop traversing array elements as soon as the result becomes known. The Some method returns True as soon as the predicate function returns True, and traverses all elements of the array only if the predicate function always returns False. Every method is the exact opposite: it returns False as soon as the predicate function returns False, and traverses all elements of the array only if the predicate function always returns True. Also, note that according to the rules of mathematics for an empty array, the Every method returns True, and the Some method returns False.
Reduce and ReduceRight Methods
The Reduce and ReduceRight methods combine the elements of an array using the function you specify and return a single value. This is a typical operation in functional programming, where it is also known as “convolution”. The examples below will help to understand the essence of this operation:
var
A: TArray;
Total, Product, Max: Double;
begin
A := [1, 2, 3, 4, 5];
// Сумма значений
Total := A.Reduce(0, function(X, Y: Double): Double
begin
Result := X + Y;
end);
WriteLn(Total); // => 15.0
// Произведение значений
Product := A.Reduce(1, function(X, Y: Double): Double
begin
Result := X * Y;
end);
WriteLn(Product); // => 120.0
// Наибольшее значение (используется альтернативная реализация Reduce)
Max := A.Reduce(function(X, Y: Double): Double
begin
if X > Y then Exit(X) else Exit(Y);
end);
WriteLn(Max); // => 5.0
end;
The Reduce method takes two arguments. In the second, a function is passed that performs the convolution operation. The task of this function is to combine in some way or collapse the two values into one to return the minimized value. In the examples above, the functions combine the two values by adding them, multiplying and choosing the largest. The first argument passes the initial value for the function.
The functions passed to the Reduce method are different from the functions passed to the ForEach and Map methods. The value of the array element is passed to them in the second argument, and the accumulated convolution result is transferred in the first argument. The first time the function is called in the first argument, the initial value is passed, passed to the Reduce method in the first argument. In all subsequent calls, the value obtained as a result of the previous function call is transmitted. In the first example, from the above, the convolution function will first be called with arguments 0 and 1. It will add these numbers and return 1. Then it will be called with arguments 1 and 2 and return 3. Then it will calculate 3 + 3 = 6, then 6 + 4 = 10 and finally 10 + 5 = 15. This last value of 15 will be returned by the Reduce method.
In the third call, in the example above, the only argument is passed to the Reduce method: the initial value is not specified here. This alternative implementation of the Reduce method uses the first element of the array as the initial value. This means that the first and second arguments to the array will be passed the first time the convolution function is called. In the examples of calculating the sum and the product, in the same way one could apply this alternative implementation of Reduce and omit the argument with the initial value.
Calling the Reduce method with an empty array without an initial value will throw an exception. If you call a method with a single value - with an array containing a single element, and without an initial value or with an empty array and an initial value - it simply returns this single value without calling the convolution function.
Implementing Reduce Methods:
function TArrayHelper.Reduce(Init: Double; Lambda: TReduceRef): Double;
var
I: Integer;
begin
Result := Init;
if Length(Self) = 0 then Exit;
for I := 0 to High(Self) do
Result := Lambda(Result, Self[I]);
end;
// Альтернативная реализация Reduce – с одним аргументом
function TArrayHelper.Reduce(Lambda: TReduceRef): Double;
var
I: Integer;
begin
Result := Self[0];
if Length(Self) = 1 then Exit;
for I := 1 to High(Self) do
Result := Lambda(Result, Self[I]);
end;
The ReduceRight method works exactly the same as the Reduce method, except that the array is processed in the reverse order, from large indexes to smaller ones (from right to left). This may be necessary if the convolution operation has associativity from right to left, for example:
var
A: TArray;
Big: Double;
begin
A := [2, 3, 4];
// Вычислить 2^(3^4).
// Операция возведения в степень имеет ассоциативность справа налево
Big := A.ReduceRight(function(Accumulator, Value: Double): Double
begin
Result := Math.Power(Value, Accumulator);
end);
Writeln(Big); // => 2.41785163922926E+0024
end;
Implementation of the ReduceRight Method:
function TArrayHelper.ReduceRight(Lambda: TReduceRef): Double;
var
I: Integer;
begin
Result := Self[High(Self)];
if Length(Self) = 1 then Exit;
for I := High(Self) - 1 downto 0 do
Result := Lambda(Result, Self[I]);
end;
It should be noted that the Every and Some methods described above are a peculiar kind of array convolution operation. However, they differ in that they seek to complete the traversal of the array as early as possible and do not always check the values of all its elements.
Instead of a conclusion
Consider another example of using anonymous methods. Suppose we have an array of numbers and we need to find the average value and standard deviation for these values:
// Вспомогательная функция: вычисление суммы аргументов.
// Свободную функцию (как и метод экземпляра) можно использовать
// в качестве параметра для метода, принимающего reference-тип
function Sum(X, Y: Double): Double;
begin
Result := X + Y;
end;
// Вычисление среднего значения (Mean) и СКО (StdDev).
procedure MeanAndStdDev;
var
Data: TArray;
Mean, StdDev: Double;
begin
Data := [1, 1, 3, 5, 5];
Mean := Data.Reduce(Sum) / Length(Data);
StdDev := Sqrt(Data
.Map(function(V: Double): Double
begin
Result := Sqr(V - Mean); // Квадраты разностей
end)
.Reduce(Sum) / Pred(Length(Data)));
WriteLn('Mean: ', Mean, ' StdDev: ', StdDev); // => Mean: 3.0 StdDev: 2.0
end;
Sources The
article has been improved, thanks to your comments.
And here is the continuation , which is devoted to closures and functions of higher orders.