カリー化と部分関数の使用



有名な C# 言語の第一人者 であり、 C# In Depthの 著者であり 、Googleの従業員であり、 stackoverflow.comの評判の 人物 #1 であり、最後にヒーローのJon Skeet Facts による記事の翻訳 この記事では、関数プログラミングの世界から来た概念である、関数のカリー化と部分適用とは何かを明確に説明しています。 さらに、彼はその違いが何であるかを詳細に説明します。 この記事を読む前に私自身がそれらを混同していたことを認めているので、翻訳をすることは私にとって有用であるように思えました。





これは少し奇妙な投稿です。読む前に、おそらく次のグループのいずれかを参照する必要があります。





一般に、 関数の カリー化部分的な適用という用語を混同する人もいることを知っています。これは、行うべきではないときに同じ意味で使用されます。 これは、ある程度理解しているトピック(モナドなど)の1つであり、自分の知識を検証する最良の方法はそれについて書くことだと判断しました。 これにより、トピックが他の開発者にとってよりアクセスしやすくなる場合は、さらに良いでしょう。





この投稿にはHaskellは含まれていません。



私が見たこの主題に関するほとんどすべての説明で、例は「正しい」関数言語、通常はHaskellで与えられました。 Haskellに反対するものは何もありません。通常、私がよく知っている言語の例を理解する方が簡単です。 さらに、このような言語で例を書く方がはるかに簡単なので、この投稿のすべての例はC#で記述されます。 実際、すべての例は1つのファイルで利用できますが 、その中のいくつかの変数は名前が変更されています。 コンパイルして実行するだけです。



C#は実際には関数型言語ではありません-デリゲートは高階関数の完全な代替物ではないことを十分理解しています。 ただし、説明されている原則を実証するには十分です。



カリー化と部分的なアプリケーションは、少数の引数を持つ関数(メソッド)を使用して実証できますが、明確にするために3つの引数を使用することにしました。 カリー化と部分的なアプリケーションを実行するための私のメソッドは一般化されます(したがって、すべてのタイプのパラメーターと戻り値は任意です)が、デモのために単純な関数を使用します。



static string SampleFunction(int a, int b, int c) { return string.Format("a={0}; b={1}; c={2}", a, b, c); }
      
      





これまでのところ、すべてがシンプルです。 この方法にはcなものは何もありませんが、驚くべきことを探してはいけません。



それは何ですか?



カリー化と部分適用の両方は、ある種の機能を別の種類に変換する方法です。 関数の近似としてデリゲートを使用するため、SampleFunctionメソッドを値として使用するには、次のように記述できます。



 Func<int, int, int, string> function = SampleFunction;
      
      





この行は、次の2つの理由で役立ちます。





これで、デリゲートを3つの引数で呼び出すことができます。



 string result = function(1, 2, 3);
      
      





または同じこと:



 string result = function.Invoke(1, 2, 3);
      
      





(C#コンパイラは、最初の短い形式を2番目の形式に変換します。生成されるILは同じになります。)



さて、3つの引数すべてを一度に使用できる場合はどうでしょうか。 特定の(やや不自然ですが)例については、3つのパラメーター(ソース、重大度、メッセージ)を持つロギング関数があり、同じクラス(これをBusinessLogicと呼びます)内で、常に同じ値をパラメーターに使用したいとします「ソース」。 クラスのどこからでも簡単にログを記録できるようにして、重大度とメッセージのみを示します。 いくつかのオプションがあります:





ロガーオブジェクトへのリンクを保存することと、ログ機能へのリンクを保存することの違いを意図的に無視します。 ロガークラスの複数の関数を使用する必要がある場合、明らかに大きな違いがありますが、カリー化と部分的なアプリケーションを考えるために、「ロガー」を「3つのパラメーターを取る関数」と考えます(例の関数として) )



動機付けのための擬似現実的な具体例を挙げたので、記事の終わりまでそれを忘れ、関数の例のみを検討します。 何か役に立つことをするふりをするBusinessLogicクラス全体を書きたくありません。 「例の関数」から「本当にやりたいこと」への精神的な変革ができると確信しています。



部分関数の使用



