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

この記事では、お気に入りのC ++での関数のカリー化と部分適用のオプションの1つについて説明します。このアクションの実験的な実装を示し、数学なしで、指でカリー化とは何か、 kari.hppの裏側について説明します機能をカレーします。 さて、ここで慣習的であるように:興味がある人-私は猫を求めます。


カレー


カレーとは何ですか? これは、 Haskellプログラマーが(もちろんモナドの後)急いで行きたいと思う最も人気のある言葉の1つであるように思えます。 また、この用語の本質はスティックのように単純であり、その意味を知っているか、 MLHaskellなどの言語で書いた人は誰でもこのセクションを安全にスキップできます。


カリー化とは、N個の引数の関数を1つの引数の関数に変換し、次の引数の関数を返すなどの操作です。 最後の引数から関数を返すまで、それらはすべて適用されません。 例に追われて:


int sum2(int lhs, int rhs) { return lhs + rhs; } 

バイナリ加算機能があります。 それを1つの引数の関数に変える方法は? 非常にシンプル:


 auto curried_sum2(int lhs) { return [=](int rhs) { return sum2(lhs, rhs); }; } 

私たちは何をしましたか? ラムダの唯一の引数を値でキャプチャし、残りの2番目の引数を取り、最終的に加算操作を実行します。 その結果、 curried_sum2関数curried_sum2を引数に順番に適用できます。


 // output: 42 std::cout << sum2(40, 2) << std::endl; std::cout << curried_sum2(40)(2) << std::endl; 

それだけです。これがカレー操作の意味です。 もちろん、これはすべてのアリティの関数に絞ることができます -本質は同じままであり、関数の各アプリケーションを引数にN-1個の引数からカリー化された関数を返す必要があります。


 auto sum3(int v1, int v2, int v3) { return v1 + v2 + v3; } auto curried_sum3(int v1) { return [=](int v2){ return [=](int v3){ return sum3(v1, v2, v3); }; }; } // output: 42 std::cout << sum3(38, 3, 1) << std::endl; std::cout << curried_sum3(38)(3)(1) << std::endl; 

部分適用


部分的なアプリケーションとは、関数Nの引数を呼び出し、これらの引数の一部のみを渡す機能です。このような呼び出しの結果は、残りの引数からの別の関数になります。


Haskellのような言語では、これらすべてがプログラマーの背後で自動モードで動作することに言及する価値があります。 これを描写しようとします。つまり、理想的には、 sum3(38,3)(1)またはsum3(38)(3,1)ように関数sum3を呼び出す機能が必要です。 さらに、関数が別のカリー化された関数を返す場合、最初の引数のリストを使用して同じ方法で呼び出すことができます。 例:


 int boo(int v1, int v2) { return v1 + v2; } auto foo(int v1, int v2) { return kari::curry(boo, v1 + v2); } // output: 42 std::cout << kari::curry(foo)(38,3,1) << std::endl; std::cout << kari::curry(foo)(38,3)(1) << std::endl; std::cout << kari::curry(foo)(38)(3,1) << std::endl; 

私は少し先を走ってkari.hppの使用を示さなければなりませんでした。はい、彼女はその方法を知っています。


目標設定


何かを書くためには、(できれば?)出力で何を取得したいかを理解する必要があります。 そして出力では、C ++で呼び出すことができるすべてのものをカリー化し、部分的に適用できるようにしたいと考えています。 すなわち:



可変数の引数を持つ関数は、カリー化する引数の数を具体的に示すことでカリー化できます。 std :: bindとの通常の相互作用とその結果も望ましいです。 そしてもちろん、いくつかの引数に関数を適用し、ネストされた関数を呼び出すことができるようにする必要があります。これにより、1つのカリー化された関数と対話しているように見えます。


当然、パフォーマンスを忘れてはなりません。 さまざまなラッパーのコストを最小化し、引数を渡して保存する必要があります。 コピーの代わりに転送し、本当に必要なものだけを保存し、できるだけ早く提供(できれば転送)します。


