Delphiでクロージャーと高階関数を使用する

この記事は、匿名メソッドに特化した以前の出版物の続きです。 今回は、著者にとって興味深いと思われる高階関数と閉包関数の使用例について説明します。



Delphiは関数型プログラミング言語ではありませんが、そのプログラムが関数をオブジェクトとして操作できるという事実は、Delphiで関数型パラダイムトリックを使用できることを意味します。 この記事の目的は、このスタイルの使用を奨励することではなく、いくつかの例と可能性を概説することです。



機能設計


高階関数(FWP)は、関数を操作し、1つ以上の関数を取得して新しい関数を返す関数です。

次の例は、FVPを使用して他の機能を設計する方法を示しています。



type TRef<AT, RT> = reference to function(X: AT): RT; var Negate: TRef<TRef<Integer, Boolean>, TRef<Integer, Boolean>>; IsOdd, IsEven: TRef<Integer, Boolean>; begin //   ,    IsOdd := function(X: Integer): Boolean begin Result := X mod 2 <> 0; end; //    Negate := function(F: TRef<Integer, Boolean>): TRef<Integer, Boolean> begin Result := function(X: Integer): Boolean begin Result := not F(X); end; end; //     IsEven := Negate(IsOdd); WriteLn(IsOdd(4)); // => False WriteLn(IsEven(4)); // => True end;
      
      





上記の例のNegate関数は、引数としてIsOdd関数を取り、そのNegate引数を渡し、IsOdd関数によって返された値の論理否定を返す新しいIsEven関数を返すため、FEPです。



ジェネリック型の使用は表示の明瞭さに寄与しないため、次の例では可能な限りそれらを避けます。



機能構成


以下は、2つの関数FとGを取り、結果F(G())を返す新しい関数を返す、より汎用的な別の関数の例です。



 type TOneArgRef = reference to function(X: Single): Single; TTwoArgRef = reference to function(X, Y: Single): Single; TCompose = reference to function(F: TOneArgRef; G: TTwoArgRef): TTwoArgRef; var Compose: TCompose; Square: TOneArgRef; Half: TOneArgRef; Sum: TTwoArgRef; SquareOfSum: TTwoArgRef; HalfSum: TTwoArgRef; begin //     "" Compose := function(F: TOneArgRef; G: TTwoArgRef): TTwoArgRef begin Result := function(X, Y: Single): Single begin Result := F(G(X, Y)); end; end; //   : // 1.    Square := function(X: Single): Single begin Result := X * X; end; // 2.    Half := function(X: Single): Single begin Result := X / 2; end; // 3.     Sum := function(X, Y: Single): Single begin Result := X + Y; end; //   " " SquareOfSum := Compose(Square, Sum); //   "" HalfSum := Compose(Half, Sum); WriteLn(SquareOfSum(2.0, 3.0)); // => 25.0 WriteLn(HalfSum(3.0, 7.0)); // => 5.0 end;
      
      





ここで、Compose関数はF(G(X、Y))を計算します。 返される関数は、すべての引数をGに渡し、Gから受け取った値をFに渡し、Fを呼び出した結果を返します。



部分適用


この用語は、複数の引数を持つ関数から、より少ない引数を受け入れ、省略された引数の値が事前に定義されている関数への変換を表します。 この手法は、その名前に非常に適しています。関数にいくつかの引数を「部分的に適用」し、残りの引数を取る関数を返します。

以下の例のBindLeft関数は、 n個の引数を取るCalc関数を取り、それらの最初のkを以前に設定された値に関連付け、 (nk) 個の引数を取ることができるPartial関数を返します(最初のk個の引数は既に適用されます)。



 type TManyArgRef = reference to function(Args: TArray<Double>): Double; TBindRef = reference to function(Args: TArray<Double>; F: TManyArgRef): TManyArgRef; var BindLeft: TBindRef; Calc, Partial: TManyArgRef; begin //  ,     Args //   F . BindLeft := function(Args: TArray<Double>; F: TManyArgRef): TManyArgRef var StoredArgs: TArray<Double>; begin StoredArgs := Args; Result := function(Args: TArray<Double>): Double begin Result := F(StoredArgs + Args); end; end; //     //     Calc := function(A: TArray<Double>): Double begin Result := A[0] * (A[1] - A[2]); end; //    Partial := BindLeft([2, 3], Calc); //      WriteLn(Partial([4])); // => -2.0 //  Partial   Calc([2, 3, 4]) end;
      
      





ここで重要なのは、BindLeftを呼び出した後、ローカル変数StoredArgsが存在しなくなり、さらに使用され、Partialを呼び出してCalcに渡されるときに使用される引数の値を保存する場合です。 この効果は閉鎖と呼ばれます。 さらに、BindLeftを呼び出すたびに、StoredArgsの新しい「インスタンス」が生成されます。 前の例では、FWPの引数がクロージャーに格納されていたときにクロージャーが使用されました。

