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

Поводом для написания статьи стал интерес к возможностям анонимных функции в Delphi. В разных источниках можно найти их теоретические основы, информацию о внутреннем устройстве, а вот примеры использования везде даются какие-то тривиальные. И многие задают вопросы:, а для чего вообще нужны эти reference, какая может быть польза от их применения? Поэтому предлагаю некоторые варианты использования анонимных методов, применяемые в других языках, возможно, более ориентированных на функциональный стиль программирования.Для упрощения и наглядности рассмотрим операции над числовым массивом, хотя сам подход применим к любым упорядоченным контейнерам (например, TList). Динамический массив не является объектным типом, поэтому для расширения его функциональности используем хэлпер. Тип элементов выберем Double: uses SysUtils, Math; type TArrayHelper = record helper for TArray 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; 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; Большинство описываемых ниже методов принимают функцию в качестве аргумента и вызывают ее для каждого элемента (или нескольких элементов) массива. В большинстве случаев указанной функции передается один аргумент: значение элемента массива. Возможны более продвинутые реализации, в которых передается не только значение, но также индекс элемента и ссылка на сам массив. Ни один из методов не изменяет исходный массив, однако функция, передаваемая этим методам, может это делать.Метод ForEach Метод ForEach выполняет обход элементов массива и для каждого из них вызывает указанную функцию. Как уже говорилось выше, функция передается методу ForEach в аргументе. При вызове этой функции метод ForEach будет передавать ей значение элемента массива и его индекс. Например: var A: TArray; 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; 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; 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; 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; 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) begin Write (X:2:0) // => 10 8 6 end); end; Реализация метода Filter: function TArrayHelper.Filter (Lambda: TFilterRef): TArray; 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; 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; 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; 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; 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; 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;

© Habrahabr.ru