作者、あなたはstd::bindを発明しようとしていますstd::bind


はい、いいえ。 std::bind 、間違いなく強力で実績のあるツールです。キラーや代替の実装を書くつもりはありません。 はい、カリー化および明示的な部分適用(使用するもの、場所、量を示す)のツールとして使用できます。 しかし、これはそのような場合には不便であり、常に適用できるわけではありません。関数の特定のアリティを知り、特定のバインダー(バインディング?)を書く必要があるからです。 例:


 int foo(int v1, int v2, int v3, int v4) { return v1 + v2 + v3 + v4; } // std::bind auto c0 = std::bind(foo, _1, _2, _3, _4); auto c1 = std::bind(c0, 15, _1, _2, _3); auto c2 = std::bind(c1, 20, 2, _1); auto rr = c2(5); std::cout << rr << std::endl; // output: 42 // kari.hpp auto c0 = kari::curry(foo); auto c1 = c0(15); auto c2 = c1(20, 2); auto rr = c2(5); std::cout << rr << std::endl; // output: 42 

API


 namespace kari { template < typename F, typename... Args > constexpr decltype(auto) curry(F&& f, Args&&... args) const; template < typename F, typename... Args > constexpr decltype(auto) curryV(F&& f, Args&&... args) const; template < std::size_t N, typename F, typename... Args > constexpr decltype(auto) curryN(F&& f, Args&&... args) const; template < typename F > struct is_curried; template < typename F > constexpr bool is_curried_v = is_curried<F>::value; template < std::size_t N, typename F, typename... Args > struct curry_t { template < typename... As > constexpr decltype(auto) operator()(As&&... as) const; }; } 



kari::curry(F&& f, Args&&... args)


オプションのargs引数が適用されたcurry_t型( curry_t関数)の関数オブジェクト、または渡された関数f引数を適用した結果を返します(関数がnullaryの場合、または渡された引数で十分な場合)。


すでにカリー化された関数をパラメーターfで渡すと、 args引数が適用されたコピーを返します。




kari::curryV(F&& f, Args&&... args)


可変数の引数を持つ関数をカリー化できます。 そのような関数は、引数なしで演算子()によって後で呼び出すことができます。 例:


 auto c0 = kari::curryV(std::printf, "%d + %d = %d"); auto c1 = c0(37, 5); auto c2 = c1(42); c2(); // output: 37 + 5 = 42 

パラメータfを使用して既にカリー化された関数を渡すと、 args引数が適用された可変数の引数によって、アプリケーションの変更されたタイプでそのコピーを返します。




kari::curryN(F&& f, Args&&... args)


argsとして渡されるものに加えて)適用する引数N特定の数を示す、可変数の引数を持つ関数をカリー化できます。 例:


 char buffer[256] = {'\0'}; auto c = kari::curryN<3>(std::snprintf, buffer, 256, "%d + %d = %d"); c(37, 5, 42); std::cout << buffer << std::endl; // output: 37 + 5 = 42 

パラメーターfが既にカリー化された関数を渡すと、 args引数が適用されたN引数によって変更されたタイプのアプリケーションを含むそのコピーを返します。




kari::is_curried<F>, kari::is_curried_v<F>


関数のカリー化をチェックするための補助構造。 例:


 const auto l = [](int v1, int v2){ return v1 + v2; }; const auto c = curry(l); // output: is `l` curried? no std::cout << "is `l` curried? " << (is_curried<decltype(l)>::value ? "yes" : "no") << std::endl; // output: is `c` curried? yes std::cout << "is `c` curried? " << (is_curried_v<decltype(c)> ? "yes" : "no") << std::endl; 



kari::curry_t::operator()(As&&... as)


