Использование анонимных методов в Delphi

в 8:45, , рубрики: Delphi, замыкания, лямбда-функции, Программирование

Поводом для написания статьи стал интерес к возможностям анонимных функции в Delphi. В разных источниках можно найти их теоретические основы, информацию о внутреннем устройстве, а вот примеры использования везде даются какие-то тривиальные. И многие задают вопросы: а для чего вообще нужны эти reference, какая может быть польза от их применения? Поэтому предлагаю некоторые варианты использования анонимных методов, применяемые в других языках, возможно, более ориентированных на функциональный стиль программирования.

Для упрощения и наглядности рассмотрим операции над числовым массивом, хотя сам подход применим к любым упорядоченным контейнерам (например, TList). Динамический массив не является объектным типом, поэтому для расширения его функциональности используем хэлпер. Тип элементов выберем Double:

uses
  SysUtils, Math;
  
type
  TArrayHelper = record helper for TArray<Double>
  strict private type
    TForEachRef = reference to procedure(X: Double; I: Integer);
    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;

Большинство описываемых ниже методов принимают функцию в качестве аргумента и вызывают ее для каждого элемента (или нескольких элементов) массива. В большинстве случаев указанной функции передается один аргумент: значение элемента массива. Возможны более продвинутые реализации, в которых передается не только значение, но также индекс элемента и ссылка на сам массив. Ни один из методов не изменяет исходный массив, однако функция, передаваемая этим методам, может это делать.

Метод ForEach

Метод ForEach выполняет обход элементов массива и для каждого из них вызывает указанную функцию. Как уже говорилось выше, функция передается методу ForEach в аргументе. При вызове этой функции метод ForEach будет передавать ей значение элемента массива и его индекс. Например:

var
  A: TArray<Double>;  
begin
  A := [1, 2, 3]; // Использование литералов массивов стало возможным в XE7
  // Умножить все элементы массива на 2
  A.ForEach(procedure(X: Double; I: Integer)
    begin
      A[I] := X * 2;      
    end);
  WriteLn(A.ToString); // => [2, 4, 6]
end;

Реализация метода ForEach:

procedure TArrayHelper.ForEach(Lambda: TForEachRef);
var
  I: Integer;
begin
  for I := 0 to Pred(Length(Self)) do
    Lambda(Self[I], I);
end;

// Вспомогательный метод: преобразование массива в строку
function TArrayHelper.ToString: string;
var
  Res: TArray<string>;
begin
  if Length(Self) = 0 then Exit('[]');
  ForEach(procedure(X: Double; I: Integer)
    begin
      Res := Res + [FloatToStr(X)];
    end);
  Result := '[' + string.Join(', ', Res) + ']';
end;

Обратите внимание, что метод ForEach не позволяет прервать итерации, пока все элементы не будут переданы функции. То есть отсутствует эквивалент инструкции Break, которую можно использовать с обычным циклом for. Если потребуется прервать итерации раньше, внутри функции можно возбуждать исключение, а вызов ForEach помещать в блок try.

Метод Map

Метод Map передает указанной функции каждый элемент массива, относительно которого он вызван, и возвращает массив значений, возвращаемых этой функцией. Например:

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;

Метод Map вызывает функцию точно так же, как и метод ForEach. Однако функция, передаваемая методу Map, должна возвращать значение. Обратите внимание, что Map возвращает новый массив: он не изменяет исходный массив.

Реализация метода Map:

function TArrayHelper.Map(Lambda: TMapRef): TArray<Double>;
var
  X: Double;
begin
  for X in Self do
    Result := Result + [Lambda(X)];
end;

Метод Filter

Метод Filter возвращает массив, содержащий подмножество элементов исходного массива. Передаваемая ему функция должна быть функцией-предикатом, т.к. должна возвращать значение True или False. Метод Filter вызывает функцию точно так же, как методы ForEach и Map. Если возвращается True, переданный функции элемент считается членом подмножества и добавляется в массив, возвращаемый методом. Например:

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)
      begin
        Write(X:2:0) // => 10 8 6
      end);    
end;

Реализация метода Filter:

function TArrayHelper.Filter(Lambda: TFilterRef): TArray<Double>;
var
  I: Integer;
begin
  for I := 0 to Pred(Length(Self)) do
    if Lambda(Self[I], I) then
      Result := Result + [Self[I]];
end;

Методы Every и Some

Методы Every и Some являются предикатами массива: они применяют указанную функцию-предикат к элементам массива и возвращают True или False. Метод Every напоминает математический квантор всеобщности ∀: он возвращает True, только если переданная Вами функция-предикат вернула True для всех элементов массива:

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;

