Использование анонимных методов в 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; 

ссылка на оригинал статьи http://habrahabr.ru/post/243721/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *