C ++での部分的な使用とカリー化

ご挨拶。



どうして起こったのかはわかりませんが、C ++ 11のラムダ式でゆっくり遊んでいました(ちなみに、数年前に驚くほど好評を博した記事をすでに書いています )。 Haskellは、C ++のコンテキストでの部分的な使用やカリー化などの概念を理解し始めました。 そして、初心者にとっては、これらの用語を定義しておくといいかもしれません。



実際、関数の部分的な使用は、関数パラメーターの1つに対して特定の値を修正する機能です。つまり、N個のパラメーターの関数からN-1個のパラメーターの関数を取得します。 2つの数値を合計するバイナリ関数がある場合:

int sum(int a, int b) { return a + b; }
      
      





次に、パラメータの1つ、たとえば42の特定の値を修正できます。したがって、引数に数値42を追加して、新しい単項関数を取得します。



厳密に言えば、関数の部分的な適用は以前のC ++標準でした。 2つのstd::binder1st



およびstd::binder2nd



クラスがこの役割を果たし、よく知られている補助関数std::bind1st



およびstd::bind2nd



は、上記のクラスのオブジェクトの作成を簡素化します。 確かに、1つの問題があります。これらのバインダーは、パラメーターの種類を知る必要があるため、通常の関数ポインターでは機能しません。 この問題やその他の問題を解決するために、STLはどこでもファンクターを使用します。 正直に言うと、私はそれらを機能オブジェクトと呼ぶことを好むでしょう。 私がHaskellに出会った時から、「ファンクター」という言葉は全く異なる本質に関連付けられてきました。 ただし、C ++プログラマーのサークルでは、この用語は、関数のように動作できるオブジェクトを指定するために正確に修正されました。 さらに、書くのが速いです:)



では、この問題はどのように解決されますか? 誰かが知らない場合は、簡単に説明します。 ちなみにバイナリ関数でのみ動作するstd::binder1st



およびstd::binder2nd



、定義するファンクタでいくつかのtypedef



を必要とします:これらはresult_type、first_argument_typeおよびsecond_argument_typeです。 ファンクタでこれらの型を毎回手動で宣言する必要がないように、単純にstd::binary_function<T0,T1,R>



から継承できます。ここで、T0とT1は関数の引数の型で、Rは戻り値の型です。



この全体を使用する例は、次のようなものです。



 template <typename T> struct Sum : public std::binary_function<T, T, T> { T operator()(T const & a, T const & b) { return a + b; } }; //   std::for_each(intArray.begin(), intArray.end(), std::bind1st(Sum<int>(), 42));
      
      







STLに長い間慣れ親しんでいるコンストラクトは、そのようなコンストラクトを恐れていません(そして、残念なことに、私はその中に含まれています)。 読みやすさについても覚えていない方がいいです。これよりも組み合わせの方が優れているからです:)特に、Boostライブラリでは、後からより強力な代替品-Boost.Bindが登場しましたが、さらに読みにくい(典型的なC ++-方法) 。 ちなみに、Boost.Bindは新しいC ++ 11標準に移行して古いバインダーを置き換えたことに注意する必要があります。 しかし、誰が...何があるときにそれを使用しますか? そうです、ラムダ! まあ、彼らとは、それは完全に異なる問題です:)文章が少なく、読みやすさが優れています(もちろん、バインダーと比較し、他の言語ではありません;))。



したがって、上で定義したSum



ファンクタがあります。 言うのを忘れているかもしれませんが、STLにはすでに同様のファンクターがあります-std :: plus <>。 しかし、独自の類似したものを書いたので、それなしでもできます。 一般に、バイナリファンクタがあり、部分的に適用された単項演算子を取得する必要があります。 ラムダでは、次のようになります。



 using namespace std; //  for_each, begin  end // ... Sum<int> sum; for_each(begin(intArray)), end(intArray), [sum] (int const & n) { return sum(42, n); });
      
      







ラムダの本体にreturn 42 + n;



直接書き込むことができる場合、 sum(42, n)



sum(42, n)



を呼び出す理由を尋ねることができます。 。 もちろん、観察は真実ですが、私たちが興味を持っているのは、まだ忘れていない場合の関数の部分的な使用です。 さらに、この関数は単純に2つの数値を加算するよりもはるかに複雑になる可能性があります。



そして、Haskellでどのように記述しますか? おそらく、このようなことが判明するでしょう:

 sum ab = a + b someFunc intList = map (λ n → sum 42 n) intList
      
      







Haskellに慣れていない場合は、絶望しないでください。 このコードは、最後のC ++の例に似ています。 ここで簡単に説明します。最初の行で、関数sum



