Delphiでの匿名メソッドの使用

この記事を書いた理由は、Delphiの匿名関数の機能に関心があったからです。 さまざまなソースで、それらの理論的基礎、内部構造に関する情報を見つけることができますが、使用例はどこにでもありふれたものです。 そして、多くの質問をします:これらの参照はなぜ必要なのでしょうか?それらのアプリケーションの使用は何ですか? したがって、他の言語で使用されている匿名メソッドを使用するためのいくつかのオプションを提案します。おそらく、関数型プログラミングスタイルを指向しています。



単純化して説明するために、数値配列の操作を検討してください。ただし、アプローチ自体は、順序付けされたコンテナ(TList <T>など)に適用できます。 動的配列はオブジェクト型ではないため、ヘルパーを使用して機能を拡張します。 要素のタイプ、ダブルを選択:



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;
      
      





以下で説明するメソッドのほとんどは、関数を引数として受け取り、配列の各要素(または複数の要素)に対して呼び出します。 ほとんどの場合、1つの引数が指定された関数に渡されます:配列要素の値。 値が送信されるだけでなく、要素のインデックスと配列自体への参照も送信される、より高度な実装が可能です。 どのメソッドも元の配列を変更しませんが、これらのメソッドに渡される関数はこれを実行できます。



ForEachメソッド


ForEachメソッドは、配列の要素を走査し、各要素に対して指定された関数を呼び出します。 上記のように、関数は引数でForEachメソッドに渡されます。 この関数が呼び出されると、ForEachメソッドは配列要素の値、そのインデックス、およびブール変数Doneを渡し、Trueを割り当てます。Trueを割り当てると、反復を終了してメソッドを終了できます(通常のforループのBreak命令と同様)。 例:



 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メソッドは、指定された関数に対して呼び出される配列の各要素を渡し、この関数によって返される値の配列を返します。 例:



 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メソッドは、元の配列の要素のサブセットを含む配列を返します。 渡される関数は述語関数でなければなりません。なぜなら 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メソッドは配列述語です。指定された述語関数を配列要素に適用し、TrueまたはFalseを返します。 すべてのメソッドは、数学的な汎用数量詞resに似ています:渡された述語関数が配列のすべての要素に対して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;
      
      





いくつかのメソッドは数学的実存量指定子に似ています∃:述語関数がTrueを返す配列に少なくとも1つの要素がある場合は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;
      
      





あらゆるメソッドの実装:

 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を返す場合にのみ配列のすべての要素を走査します。 すべてのメソッドは正反対です。述語関数が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メソッドは2つの引数を取ります。 2番目では、畳み込み演算を実行する関数が渡されます。 この関数のタスクは、何らかの方法で結合するか、2つの値を1つにまとめて最小化された値を返すことです。 上記の例では、関数は2つの値を組み合わせ、それらを加算し、乗算して最大値を選択します。 最初の引数は、関数の初期値を渡します。



Reduceメソッドに渡される関数は、ForEachおよびMapメソッドに渡される関数とは異なります。 配列要素の値は2番目の引数で渡され、累積された畳み込み結果は最初の引数で送信されます。 最初の引数で最初に関数が呼び出されると、初期値が渡され、最初の引数でReduceメソッドに渡されます。 後続のすべての呼び出しでは、前の関数呼び出しから取得した値が渡されます。 上記の最初の例では、畳み込み関数は最初に引数0と1で呼び出されます。これらの数値を追加して1を返します。その後、引数1と2で呼び出され、3を返します。 6 + 4 = 10、最後に10 + 5 =15。最後の15の値は、Reduceメソッドによって返されます。



上記の例の3番目の呼び出しでは、Reduceメソッドに唯一の引数が渡されます。ここでは初期値は指定されません。 Reduceメソッドのこの代替実装では、配列の最初の要素を初期値として使用します。 これは、畳み込み関数が最初に呼び出されたときに、配列の最初と2番目の引数が渡されることを意味します。 合計と積の計算の例では、同じ方法で、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;
      
      





ソースコード



コメントのおかげで記事が改善されました。



そして、ここに続きがあります 。これは、高次のクロージャーと関数に捧げられています。



All Articles