部分的なアプリケーションは、N個のパラメーターとこれらのパラメーターのいずれかの値を持つ関数を取り、N-1パラメーターを持つ関数を返します。そのため、呼び出されると、必要なすべての値(部分アプリケーション関数自体に渡される最初の引数と、残りのN-1戻り関数に渡される引数)。 したがって、これらの2つの呼び出しは、3つのパラメーターを持つメソッドと同等である必要があります。



 //   string result1 = function(1, 2, 3); //     Func<int, int, string> partialFunction = ApplyPartial(function, 1); string result2 = partialFunction(2, 3);
      
      





この場合、1行目の最初の1つのパラメーターで部分的なアプリケーションを実装しました。ApplyPartial 記述できます。これは、より多くの引数を取るか、関数の最終実行の別の位置でそれらを置き換えます。 どうやら、最初からパラメータを1つずつ収集することが最も一般的なアプローチです。



無名関数(この場合はラムダ式ですが、匿名メソッドはそれほど冗長ではありません)のおかげで、ApplyPartialの実装は簡単です。



 static Func<T2, T3, TResult> ApplyPartial<T1, T2, T3, TResult> (Func<T1, T2, T3, TResult> function, T1 arg1) { return (b, c) => function(arg1, b, c); }
      
      





一般化により、このメソッドは実際よりも複雑に見えます。 C#に高次型がないため、使用するデリゲートごとにこのメソッドを実装する必要があることに注意してください。4つのパラメーターを持つ関数のバージョンが必要な場合は、ApplyPartial <T1、T2メソッドが必要です。 、T3、T4、TResult>など。 また、おそらくアクションデリゲートファミリのメソッドのセットが必要です。



最後に注意することは、これらのすべてのメソッドを使用すると、必要に応じて、パラメーターなしの結果関数まで、部分的なアプリケーションを再度実行できることです。



 Func<int, int, string> partial1 = ApplyPartial(function, 1); Func<int, string> partial2 = ApplyPartial(partial1, 2); Func<string> partial3 = ApplyPartial(partial2, 3); string result = partial3();
      
      





繰り返しますが、最後の行のみが元の関数を呼び出します。



わかりました、これは機能の部分的な適用です。 比較的簡単です。 私の意見では、カレーは理解するのが少し難しいです。



キャリング



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





(繰り返しますが、これは3つのパラメーターを持つ関数にのみ適用されることに注意してください。他の署名でどのように機能するかを明らかに期待しています。)



「同等」の例では、次のように記述できます。



 //   string result1 = function(1, 2, 3); //    Func<int, Func<int, Func<int, string>>> f1 = Curry(function); Func<int, Func<int, string>> f2 = f1(1); Func<int, string> f3 = f2(2); string result2 = f3(3); //     ... var curried = Curry(function); string result3 = curried(1)(2)(3);
      
      





最後の例の違いは、関数型言語がしばしば型推論と関数型のコンパクトな表現を持つことが多い理由を示しています。宣言f1は非常に怖いです。



Curryメソッドの処理内容がわかったので、その実装は驚くほど簡単です。 実際、上記のポイントをラムダ式に変換するだけです。 美しさ:



 static Func<T1, Func<T2, Func<T3, TResult>>> Curry<T1, T2, T3, TResult> (Func<T1, T2, T3, TResult> function) { return a => b => c => function(a, b, c); }
      
      





コードをより理解しやすくしたい場合は、括弧を自由に追加してください。個人的には混乱を招くだけだと思います。 いずれにせよ、私たちは欲しいものを手に入れました。 (ラムダ式や匿名メソッドなしで書くのがどれだけ疲れるのかを考える価値があります。 難しくなく、ただ疲れます。)



これはカレーです。 そう思う。 おそらく。



おわりに



私はカレーを使用したことがあるとは言えませんが、 野田時間のテキスト解析の一部では実際に部分的なアプリケーションを使用しています。 (誰かが本当に私にこれをチェックして欲しいなら、私はそれをするでしょう。)



これら2つの関連する概念の違いを示したとは思いますが、それでも非常に異なる概念です。 終わりになったので、2つのパラメーターを持つ関数でそれらの違いがどのように表示されるかを考えてください。3つのパラメーターを使用することにした理由を理解してください:)



私の第六の感覚は、カリー化は学術的文脈において有用な概念であるが、実際には部分的な適用がより有用であることを教えてくれます。 ただし、これは関数型言語を最大限に使用しなかった人の6番目の感覚です。 F#の使用を開始する場合は、補完的な投稿を書くかもしれません。 今、私は経験豊富な読者がコメントで有用な考えを提供できることを願っています。



All Articles