を宣言しました。これはa



b



を取りa



それらの合計を返します。 次に、いくつかのリストをパラメーター(おそらく、パラメーターの名前で判断して整数のリスト)を取り、それで何かを行う関数を発表しました。 map



関数は、 std::for_each



類似しています。 ある種の関数を取り、リストの各要素に対して呼び出します。 関数として、 sum



関数を呼び出すラムダを渡し、最初のパラメーターとして固定値を明示的に渡し、ラムダ引数は自然に2番目のパラメーターとして機能します。 これはすべて重要ではありません...または、同じHaskellコードを次のように記述できなければ、重要ではなかったかもしれません。



 sum ab = a + b someFunc intList = map (sum 42) intList
      
      







ご覧のとおり、今回はラムダの代わりに、はるかに短い構成、つまり1つのパラメーターを使用したバイナリ関数sum



呼び出しを使用しました。 これはどのように機能しますか? はい、とても簡単です! :)呼び出し(sum 42)



は、1つのパラメーターを取り、それを数値42で合計する新しい関数を返します。実際、これは同じ部分的なアプリケーションです- sum



関数と言います。 ただし、2番目のパラメーターはまだわからないため、後で対処する必要があります。 これはすべて、Haskellのすべての関数がカリー化されているという事実のために機能します(ところで、そのような数学者がいました-Haskell Curry;)。 それで、それが何であるかを理解する時です。



まず、カレーは操作です。 つまり、Haskell宇宙の一部の財産や魔法の生き物ではなく、変容です。 第二に、これは関数に対して実行される変換です。N個のパラメーターの関数を受け取り、1つのパラメーターの同様の関数に変換し、N-1個のパラメーターの関数を返します。 例で説明します。 これを行うには、C ++関数sum



に戻りますが、もう1つパラメーターを追加します(念のため)。

 template <typename T1, typename T2, typename T3, typename R> R sum(T1 a, T2 b, T3 c) { return a + b + c; }
      
      





パラメータと戻り値の型にのみ関心があるため、その型を次のように記述します。

 sum :: ((T1 × T2 × T3) → R)
      
      





このレコードは、関数がそれぞれ型T1、T2、T3の3つの引数を取り、型Rの値を返すことを意味します。実際、カリー化後、上記の定義により、次のようになります。

 sum :: (T1 → ((T2 × T3) → R))
      
      





つまり、T1型の1つのパラメーターを受け取り、T2およびT3型の2つの引数をそれぞれ受け取り、Rの値を(以前と同様に)戻す別の関数を返す関数です。実際、関数の最初のパラメーターを「噛み切る」そして、彼らは言う、「彼を覚えている、心配しないで」。 そして、パラメータを1つ少なくする同様の関数を返します。 何にも似ていませんか? しかし、これに基づいて、部分的なアプリケーションを実現できます!



しかし...実際、私は少しcでした。 すべてがこのように機能する場合、連続する各関数引数を部分的に適用した後、結果の関数をカリー化する必要があります。 自分で判断してください:三項関数があり、それをカリー化して単項関数を取得します。 この単項関数には、その唯一のパラメーターを渡し、その結果、別のバイナリ関数を取得します。 ここで、別のパラメーターに対して部分的なアプリケーションを実行する場合、バイナリ関数に対してのみカリー化を実行する必要があります。 これを毎回だますことは意味がないので、実際、カレーの結果として、次のようになります。

 sum :: (T1 → (T2 → (T3R)))
      
      





本質的に、これは同じであり、バイナリ関数を返す代わりに、カリー化されたとおりにすぐに返します。 したがって、多入力関数から、単項チェーンを取得しました。これは、最初に引数を部分的に順番に適用することを可能にし、次に、すべての引数を適用するときに元の関数と同等に動作する、つまり同じ出力結果を与える。



これまでのところ、すべてが多かれ少なかれ明確になっていることを願っています。 しかし、あなたの多くはすでに感嘆の声を上げてモニターに指を突っ込んでいると思います:「さて、最後に私にブリジャコードを見せてください!」当然、私は反対することはできません。 したがって、理論が終了したら、実践に移ります。 2012年ですので、新しいC ++ 11標準を使用しますが、Microsoft Visual Studio 2010がサポートするもののみに制限しようとします-新しい標準のサポートに関しては、おそらく最も遅れているリリースコンパイラです。



