C ++の関数型プログラミング要素:部分的なアプリケーション

私は理論に深く入りません。 部分的なアプリケーションとは、インターネットで簡単に見つけられるものです。 ウィキペディアに含む。







つまり、これは関数のk



引数をn



引数から修正し、 (n - k)



n



引数の関数にすることができるメカニズムです。







 //    f   : int f (int a, int b, int c, int d) { return a + b + c + d; } //    : auto g = part(f, 1, 2); // 1 + 2 + ... //   : assert(g(3, 4) == 10); // ... + 3 + 4 = 10
      
      





Habréを含む多くの出版物がこの主題に関して既に存在します:







  1. C ++可変長テンプレート。 カレーと部分使用
  2. C ++での部分的な使用とカリー化
  3. カレーC ++


そして、「 どのように機能カレーを作るべきですか? 」stackoverflowのブランチは、このトピックに最初に出会った人たちの単なる倉庫です。







残念ながら、量はまだ質に成長しておらず、私は良い、使用可能なオプションを見ていません。 同時に、これは好奇心is盛です。







素晴らしい事実番号1 。 上記の記事には、正しい(私の意見では)部分的なアプリケーションを実装するために必要なすべてのテクニックが含まれています。







慎重にすべてを分析し、キューブを正しい順序に並べるだけです。 それが私がこの記事でやろうとしていることです。









内容



  1. 目標
  2. 既存のソリューション
  3. 哲学する
  4. 新しいソリューション
  5. 実装
  6. おわりに




目標



私たちの目標は何ですか:







  1. 実際に必要な機能の実装







    つまり、関数の部分的なアプリケーションの何らかの形での実装。







  2. 可能な限り最高の効率







    C ++で書くことを忘れてはなりません。その基本原則の1つは、「オーバーヘッドのない抽象化」の原則です( Stroustrup、Foundations of C ++を参照)。







    特に、余分なコピーや転送は発生しません。







    非表示のテキスト
    これは、プラスのプログラミングで最も重要な場所の1つであるだけでなく、同時に最も興味深い場所の1つであることを付け加えます。


  3. 使いやすさ







    アプリケーションプログラマは部分的に適用された関数を簡単に作成できるはずです。







    また、既存のソリューションと組み合わせて、部分的に適用された機能を使用することも簡単でなければなりません。 たとえば、部分的に適用された関数をいくつかの標準アルゴリズムに渡します。











既存のソリューション



利用可能なソリューションの作成者は、2つの主なオプションを提供しています。 可能であれば、それらの使用明示的なアプリケーションを呼び出します







可能な限りアプリケーション



この手法では、部分的なアプリケーションの結果は、再び部分的に適用できる関数になります。 そして、元の関数の呼び出しは、受け取った引数が呼び出しを行うのに十分であるときに発生します。







 //   : int f (int a, int b, int c, int d); //   : auto g = part(f); //   : auto h = g(1); //       `f`,     . //    : auto i = h(2, 3); //   -     `f`. //   ,    , , //    . assert(i(4) == 10);
      
      





機知に富んだアイデア。 しかし、残念ながら、実際には機能しません。







なんで? 任意の数の変数の合計を計算する関数を部分的に適用するとします。







 auto sum (auto ... xs);
      
      





この場合、部分適用のプロセスでいつ停止するかはわかりません。

この量は、1つ、2つ、3つ、実際には任意の数の変数から計算されます。







したがって、 f



代わりにsum



使用すると、前の例は2番目のステップで中断します。







 auto g = part(sum); auto ??? = g(1); //  ,   ?
      
      





関数に異なる数の引数を持つ複数のオーバーロードがある場合、同じことが起こります。 どちらを呼び出す必要があるかは明確ではありません。







正しい数の引数をスリップできる場合、別の問題があります。 この例では、これを描写できます。







 auto g = part(f, 1, 2, 3); auto h = g(4, 5, 6); // ...
      
      





その後、関数は部分的に無限に適用され、呼び出されることはありません。







明示的な適用



したがって、著者は、部分的なアプリケーションをいつ停止するかわからないので、明示的にやろうと言います。







