この記事では、お気に入りのC ++での関数のカリー化と部分適用のオプションの1つについて説明します。このアクションの実験的な実装を示し、数学なしで、指でカリー化とは何か、 kari.hppの裏側について説明します機能をカレーします。 さて、ここで慣習的であるように:興味がある人-私は猫を求めます。
カレー
カレーとは何ですか? これは、 Haskellプログラマーが(もちろんモナドの後)急いで行きたいと思う最も人気のある言葉の1つであるように思えます。 また、この用語の本質はスティックのように単純であり、その意味を知っているか、 MLやHaskellなどの言語で書いた人は誰でもこのセクションを安全にスキップできます。
カリー化とは、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
関数オブジェクトを作成するか、渡された関数f
をargs
引数に適用するヘルパー関数。
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つの興味深い点:
- これらの引数で呼び出され、アプリケーションカウンタ
N
ゼロの場合にのみ、関数を引数に適用します - 関数が適用できない場合、そのような呼び出しは部分的なアプリケーションであると
curry_t
、関数とその内容として渡される引数を使用して機能オブジェクトcurry_t
を作成します
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つのオーバーロードされた呼び出し演算子関数があります。
パラメーターのない関数は、可変数の引数(
curryV
またはcurryN
を使用して作成された)を持つ関数を使用しようとする方法として機能します。 その中で、アプリケーションカウンターをゼロに下げて、関数を使用し、このために必要なmake_curry
関数をすべて渡す時間であることを示します。
1つの引数の関数は、アプリケーションカウンタを1つ下げ(下げる余地がある場合)、新しい引数
a
を既に蓄積されたargs_
引数を持つタプルに追加し、make_curry
ます。
可変数の引数の関数は、複数の引数を部分的に適用するための策略として機能します。 彼女はそれらを1つずつ再帰的に適用します。 これらを一度に適用すると、次の2つの理由で失敗します。
- 引数がなくなる前にアプリケーションカウンターがゼロに達することがあります
- 関数
f_
は以前に呼び出して別のカリー化された関数を返すことができ、次の引数はそのためのものです
- 最後の関数は、 左辺値で
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
- 関数の構成
(*2)
と(+2)
は4
適用されます。(4 + 2) * 2 = 12
- 関数
(*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 ++でできるか? さて、あなたが見ることができるように、どういうわけか、しかし両方ともできました。 最後まで読んでくれた人に感謝します。