簡単なものから始めましょう。 バイナリ関数があります。 カリー化の結果として、別の単項関数を返す単項関数を取得する必要があります。 C ++ 11を使用すると、蒸したカブよりも簡単です:

 #include <cstddef> #include <iostream> using namespace std; template <typename R, typename T0, typename T1> function<function<R(T1)>(T0)> curry_(function<R(T0,T1)> f) { return [=] (T0 const & t0) -> function<R(T1)> { return [=] (T1 const & t1) { return f(t0, t1); }; }; } int sum(int a, int b) { return a + b; } int main() { auto curried_sum = curry_<int,int,int>(sum); cout << sum(42, 10) << endl; // => 52 cout << curried_sum(42)(10) << endl; // => 52 return EXIT_SUCCESS; }
      
      







一般的に、次のようなことがあります: curry_



関数は、3つのテンプレートパラメーター(関数の引数の型と戻り値の型)に依存します。 これは、引数としてタイプstd::function<>



オブジェクトを取ります。 誰も知らない場合、これはファンクタ、ラムダ、さらには関数へのポインタさえ格納できるユニバーサルコンテナです(はい!これ以上バントはありません:))。 それが実際に機能的なオブジェクトであること、つまり、 operator()



オーバーロードされていることが重要です。 次に、単項ラムダ(基本的には匿名ファンクター)を返すだけで、別の単項ラムダが返されます。 これは、ロシア語からC ++へのカリー化という用語の定義のほぼ1つの翻訳です。



今、重要な瞬間が来ました。 この場所を読んだすべての人は、どのオプションが一番好きかという内なる声を尋ねるべきです。



 std::bind1st(std::function<int(int,int)>(sum), 42)(10); //  curry_<int,int,int>(sum)(42)(10);
      
      







最初に、あなたがもはや読むことができないなら、私を信じてください。 2番目の場合、私はあなたに認めなければなりません...私自身は2番目の選択肢はあまり好きではありませんが、私の意見ではすでに1番目の選択肢よりはるかに優れています。



何らかの方法で、引数のタイプと戻り値を明示的に指定する必要があることに少し悩まされます。 コンパイラは、すべての型を自動的に出力することはできないため、プロンプトを表示する必要があります。 さらに、三項関数のcurry_



関数をオーバーロードしたい場合、コンパイラーはstd::function<>



インスタンスを区別できないため失敗します。 私が理解しているように、この理由は、 std::function<>



任意の型を受け入れるテンプレートコンストラクターがありますが、問題を正しく理解したかどうかは完全にはわからないため、ここでは説明しません。 しかし、事実は残っています。 そして、あなたはそれについて何かをする必要があります。



次のように、引数のタイプと戻り値を示す必要性の問題を解決しようとします。 curry_



関数はそのままにしてcurry_



、そのためにテンプレートラッパーを作成します。このラッパーは、任意のタイプのテンプレートパラメーターとして受け入れます。 次に、 function_traits



ようなものを記述しfunction_traits



(ところで、これが標準にないのは奇妙ですよね?)、引数のタイプなどを覚えておきます いわゆる特性クラスを作成するアプローチは、STLで一般的に使用されています。



 template <typename Func> struct function_traits {}; //      template <typename R, typename T0, typename T1> struct function_traits<R(*)(T0,T1)> { typedef R result_type; typedef T0 first_argument_type; typedef T1 second_argument_type; }; //   curry_ template <typename Functor> function< function< typename function_traits<Functor>::result_type(typename function_traits<Functor>::second_argument_type) >(typename function_traits<Functor>::first_argument_type) > curry(Functor f) { return curry_ < typename function_traits<Functor>::result_type , typename function_traits<Functor>::first_argument_type , typename function_traits<Functor>::second_argument_type > (f); }
      
      







さて、非常に読みにくいコードはわずか20行で、既に次のように記述できます。

 cout << curry(sum)(42)(10) << endl;
      
      







私の意見では、これは成功です! :)三項関数にcurry_



を実装することは残っています。さらに、これらの目的のために別の関数名を取得する必要がないように-関数をオーバーロードすることですべてを解決します。 これまでのところ、私の本能は、これが問題になることを教えてくれます。 curry



ラッパー関数を見てください:1つのパラメーター(直接カリーされる関数)のみを取りますが、関数のアリティに応じて常に異なるタイプのオブジェクトを返す必要があります(つまり、関数を返す関数または返す関数を返します)関数を返す関数、または... mmmで十分です)。



この問題を解決するには、 少しメタプログラミングを曲げて、インスピレーションを考えて見つける必要があります。 そのため、まず第一に、単項、2項、3項、... n項関数を区別する必要があります。 このため、テンプレートの特殊化に応じて異なる値で初期化される静的定数をfunction_traits



追加できると思います。 次に、 curry