右側の部分的なアプリケーションは、次のように決定できます。



  BindRight := function(Args: TArray<Double>; F: TManyArgRef): TManyArgRef var StoredArgs: TArray<Double>; begin StoredArgs := Args; Result := function(Args: TArray<Double>): Double begin Result := F(Args + StoredArgs); //   end; end;
      
      







キャリング


部分的なアプリケーションは、 k個の引数を使用して、 n個のパラメーターを持つ関数をnk 個のパラメーターを持つ関数に変換しますが、カリー化は関数を1つの引数の関数に分解します。 変換された関数を除いて、Curryメソッドに追加の引数を渡しません。



 type TOneArgRef = reference to function(X: Double): Double; TThreeArgRef = reference to function(X, Y, Z: Double): Double; TSecondStepRef = reference to function(X: Double): TOneArgRef; TFirstStepRef = reference to function(X: Double): TSecondStepRef; TCurryRef = reference to function(F: TThreeArgRef): TFirstStepRef; var Curry: TCurryRef; Calc: TThreeArgRef; F1: TFirstStepRef; F2: TSecondStepRef; F3: TOneArgRef; Re: Double; begin //        Curry := function(F: TThreeArgRef): TFirstStepRef begin Result := function(A: Double): TSecondStepRef begin Result := function(B: Double): TOneArgRef begin Result := function(C: Double): Double begin Result := F(A, B, C); end; end; end; end; //     , //    Calc := function(A, B, C: Double): Double begin Result := A + B + C; end; //     Calc,   F1 := Curry(Calc); F2 := F1(1); F3 := F2(2); Re := F3(3); WriteLn(Re); // => 6.0 end;
      
      





カレーの一般化バージョンは、もう少しコンパクトに見えます。
 type TRef<AT, RT> = reference to function(Args: AT): RT; TCalc<T> = reference to function(X, Y, Z: T): T; var Curry: TRef<TCalc<Double>,TRef<Double,TRef<Double,TRef<Double,Double>>>>; Calc: TCalc<Double>; begin //    Curry := function(F: TCalc<Double>): TRef<Double,TRef<Double,TRef<Double,Double>>> begin Result := function(A: Double): TRef<Double,TRef<Double,Double>> begin Result := function(B: Double): TRef<Double,Double> begin Result := function(C: Double): Double begin Result := F(A, B, C); end; end; end; end; //    Calc := function(A, B, C: Double): Double begin Result := A + B + C; end; //  WriteLn(Curry(Calc)(1)(2)(3)); // => 6.0 end;
      
      







メモ化


メモ化された関数は、以前に計算された結果を保存する関数です。 つまり、関数の結果テーブルが作成され、特定のパラメーター値に対して計算されて、結果がこのテーブルに入力されます。 将来、結果はこの表から取得されます。 この手法により、追加メモリを使用してプログラムを高速化できます。 もちろん、memizable関数は副作用なしで動作するはずであり、定義の離散領域を持つことが望ましいです。

次の例は、高次のMemoize関数を示しています。この関数は、引数として関数を取り、メモされたバージョンを返します。



 type TRef = reference to function(X: Integer): Double; TMemoize = reference to function(F: TRef): TRef; var Memoize: TMemoize; Calc: TRef; MemoizedCalc: TRef; begin //  Memoize Memoize := function(F: TRef): TRef var Cache: ICache<Integer, Double>; begin Cache := TCache<Integer, Double>.Create; Result := function(X: Integer): Double begin //      ... if not Cache.TryGetValue(X, Result) then begin Result := F(X); // ...   Cache.Add(X, Result); //    end; end; end; // ,     Calc := function(X: Integer): Double var I: Integer; begin Result := 0; for I := 1 to High(Word) do Result := Result + Ln(I) / Sin(I) * X; end; //    Calc MemoizedCalc := Memoize(Calc); end;
      
      





Memoize関数は、キャッシュとして使用するためにTCacheオブジェクトを作成し、ローカル変数に割り当てます。これにより、返された関数に対してのみ(クロージャを介して)使用可能になります。 返される関数は、引数をキーに変換します。 キャッシュに値が存在する場合、結果として単純に返されます。 それ以外の場合、指定された引数の値を計算する元の関数が呼び出されます。 結果の値はキャッシュされて返されます。



キャッシュの実装
 interface uses Generics.Collections; type //       ICache<TKey, TValue> = interface function TryGetValue(Key: TKey; out Value: TValue): Boolean; procedure Add(Key: TKey; Value: TValue); end; TCache<TKey, TValue> = class(TInterfacedObject, ICache<TKey, TValue>) private FDictionary: TDictionary<TKey, TValue>; public constructor Create; destructor Destroy; override; function TryGetValue(Key: TKey; out Value: TValue): Boolean; procedure Add(Key: TKey; Value: TValue); end; implementation constructor TCache<TKey, TValue>.Create; begin FDictionary := TDictionary<TKey, TValue>.Create; end; destructor TCache<TKey, TValue>.Destroy; begin FDictionary.Free; inherited; end; procedure TCache<TKey, TValue>.Add(Key: TKey; Value: TValue); begin FDictionary.Add(Key, Value); end; function TCache<TKey, TValue>.TryGetValue(Key: TKey; out Value: TValue): Boolean; begin Result := FDictionary.TryGetValue(Key, Value); end;
      
      