カリー化された関数の部分的または完全な使用の演算子。 呼び出しの結果は、タイプF元の関数の残りの引数のカリー化された関数、または呼び出しで渡された累積引数と新しい引数への適用によって取得されたこの関数の結果です。 例:


 int foo(int v1, int v2, int v3, int v4) { return v1 + v2 + v3 + v4; } auto c0 = kari::curry(foo); auto c1 = c0(15, 20); // partial application auto rr = c1(2, 5); // function call - foo(15,20,2,5) std::cout << rr << std::endl; // output: 42 

curryVまたはcurryNを使用したcurryV関数への引数なしの呼び出しは、この呼び出しに十分な引数がある場合、それを呼び出そうとします。 それ以外の場合は、部分的に適用された関数を返します。 例:


 auto c0 = kari::curryV(std::printf, "%d + %d = %d"); auto c1 = c0(37, 5); auto c2 = c1(42); // force call variadic function std::printf c2(); // output: 37 + 5 = 42 

実装の詳細


実装の詳細を引用して、テキストの量を減らし、 SFINAEの不必要な説明と山を避け 、14年目の標準の一部として追加しなければならなかったものの実装を避けるために、C ++ 17を使用します。 これらすべての詳細はプロジェクトリポジトリで表示でき、同時にアスタリスクを付けることができます:)




make_curry(F&& f, std::tuple<Args...>&& args)


curry_t関数オブジェクトを作成するか、渡された関数fargs引数に適用するヘルパー関数。


 template < std::size_t N, typename F, typename... Args > constexpr auto make_curry(F&& f, std::tuple<Args...>&& args) { if constexpr ( N == 0 && std::is_invocable_v<F, Args...> ) { return std::apply(std::forward<F>(f), std::move(args)); } else { return curry_t< N, std::decay_t<F>, Args... >(std::forward<F>(f), std::move(args)); } } template < std::size_t N, typename F > constexpr decltype(auto) make_curry(F&& f) { return make_curry<N>(std::forward<F>(f), std::make_tuple()); } 

この関数の2つの興味深い点:





struct curry_t


蓄積された引数を保存する機能オブジェクトと、最終アプリケーションで呼び出す必要のある関数は、呼び出されて部分的に適用されます。


 template < std::size_t N, typename F, typename... Args > struct curry_t { template < typename U > constexpr curry_t(U&& u, std::tuple<Args...>&& args) : f_(std::forward<U>(u)) , args_(std::move(args)) {} private: F f_; std::tuple<Args...> args_; }; 

args_引数をstd :: tupleに args_したargs_ は、いくつかの理由で良い考えです。


1)必要に応じてリンクを保存するために、デフォルトでは値によってstd :: refを使用したシチュエーションの自動処理
2)関数の引数への便利な適用( std :: apply
3)それは準備ができており、あなたの手で書く必要はありません:)


呼び出されたオブジェクトまたは関数f_も、値f_に保存し、コンストラクターのユニバーサルリンクを介して移動またはコピーして作成するときに、そのタイプを慎重に選択する必要があります(詳細は以下を参照)


テンプレートパラメータNは、可変数のパラメータを持つ関数のアプリケーションカウンタとして機能します。




curry_t::operator()(const As&...)


