[Из песочницы] Использование анонимных методов в Delphi20.11.2014 16:48
Поводом для написания статьи стал интерес к возможностям анонимных функции в 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