メモ化された関数を持つプログラムは、同じ引数で定期的に呼び出され、メモ化を使用しない類似のプログラムよりも高速に実行されるはずです。 違いを確認します。



 uses SysUtils, DateUtils; var I: Integer; Time: TDateTime; Ms1, Ms2: Int64; Res1, Res2: Double; begin Res1 := 0; Res2 := 0; //   Time := Now; for I := 1 to 1000 do Res1 := Res1 + Calc(I mod 100); Ms1 := MilliSecondsBetween(Now, Time); //   Time := Now; for I := 1 to 1000 do Res2 := Res2 + MemoizedCalc(I mod 100); Ms2 := MilliSecondsBetween(Now, Time); WriteLn(Res1 = Res2); // => True WriteLn(Ms1 > Ms2); // => True end;
      
      







発電機


ここで、ジェネレーターとは、関数を返すFWPを意味し、その呼び出しは特定のシーケンスの次のメンバーにつながります。 次の例では、フィボナッチ数列と階乗ジェネレーターの2つのジェネレーターが作成されます。 ジェネレーターの以前の要素は回路に保存されます。



 type TRef = reference to function: Cardinal; TGenRef = reference to function: TRef; var FibGen, FactGen: TGenRef; FibVal, FactVal: TRef; I: Integer; begin // -,     FibGen := function: TRef var X, Y: Cardinal; begin X := 0; Y := 1; Result := function: Cardinal begin Result := Y; Y := X + Y; X := Result; end; end; // -,    FactGen := function: TRef var X, Y: Cardinal; begin X := 1; Y := 1; Result := function: Cardinal begin Result := Y; Y := Y * X; Inc(X); end; end; //   -    . //     Delphi,     . FibVal := FibGen(); FactVal := FactGen(); for I := 1 to 10 do WriteLn(FibVal, #9, FactVal); end;
      
      







ジェネレーターの利点は、次の各要素を計算するために、最初からシーケンス全体を計算する必要がないことです。 ジェネレーターでは無限のシーケンスを操作することもできますが、それらは要素への順次アクセスのみを提供し、インデックスによる要素へのアクセスは許可しません。ne値を取得するには、n-1回の反復を実行する必要があります。



遅延計算


ジェネレータは、リスト項目、テキスト行、字句解析器内のトークンなど、順次データ処理に便利に使用できます。 ジェネレーターは、Unixコマンドパイプラインのようにチェーンできます。 このアプローチの最も興味深い点は、 遅延計算の原則に従うことです。値は、必要に応じてジェネレーター(またはパイプライン)から「抽出」され、一度にすべてではありません。 この機能は、ソーステキストがフィルター処理され、ジェネレーターのチェーンを1行ずつ通過する次の例で示されます。



 type TStringRef = reference to function: string; TEachLineRef = reference to function(S: string): TStringRef; TArgMap = reference to function(S: string): string; TMap = reference to function(A: TStringRef; F: TArgMap): TStringRef; TArgSelect = reference to function(S: string): Boolean; TSelect = reference to function(A: TStringRef; F: TArgSelect): TStringRef; const //  ,    TEXT = '#comment ' + sLineBreak + '' + sLineBreak + ' hello' + sLineBreak + ' world ' + sLineBreak + ' quit ' + sLineBreak + ' unreached'; var EachLine: TEachLineRef; Map: TMap; Select: TSelect; Lines, Trimmed, Nonblank: TStringRef; S: string; begin // ,     . EachLine := function(S: string): TStringRef begin Result := function: string begin Result := S.Substring(0, S.IndexOf(sLineBreak)); S := S.Substring(S.IndexOf(sLineBreak) + 1); end; end; // ,  ,   -  F  A Map := function(A: TStringRef; F: TArgMap): TStringRef begin Result := function: string begin Result := F(A); end; end; // -,   A,  F(A) = True Select := function(A: TStringRef; F: TArgSelect): TStringRef begin Result := function: string begin repeat Result := A; until F(Result); end; end; //      : //      Lines := EachLine(TEXT); //          Trimmed := Map(Lines, function(S: string): string begin Result := S.Trim; end); // ,      Nonblank := Select(Trimmed, function(S: string): Boolean begin Result := (S.Length > 0) and (S[1] <> '#'); end); //         , // ,    'quit' repeat S := Nonblank; if S = 'quit' then Break; WriteLn(S); until False; end;
      
      







記事のソースはここからダウンロードできます。



All Articles