そしてもちろん、機能オブジェクトを呼び出す演算子で起こっていることのすべての塩。


 template < std::size_t N, typename F, typename... Args > struct curry_t { // 1 constexpr decltype(auto) operator()() && { return detail::make_curry<0>( std::move(f_), std::move(args_)); } // 2 template < typename A > constexpr decltype(auto) operator()(A&& a) && { return detail::make_curry<(N > 0 ? N - 1 : 0)>( std::move(f_), std::tuple_cat( std::move(args_), std::make_tuple(std::forward<A>(a)))); } // 3 template < typename A, typename... As > constexpr decltype(auto) operator()(A&& a, As&&... as) && { return std::move(*this)(std::forward<A>(a))(std::forward<As>(as)...); } // 4 template < typename... As > constexpr decltype(auto) operator()(As&&... as) const & { auto self_copy = *this; return std::move(self_copy)(std::forward<As>(as)...); } } 

4つのオーバーロードされた呼び出し演算子関数があります。


  1. パラメーターのない関数は、可変数の引数( curryVまたはcurryNを使用して作成された)を持つ関数を使用しようとする方法として機能します。 その中で、アプリケーションカウンターをゼロに下げて、関数を使用し、このために必要なmake_curry関数をすべて渡す時間であることを示します。


  2. 1つの引数の関数は、アプリケーションカウンタを1つ下げ(下げる余地がある場合)、新しい引数aを既に蓄積されたargs_引数を持つタプルに追加し、 make_curryます。


  3. 可変数の引数の関数は、複数の引数を部分的に適用するための策略として機能します。 彼女はそれらを1つずつ再帰的に適用します。 これらを一度に適用すると、次の2つの理由で失敗します。


    • 引数がなくなる前にアプリケーションカウンターがゼロに達することがあります
    • 関数f_は以前に呼び出して別のカリー化された関数を返すことができ、次の引数はそのためのものです

  4. 最後の関数は、 左辺値curry_tを呼び出し、 右辺 で関数を呼び出すためのブリッジとして機能します。

何が起こっているかの魔法は、 ref修飾された関数にメモを追加することです。 要するに、彼らの助けを借りて、オブジェクトが右辺値リンクによって呼び出されたことがわかり、引数を最終的なmake_curry呼び出し関数にコピーする代わりに安全に移動できることがmake_curryます。 それ以外の場合、引数が適切であることを認識してこの関数を再度呼び出す機能を保存するために、引数をコピーする必要があります。


ボーナス


最後に、 kari.hppに組み込まれ、ボーナスとしてバンドルされている構文糖の例をいくつか紹介します。


オペレーターセクション


Haskell言語に精通しているプログラマーは演算子セクションに精通しているため、その中での演算子の部分的な使用について簡単に説明できます。 たとえば、構造(*2) 、1つの引数の関数を生成し、その結果は、渡された引数の2倍になります。 このようなものをC ++で描きたかったのです。 すぐに言ってやった!


 using namespace kari::underscore; std::vector<int> v{1,2,3,4,5}; std::accumulate(v.begin(), v.end(), 0, _+_); // result: 15 std::transform(v.begin(), v.end(), v.begin(), _*2); // v = 2, 3, 6, 8, 10 std::transform(v.begin(), v.end(), v.begin(), -_); // v = -2,-3,-6,-8,-10 

機能構成


さて、 機能構成を描くという考えがなければ、診断は決定的ではありません。 合成operator *は、数学で利用可能な合成記号の中で最も外側に類似するものとしてoperator *が選択されました。 また、結果の関数を引数に適用できます。 結果:


 using namespace kari::underscore; // 1 std::cout << (_*2) * (_+2) * 4 << std::endl; // output: 12 // 2 std::cout << 4 * (_*2) * (_+2) << std::endl; // output: 10 

  1. 関数の構成(*2)(+2)4適用されます。 (4 + 2) * 2 = 12
  2. 関数(*2)4に適用され、結果(+2)適用されます。 (4 * 2 + 2) = 10

同時に、非常に複雑な構成を無意味な表記で構成することもできますが、Haskellプログラマーだけが理解できます:)


 // (. (+2)) (*2) $ 10 == 24 // haskell  std::cout << (_*(_+2))(_*2) * 10 << std::endl; // output: 24 // ((+2) .) (*2) $ 10 == 22 // haskell  std::cout << ((_+2)*_)(_*2) * 10 << std::endl; // output: 22 

おわりに


そして、私なしでは、実際のプロジェクトでこれを使用する価値がないことは明らかですが、私はそれを言わなければなりませんでした。 目標は、むしろ自分自身と新しいC ++をテストすることでした。 できますか? C ++でできるか? さて、あなたが見ることができるように、どういうわけか、しかし両方ともできました。 最後まで読んでくれた人に感謝します。



Source: https://habr.com/ru/post/J340722/


All Articles