📜 ⬆️ ⬇️

Using anonymous methods in Delphi

The reason for writing this article was the interest in the possibilities of anonymous functions in Delphi. In various sources you can find their theoretical foundations, information about the internal structure, but here are some examples of the use of some trivial. And many people ask questions: why do we need these references , what could be the use of their use? Therefore, I propose some variants of using anonymous methods used in other languages, perhaps more oriented to a functional programming style.

For simplicity and clarity, we consider operations on a numeric array, although the approach itself is applicable to any ordered containers (for example, TList <T>). A dynamic array is not an object type, so we use a helper to extend its functionality. The item type is Double:

uses SysUtils, Math; type TArrayHelper = record helper for TArray<Double> 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<Double>; function Filter(Lambda: TFilterRef): TArray<Double>; 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 an array. In most cases, a single argument is passed to the specified function: the value of the element in the array. More advanced implementations are possible in which not only the value is transferred, but also the element index and the link to the array itself. None of the methods change the original array, but the function passed to these methods can do this.

ForEach method

The ForEach method performs a traversal of array elements and calls the specified function for each of them. As mentioned above, the function is passed to the ForEach method in the argument. When calling this function, the ForEach method will pass to it the value of the array element, its index, as well as the Boolean variable Done, the assignment of which True will allow to interrupt the iterations and exit the method (analogous to the Break instruction for a normal for loop). For example:
')
 var A: TArray<Double>; 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<string>; 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 for which it is called to the specified function, and returns an array of values ​​returned by this function. For example:

 var A, R: TArray<Double>; 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. Notice that Map returns a new array: it does not modify the original array.

Implementation of the Map method:
 function TArrayHelper.Map(Lambda: TMapRef): TArray<Double>; 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, since must 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 example:

 var Data: TArray<Double>; MidValues: TArray<Double>; 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; 

Implementing the Filter method:
 function TArrayHelper.Filter(Lambda: TFilterRef): TArray<Double>; 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

The Every and Some methods are array predicates: they apply the specified predicate function to the elements of the array and return True or False. The Every method resembles a mathematical quantifier of universality ∀: it returns True only if the predicate function you returned returned True for all elements of the array:

 var A: TArray<Double>; 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; 

The Some method resembles a mathematical existence quantifier ∃: it returns True if there is at least one element in the array for which the predicate function returns True, and the False value is returned by the method only if the predicate function returns False for all array elements:

 var A: TArray<Double>; 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; 

Implementing the 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 the elements of the array as soon as the result becomes known. The Some method returns True as soon as the predicate function returns True, and performs a crawl of all array elements only if the predicate function always returns False. The Every method is the exact opposite: it returns False as soon as the predicate function returns False, and performs a traversal of all array elements only if the predicate function always returns True. In addition, note that in accordance with 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<Double>; 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 two values ​​into one to return the collapsed value. In the examples above, the functions are performed by combining two values, adding them, multiplying and choosing the largest. The first argument is 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 in the first argument the accumulated result of the convolution is passed. The first call in the first argument of the function is passed the initial value passed to the Reduce method in the first argument. In all subsequent calls, the value resulting from 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 adds these numbers and returns 1. Then it is called with arguments 1 and 2 and returns 3. Then it will calculate 3 + 3 = 6, then 6 + 4 = 10 and, finally, 10 + 5 = 15. This last value 15 will be returned by the Reduce method.

In the third call, in the example above, a single 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 when the convolution function is first called, the first and second arguments of the array will be passed. In the examples of calculating the sum and 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 cause 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 that 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 in the same way as the Reduce method, except that the array is processed in the reverse order, from large indices to smaller ones (from right to left). This may be necessary if the convolution operation is associative from right to left, for example:

 var A: TArray<Double>; 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 conclusion

Consider another example of using anonymous methods. Suppose we have an array of numbers and we need to find the mean 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<Double>; 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 higher order functions.

Source: https://habr.com/ru/post/243721/


All Articles