Метод Some напоминает математический квантор существования ∃: он возвращает True, если в массиве имеется хотя бы один элемент, для которого функция-предикат вернет True, а значение False возвращается методом, только если функция-предикат вернет False для всех элементов массива:

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;

Реализация методов Every и Some:

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;

Обратите внимание, что оба метода, Every и Some, прекращают обход элементов массива, как только результат становится известен. Метод Some возвращает True, как только функция-предикат вернет True, и выполнит обход всех элементов массива, только если функция-предикат всегда возвращает False. Метод Every является полно противоположностью: он возвращает False, как только функция-предикат вернет False, и выполняет обход всех элементов массива, только если функция-предикат всегда возвращает True. Кроме того, отметьте, что в соответствии с правилами математики для пустого массива метод Every возвращает True, а метод Some возвращает False.

Методы Reduce и ReduceRight

Методы Reduce и ReduceRight объединяют элементы массива, используя указанную Вами функцию, и возвращают единственное значение. Это типичная операция в функциональном программировании, где она известна также под названием «свертка». Примеры ниже помогут понять суть этой операции:

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;

Метод Reduce принимает два аргумента. Во втором передается функция, которая выполняет операцию свертки. Задача этой функции – объединить некоторым способом или свернуть два значения в одно вернуть свернутое значение. В примерах выше функции выполняют объединение двух значений, складывая их, умножая и выбирая наибольшее. В первом аргументе передается начальное значение для функции.

Функции, передаваемые методу Reduce, отличаются от функций, передаваемых методам ForEach и Map. Значение элемента массива передается им во втором аргументе, а в первом аргументе передается накопленный результат свертки. При первом вызове в первом аргументе функции передается начальное значение, переданное методу Reduce в первом аргументе. Во всех последующих вызовах передается значение, полученное в результате предыдущего вызова функции. В первом примере, из приведенных выше, функция свертки сначала будет вызвана с аргументами 0 и 1. Она сложит эти числа и вернет 1. Затем она будет вызвана с аргументами 1 и 2 и вернет 3. Затем она вычислит 3 + 3 = 6, затем 6 + 4 = 10 и, наконец, 10 + 5 = 15. Это последнее значение 15 будет возвращено методом Reduce.

В третьем вызове, в примере выше, методу Reduce передается единственный аргумент: здесь не указано начальное значение. Эта альтернативная реализация метода Reduce в качестве начального значения использует первый элемент массива. Это означает, что при первом вызове функции свертки будут переданы первый и второй аргументы массива. В примерах вычисления суммы и произведения точно так же можно было бы применить эту альтернативную реализацию Reduce и опустить аргумент с начальным значением.

Вызов метода Reduce с пустым массивом без начального значения вызовет исключение. Если вызвать метод с единственным значением – с массивом, содержащим единственный элемент, и без начального значения или с пустым массивом и начальным значением – он просто вернет это единственное значение, не вызывая функцию свертки.

Реализация методов Reduce:

function TArrayHelper.Reduce(Init: Double; Lambda: TReduceRef): Double;
var
  I: Integer;
begin
  Result := Init;
  if Length(Self) = 0 then Exit;
  for I := 0 to Pred(Length(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 Pred(Length(Self)) do
    Result := Lambda(Result, Self[I]);
end;

Метод ReduceRight действует точно так же, как и метод Reduce, за исключением того, что массив обрабатывается в обратном порядке, от больших индексов к меньшим (справа налево). Это может потребоваться, если операция свертки имеет ассоциативность справа налево, например:

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;

Реализация метода ReduceRight:

function TArrayHelper.ReduceRight(Lambda: TReduceRef): Double;
var
  I: Integer;
begin
  Result := Self[Pred(Length(Self))];
  if Length(Self) = 1 then Exit;
  for I := Length(Self) - 2 downto 0 do
    Result := Lambda(Result, Self[I]);
end;

Следует отметить, что методы Every и Some, описанные выше, являются своеобразной разновидностью операции свертки массива. Однако они отличаются тем, что стремятся завершить обход массива как можно раньше и не всегда проверяют значения всех его элементов.

Вместо заключения

Рассмотрим еще один пример использования анонимных методов. Пусть у нас имеется массив чисел и нам необходимо найти среднее значение и стандартное отклонение для этих значений:

// Вспомогательная функция: вычисление суммы аргументов.
// Свободную функцию (как и метод экземпляра) можно использовать
// в качестве параметра для метода, принимающего 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;

Автор: 1ntr0

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js