つまり、引数なしの「括弧」演算子のオーバーロードを作成します。その呼び出しは、すでに部分的に適用されているパラメーターで内部関数を呼び出す必要があることを意味します。







 auto g = part(sum); auto h = g(1); //  ,   ?  ! auto i = h(2, 3, 4, 5, 6); //      . assert(i() == 21); //    .
      
      





古い問題は解決されました。 しかし、新しいものが生まれました。







現在、部分的に適用された関数の使用は、それが部分的に適用された関数であることを知ることを意味します。 これは、標準のアルゴリズムでは使用できないことを意味します。 彼らは単に引数を投げた後でも空の「括弧」を呼び出す必要があることを知らないだけです。









哲学する



先に進むために、私たちは少し哲学し、いくつかの重要な考えを表現します。







最初の考え



部分的な使用は遅延呼び出しです。 つまり、部分適用では「部分計算」は行われません。 いくつかの引数を覚えているだけで、残りが来るのを待ちます。







合計、積、およびその他の連想関数について、部分的に計算された結果(合計または以前に取得した数値の積)を返すことができ、部分的な適用を続行できる松葉杖がまだ考えられる場合、そのような解決策は任意の関数に対して機能しません。







再考



C ++には組み込みの部分的なアプリケーションメカニズムはありません。 これは、一般的に言えば、プログラマーは、引数の一部を任意の関数に転送し、残りをいつか「休める」ことを期待していないことを意味します。







したがって、プログラマーは部分的なアプリケーションを使用して、それを使用することを知っています。

したがって、 C ++での部分的な適用は常に明示的です。







考えた結果



これらのオプションは適切ではないことがわかります。 一見小さな欠陥があるため、これらの「クールな」決定は非常に非実用的であり、したがって日常の使用には不適切であると認識される必要があります。







それでも、C ++は関数型言語ではないことを忘れないでください。 ある言語の構成とイデオロギーを単に別の言語に移して移すことはできません。 ここでは、外国語からの詩の翻訳と同様に、翻訳は逐語的ではなく、詩的、つまり翻訳言語の韻であるべきです。









新しいソリューション



上記に基づいて、私は次の結論に達しました。 プログラマーは、部分的なアプリケーションがどこにあり、関数呼び出しがどこにあるかを常に知っているため、モデルを単純化し、より普遍的にすることができます。







次の2つの段階で構成されます。







  1. 元の引数をキャプチャします。
  2. 残りの引数を使用した無条件呼び出し。


例で説明します。







 //   : auto sum (auto ... xs); //      . auto f = part(sum, 1, 2, 3); //      `f`. //   : assert(f(4) == 10); assert(f(4, 5) == 15); assert(f(4, 5, 6) == 21);
      
      





さらにいくつかの引数を部分的に適用する必要がある場合、部分的なアプリケーション関数への同じ明示的な呼び出しを使用して、それらは「破棄」されます。







 auto g = part(f, 4, 5); //  part(sum, 1, 2, 3, 4, 5).
      
      





これはstd::bind



です!



思われますが、違います。 一方、 std::bind



使用すると、引数をより柔軟に処理できます。 ランダムな場所に置いたり、混ぜたりしてください。







一方、 std::bind



はプレースホルダーの明示的な配置を必要とし、任意の数の引数では機能しません。 つまり、ユーザーは、将来どのくらいの数の引数が「スロー」されるか、およびコール内のどの特定の位置に立つかを事前に示す必要があります。







したがって、結果として得られるソリューションは非常に独立しており、他の利用可能なメカニズムの特殊なケースではないと考えています。









実装



おそらく最も興味深い部分です。 コード。 記事の冒頭で、私はすでに1つの注目すべき事実に言及しました。 だから、もう一つあります。







素晴らしい事実番号2 。 標準ライブラリ(C ++ 17)には、このモデルの部分的なアプリケーションの実装に必要なほぼすべてのものがあります。







1つの操作を再定義するだけで済みますが、同じ標準ツールでも表現できます。







そのため、次のものが必要になります(完全かつ網羅的なリスト):







  1. std ::フォワード
  2. std ::移動
  3. std :: forward_as_tuple
  4. std ::適用
  5. std ::呼び出す
  6. std :: tuple_cat
  7. std :: make_tuple


必要なものはほとんどすべてあると言ったとき、1つの関数を再定義する必要があることを意味しました。







 template <typename T> constexpr decltype(auto) forward_tuple (T && t) { return apply(forward_as_tuple, std::forward<T>(t)); }
      
      