ラッパー関数に追加のダミー引数を追加します。これは、コンパイル段階で関数のオーバーロードの解決にのみ参加します。



次のものが得られます。

 template <typename Func> struct function_traits {}; //      template <typename R, typename T0> struct function_traits<R(*)(T0)> { typedef R result_type; typedef T0 argument_type; static const int arity = 1; }; //      template <typename R, typename T0, typename T1> struct function_traits<R(*)(T0,T1)> { typedef R result_type; typedef T0 first_argument_type; typedef T1 second_argument_type; static const int arity = 2; }; //     template <typename R, typename T0, typename T1, typename T2> struct function_traits<R(*)(T0,T1,T2)> { typedef R result_type; typedef T0 first_argument_type; typedef T1 second_argument_type; typedef T2 third_argument_type; static const int arity = 3; }; //     template<typename Functor, int NArgs> struct count_args : std::enable_if<function_traits<Functor>::arity == NArgs> { static_assert(NArgs >= 0, "Negative number? WTF?"); }; //  //       ,     template <typename Functor> Functor curry(Functor f, typename count_args<Functor, 1>::type * = 0) { return f; } //   template <typename Functor> std::function< std::function< typename function_traits<Functor>::result_type(typename function_traits<Functor>::second_argument_type) >(typename function_traits<Functor>::first_argument_type) > curry(Functor f, typename count_args<Functor, 2>::type * = 0) { return curry_ < typename function_traits<Functor>::result_type , typename function_traits<Functor>::first_argument_type , typename function_traits<Functor>::second_argument_type > (f); } //   template <typename Functor> std::function< std::function< std::function< typename function_traits<Functor>::result_type(typename function_traits<Functor>::third_argument_type) >(typename function_traits<Functor>::second_argument_type) >(typename function_traits<Functor>::first_argument_type) > curry(Functor f, typename count_args<Functor, 3>::type * = 0) { return curry_ < typename function_traits<Functor>::result_type , typename function_traits<Functor>::first_argument_type , typename function_traits<Functor>::second_argument_type , typename function_traits<Functor>::third_argument_type > (f); }
      
      







一般的に、ここではすべてが非常に明確です。 2番目のダミーパラメーターを追加して、関数のオーバーロードを実装しました。 このパラメーターは、 std::enable_if



を使用して、オーバーロードの解決中に不適切なcurry



オプションを「オフ」にします。 また、元の関数を返すだけの単項関数のカレー実装も追加しました。 三項関数のcurry_



関数の実装を記述することは残っています。 理論的な部分では、三項関数のカリー化中、結果は単項となり、理論的には2つの引数の関数を返すことができますが、実際にはカリー化されたバージョンを返します。 この知識があれば、3つの引数の実装は非常に簡単です。

 <typename R, typename T0, typename T1, typename T2> function<function<function<R(T2)>(T1)>(T0)> curry_(function<R(T0,T1,T2)> f) { return [=] (T0 const & t0) -> function<function<R(T2)>(T1)> { return curry_<R,T1,T2>([=] (T1 const & t1, T2 const & t2) { return f(t0,t1,t2); }); }; }
      
      





一般に、これらすべてを(もちろん使用例は除きますが) mega



ネームスペースでラップし、ファンクターのfunction_traits



特殊化を追加し、1つのヘッダーファイルに詰めてGitHubにアップロードしました。 何らかの形でREADMEを追加する必要があります:)これで、3項関数を使用してナンセンスを書くことができます。 はい、このようにでも:

 string foo(string s, int i, double d) { ostringstream str; str << s << ": " << i << " . = " << d << "$"; return str.str(); } int main() { cout << mega::curry(foo)("-")(2)(9.95) << endl; // => -: 2 . = 9.95$ return EXIT_SUCCESS; }
      
      







このアプローチの魅力は、「より広い」ものから新しい、より具体的な機能を即座に構築できることです。 正確にこれをどのように、どこで適用できるか、誰もが自分で決定します。



正直なところ、似たようなものが既に実装されているかどうかはチェックしなかったので、アカデミックな観点から興味深いので、別のバイクを書いたことを除外しません。 さらに、最適性の観点から、あまり詳細に説明しませんでした。 C ++コンパイラはそのようなことを最適化しないとすぐに言うことができます。その結果、レジスタ間で異なる情報をドラッグすることで、大量のcall



を取得します。 しかし、あなたは利便性のために支払う必要があります。



ご清聴ありがとうございました。



フォースがあなたと共にいるように



PS早朝になりましたが、オンラインで出かける可能性が最も高いのは午後遅くです。 だから私なしではあまりやらないでください:)



All Articles