Источник: Блог компании Embarcadero
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;
Исходники
Интернет-магазин