この関数は、任意のタプルを取り、入力タプルの要素への参照で構成される新しいタプルを返します。 入力タプルにlvalue



への参照が含まれていた場合、それらはそのまま残ります。 タプルに格納されたのと同じオブジェクトは、参照によってrvalue



渡されます。







これで、必要なものはすべて揃っていると自信を持って言えます。 コードを書くことができます。







  1. part



    機能







     template <typename ... As> constexpr auto part (As && ... as) -> part_fn<decltype(std::make_tuple(std::forward<As>(as)...))> { return {std::make_tuple(std::forward<As>(as)...)}; }
          
          





    任意の数の引数を取ります。最初の引数は関数または関数オブジェクトです。 make_tuple



    関数を使用してすべてを1つのタプルに保存し、このタプルをpart_fn



    構造にラップして返します。







  2. 構造part_fn









     template <typename Tuple> struct part_fn { template <typename ... As> constexpr decltype(auto) operator () (As && ... as) const & { return apply(invoke, std::tuple_cat(forward_tuple(t), std::forward_as_tuple(std::forward<As>(as)...))); } template <typename ... As> constexpr decltype(auto) operator () (As && ... as) & { return apply(invoke, std::tuple_cat(forward_tuple(t), std::forward_as_tuple(std::forward<As>(as)...))); } template <typename ... As> constexpr decltype(auto) operator () (As && ... as) && { return apply(invoke, std::tuple_cat(forward_tuple(std::move(t)), std::forward_as_tuple(std::forward<As>(as)...))); } Tuple t; };
          
          





    1. 部分的に適用されたオブジェクト(関数とその最初のk



      引数)を保存します。







    2. 任意の数の引数を取る括弧演算子があります







      1. 呼び出されると、入力引数へのリンクのタプルがforward_as_tuple



        関数を使用して形成されます。
      2. 以前に保存されたオブジェクトを含むタプルも、 forward_tuple



        したforward_tuple



        関数を使用してリンクタプルに変換forward_tuple



        ます。
      3. 両方のリンクタプルはtuple_cat



        関数を使用して1つに接着されます。 リンクの1つの大きなタプルが判明しました。
      4. 接着されたタプルは展開され、 apply



        関数を使用してinvoke



        関数に渡されます。
      5. 受信した引数から関数呼び出しを呼び出します。






それだけです







この時点で、トリッキーなテンプレートコード(たとえば私のような)が好きな人は、少しがっかりするはずです。







一方、ソリューションのシンプルさ、優雅さ、およびいくつかの標準的な「キューブ」から組み立てられるという事実は、それが非常に実行可能であり、アプリケーションコードに簡単に統合できることを間接的に示しています。







使用する



 int modulo_less (int modulo, int a, int b) { return (a % modulo) < (b % modulo); } auto v = std::vector<int>{...}; std::sort(v.begin(), v.end(), part(modulo_less, 7));
      
      





make_tuple



呼び出しのおかげで、 std::ref/cref



サポートされます。







 int append (std::string & s, int x) { return s += std::to_string(x); } auto s = std::string("qwerty"); auto g = part(append, std::ref(s)); g(5); g(6); assert(s == "qwerty56");
      
      





また、 invoke



関数のおかげで、クラスメソッドの呼び出しstd :: invokeを参照)など、さまざまな複雑なケースがサポートされます。







そして、これはすべて「箱から出して」、特別なジェスチャーなしで行われます。









おわりに



私の決定が正しいと思うのはなぜですか。







  1. シンプルで透明です。 これは、 すでに言語に含まれている「基本」コンポーネントから組み立てられます。
  2. 効果的です。 オーバーヘッドなしの抽象化。
  3. ハンマーのように安全です。 「魔法」はありません。
  4. 既存のC ++プログラミングパラダイムとうまく統合されます。
  5. 標準ライブラリで使用されるツールと互換性があります。 たとえば、 reference_wrapper



    を使用して、参照によるパラメーターの転送を通知し( std::make_tuple



    std::bind



    std::thread



    )、さらに非常に重要なアルゴリズムを使用します。


»完全なソースコードはこちら







コンテンツへ








All Articles