Поводом для написания статьи стал интерес к возможностям анонимных функций в Delphi. В разных источниках можно найти их теоретические основы, информацию о внутреннем устройстве, а вот примеры использования везде даются какие-то тривиальные. И многие задают вопросы: а для чего вообще нужны эти reference, какая может быть польза от их применения? Поэтому предлагаю некоторые варианты использования анонимных методов, применяемые в других языках, возможно, более ориентированных на функциональный стиль программирования.
Для упрощения и наглядности рассмотрим операции над числовым массивом, хотя сам подход применим к любым упорядоченным контейнерам (например, TList<T>). Динамический массив не является объектным типом, поэтому для расширения его функциональности используем хэлпер. Тип элементов выберем Double:
Большинство описываемых ниже методов принимают функцию в качестве аргумента и вызывают ее для каждого элемента (или нескольких элементов) массива. В большинстве случаев указанной функции передается один аргумент: значение элемента массива. Возможны более продвинутые реализации, в которых передается не только значение, но также индекс элемента и ссылка на сам массив. Ни один из методов не изменяет исходный массив, однако функция, передаваемая этим методам, может это делать.
Метод ForEach выполняет обход элементов массива и для каждого из них вызывает указанную функцию. Как уже говорилось выше, функция передается методу ForEach в аргументе. При вызове этой функции метод ForEach будет передавать ей значение элемента массива, его индекс, а также булеву переменную Done, присвоение True которой позволит прервать итерации и выйти из метода (аналог инструкции Break для обычного цикла for). Например:
Реализация метода ForEach:
Метод Map передает указанной функции каждый элемент массива, относительно которого он вызван, и возвращает массив значений, возвращаемых этой функцией. Например:
Метод Map вызывает функцию точно так же, как и метод ForEach. Однако функция, передаваемая методу Map, должна возвращать значение. Обратите внимание, что Map возвращает новый массив: он не изменяет исходный массив.
Реализация метода Map:
Метод Filter возвращает массив, содержащий подмножество элементов исходного массива. Передаваемая ему функция должна быть функцией-предикатом, т.к. должна возвращать значение True или False. Метод Filter вызывает функцию точно так же, как методы ForEach и Map. Если возвращается True, переданный функции элемент считается членом подмножества и добавляется в массив, возвращаемый методом. Например:
Реализация метода Filter:
Методы Every и Some являются предикатами массива: они применяют указанную функцию-предикат к элементам массива и возвращают True или False. Метод Every напоминает математический квантор всеобщности ∀: он возвращает True, только если переданная Вами функция-предикат вернула True для всех элементов массива:
Метод Some напоминает математический квантор существования ∃: он возвращает True, если в массиве имеется хотя бы один элемент, для которого функция-предикат вернет True, а значение False возвращается методом, только если функция-предикат вернет False для всех элементов массива:
Реализация методов Every и Some:
Обратите внимание, что оба метода, Every и Some, прекращают обход элементов массива, как только результат становится известен. Метод Some возвращает True, как только функция-предикат вернет True, и выполнит обход всех элементов массива, только если функция-предикат всегда возвращает False. Метод Every является полно противоположностью: он возвращает False, как только функция-предикат вернет False, и выполняет обход всех элементов массива, только если функция-предикат всегда возвращает True. Кроме того, отметьте, что в соответствии с правилами математики для пустого массива метод Every возвращает True, а метод Some возвращает False.
Методы Reduce и ReduceRight объединяют элементы массива, используя указанную Вами функцию, и возвращают единственное значение. Это типичная операция в функциональном программировании, где она известна также под названием «свертка». Примеры ниже помогут понять суть этой операции:
Метод 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:
Метод ReduceRight действует точно так же, как и метод Reduce, за исключением того, что массив обрабатывается в обратном порядке, от больших индексов к меньшим (справа налево). Это может потребоваться, если операция свертки имеет ассоциативность справа налево, например:
Реализация метода ReduceRight:
Следует отметить, что методы Every и Some, описанные выше, являются своеобразной разновидностью операции свертки массива. Однако они отличаются тем, что стремятся завершить обход массива как можно раньше и не всегда проверяют значения всех его элементов.
Рассмотрим еще один пример использования анонимных методов. Пусть у нас имеется массив чисел и нам необходимо найти среднее значение и стандартное отклонение для этих значений:
Исходники
Статья была улучшена, благодаря Вашим комментариям.
А здесь продолжение, которое посвящено замыканиям и функциям высших порядков.
Для упрощения и наглядности рассмотрим операции над числовым массивом, хотя сам подход применим к любым упорядоченным контейнерам (например, TList<T>). Динамический массив не является объектным типом, поэтому для расширения его функциональности используем хэлпер. Тип элементов выберем 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;
Большинство описываемых ниже методов принимают функцию в качестве аргумента и вызывают ее для каждого элемента (или нескольких элементов) массива. В большинстве случаев указанной функции передается один аргумент: значение элемента массива. Возможны более продвинутые реализации, в которых передается не только значение, но также индекс элемента и ссылка на сам массив. Ни один из методов не изменяет исходный массив, однако функция, передаваемая этим методам, может это делать.
Метод ForEach
Метод ForEach выполняет обход элементов массива и для каждого из них вызывает указанную функцию. Как уже говорилось выше, функция передается методу ForEach в аргументе. При вызове этой функции метод ForEach будет передавать ей значение элемента массива, его индекс, а также булеву переменную Done, присвоение True которой позволит прервать итерации и выйти из метода (аналог инструкции Break для обычного цикла for). Например:
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;
Реализация метода ForEach:
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
Метод 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; var Done: Boolean)
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 High(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 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;
Метод 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[High(Self)];
if Length(Self) = 1 then Exit;
for I := High(Self) - 1 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;
Исходники
Статья была улучшена, благодаря Вашим комментариям.
А здесь продолжение, которое посвящено замыканиям и функциям высших порядков.