Boost mailinglist で、タプルのようなエンティティを作成する次の巧妙なトリックが@LouisDionneによって最近投稿されました。
#include <iostream>
auto list = [](auto ...xs) {
return [=](auto access) { return access(xs...); };
};
auto length = [](auto xs) {
return xs([](auto ...z) { return sizeof...(z); });
};
int main()
{
std::cout << length(list(1, '2', "3")); // 3
}
実際の例 。
賢いのは、list
が可変パラメータリストを入力として取り、出力としてラムダを返すラムダで、別のラムダをその入力に作用させることです。同様に、length
は、リストのようなエンティティをとるラムダであり、可変長のsizeof...
演算子をリストの元の入力パラメーターに提供します。 sizeof...
演算子は、list
に渡すことができるようにラムダの内側にラップされています。
質問:このタプル作成イディオムに名前はありますか?おそらく、高次関数がより一般的に使用される関数型プログラミング言語からのものです。
これはモナドのようなもの、特に継続モナドと同じ精神の何かの微妙な実装だと思います。
モナドは、計算の異なるステップ間の状態をシミュレートするために使用される関数型プログラミング構造です(関数型言語はステートレスであることを思い出してください)。
モナドは、さまざまな関数を連鎖させて、「計算パイプライン」を作成し、各ステップが計算の現在の状態を把握するようにします。
モナドには2つの主要なピラールがあります。
ウィキペディア には、モナドに関する非常に良い例と説明があります。
与えられたC++ 14コードを書き換えましょう:
auto list = []( auto... xs )
{
return [=]( auto access ) { return access(xs...); };
};
ここで、モナドのreturn
関数を特定すると思います。値を取り、モナドの方法で返します。具体的には、この戻り値は、「タプル」カテゴリから可変個パックカテゴリに移動するファンクタ(数学的な意味では、C++ファンクタではない)を返します。
auto pack_size = [](auto... xs ) { return sizeof...(xs); };
pack_size
は通常の関数です。パイプラインでいくつかの作業を行うために使用されます。
auto bind = []( auto xs , auto op )
{
return xs(op);
};
そしてlength
はモナドbind
演算子に近いものの非ジェネリックバージョンにすぎません。これは、前のパイプラインステップからモナド値を取得し、指定された関数(関数実際に機能します)。その関数は、この計算ステップによって実行される機能です。
最後に、呼び出しは次のように書き直すことができます。
auto result = bind(list(1,'2',"3"), pack_size);
それで、このタプル作成イディオムの名前は何ですか?まあ、これは " モナドのようなタプル "、それは正確にモナドではないので、タプルの表現と展開は同様の方法で機能し、Haskell継続モナドに残ります。
面白いC++プログラミングの揺れのために、私はこのモナドのようなものを探求してきました。いくつかの例 here を見つけることができます。
私はこのイディオムTuple-continuatorまたはより一般的にはmonadic-continuator。間違いなく、継続モナドのインスタンスです。 C++プログラマーのための継続モナドのすばらしい紹介は here です。本質的に、上記のlist
ラムダは値(可変パラメータパック)を取り、単純な '継続子'(内部クロージャ)を返します。このコンティネーターは、呼び出し可能オブジェクト(access
と呼ばれる)が与えられると、パラメーターパックをそれに渡し、呼び出し可能オブジェクトが返すものをすべて返します。
FPCompleteブログポストから借用した、コンティニュエーターは多かれ少なかれ次のようなものです。
_template<class R, class A>
struct Continuator {
virtual ~Continuator() {}
virtual R andThen(function<R(A)> access) = 0;
};
_
上記のContinuator
は抽象的です-実装を提供していません。だから、ここに簡単なものがあります。
_template<class R, class A>
struct SimpleContinuator : Continuator<R, A> {
SimpleContinuator (A x) : _x(x) {}
R andThen(function<R(A)> access) {
return access(_x);
}
A _x;
};
_
SimpleContinuator
は、タイプA
の1つの値を受け入れ、access
が呼び出されたときにandThen
に渡します。上記のlist
ラムダは基本的に同じです。より一般的です。単一の値の代わりに、内部クロージャーはパラメーターパックをキャプチャし、それをaccess
関数に渡します。きちんと!
うまくいけば、それが継続体であるということの意味を説明します。しかし、モナドになるとはどういう意味ですか?これが良い 紹介 画像を使ったものです。
list
ラムダは、継続モナドとして実装されているリストモナドでもあると思います。 継続モナドはすべてのモナドの母です であることに注意してください。つまり、継続モナドで任意のモナドを実装できます。もちろん、リストモナドは手の届くところにあります。
パラメータパックは自然に「リスト」(多くの場合、異種のタイプ)であるため、リスト/シーケンスモナドのように機能することは理にかなっています。上記のlist
ラムダは、C++パラメータパックをモナド構造に変換する非常に興味深い方法です。したがって、操作を次々に連鎖させることができます。
ただし、上記のlength
ラムダはモナドを壊し、ネストされたラムダは単に整数を返すため、少しがっかりします。以下に示すように、長さ「getter」を書き込むには、おそらくより良い方法があります。
---- Functor ----
リストラムダがモナドであると言える前に、それがファンクタであることを示す必要があります。つまり、fmapはリスト用に作成する必要があります。
上記のラムダのリストは、パラメータパックからファンクタの作成者として機能します。基本的には、return
として機能します。その作成されたファンクターはそれ自体(キャプチャー)でパラメーターパックを保持し、可変数の引数を受け入れる呼び出し可能オブジェクトを提供する場合、それへの「アクセス」を許可します。呼び出し可能オブジェクトはEXACTLY-ONCEと呼ばれることに注意してください。
そのようなファンクタのためにfmapを書いてみましょう。
_auto fmap = [](auto func) {
return [=](auto ...z) { return list(func(z)...); };
};
_
Funcのタイプは(a-> b)でなければなりません。つまり、C++では、
_template <class a, class b>
b func(a);
_
Fmapのタイプはfmap: (a -> b) -> list[a] -> list[b]
です。つまり、C++では、
_template <class a, class b, class Func>
list<b> fmap(Func, list<a>);
_
つまり、fmapは単にlist-of-aをlist-of-bにマッピングします。
今できる
_auto twice = [](auto i) { return 2*i; };
auto print = [](auto i) { std::cout << i << " "; return i;};
list(1, 2, 3, 4)
(fmap(twice))
(fmap(print)); // prints 2 4 6 8 on clang (g++ in reverse)
_
したがって、それはファンクターです。
----モナド----
では、flatmap
(別名bind
、selectmany
)を書いてみましょう
フラットマップのタイプはflatmap: (a -> list[b]) -> list[a] -> list[b].
です
つまり、aをlist-of-bおよびlist-of-aにマップする関数を指定すると、flatmapはlist-of-bを返します。基本的に、それはlist-of-aから各要素を取得し、その上でfuncを呼び出し、1つずつ(可能性としては空の)list-of-bを受け取り、最後にすべてのlist-of-bを連結し、最後に最終的なリストを返します-of-b。
リスト用のフラットマップの実装は次のとおりです。
_auto concat = [](auto l1, auto l2) {
auto access1 = [=](auto... p) {
auto access2 = [=](auto... q) {
return list(p..., q...);
};
return l2(access2);
};
return l1(access1);
};
template <class Func>
auto flatten(Func)
{
return list();
}
template <class Func, class A>
auto flatten(Func f, A a)
{
return f(a);
}
template <class Func, class A, class... B>
auto flatten(Func f, A a, B... b)
{
return concat(f(a), flatten(f, b...));
}
auto flatmap = [](auto func) {
return [func](auto... a) { return flatten(func, a...); };
};
_
これで、リストを使用して多くの強力なことができます。例えば、
_auto pair = [](auto i) { return list(-i, i); };
auto count = [](auto... a) { return list(sizeof...(a)); };
list(10, 20, 30)
(flatmap(pair))
(count)
(fmap(print)); // prints 6.
_
Count関数は、単一の要素のリストを返すため、モナドを保持する操作です。長さを取得したい場合(リストにラップされていない場合)、モナディックチェーンを終了し、次のように値を取得する必要があります。
_auto len = [](auto ...z) { return sizeof...(z); };
std::cout << list(10, 20, 30)
(flatmap(pair))
(len);
_
正しく実行すると、 コレクションパイプライン パターン(たとえば、filter
、reduce
)をC++パラメータパックに適用できるようになります。甘い!
----モナドの法則----
list
モナドが3つすべての モナドの法則 を満たしていることを確認しましょう。
_auto to_vector = [](auto... a) { return std::vector<int> { a... }; };
auto M = list(11);
std::cout << "Monad law (left identity)\n";
assert(M(flatmap(pair))(to_vector) == pair(11)(to_vector));
std::cout << "Monad law (right identity)\n";
assert(M(flatmap(list))(to_vector) == M(to_vector));
std::cout << "Monad law (associativity)\n";
assert(M(flatmap(pair))(flatmap(pair))(to_vector) ==
M(flatmap([=](auto x) { return pair(x)(flatmap(pair)); }))(to_vector));
_
すべてのアサートは満たされています。
----収集パイプライン----
上記の「リスト」ラムダはおそらくモナドであり、ことわざ「リストモナド」の特徴を共有していますが、それは非常に不愉快です。特に、filter
(a.k.a where
)などの一般的な コレクションパイプライン コンビネーターの動作は一般的な期待に応えません。
その理由は、C++ラムダがどのように機能するかです。各ラムダ式は、一意の型の関数オブジェクトを生成します。したがって、list(1,2,3)
は、list(1)
とは関係のない型と空のリスト(この場合はlist()
)を生成します。
C++では関数が2つの異なる型を返すことができないため、where
の単純な実装はコンパイルに失敗します。
_auto where_broken = [](auto func) {
return flatmap([func](auto i) {
return func(i)? list(i) : list(); // broken :-(
});
};
_
上記の実装では、funcはブール値を返します。これは、各要素に対してtrueまたはfalseを示す述語です。 ?:演算子はコンパイルされません。
したがって、別のトリックを使用して、コレクションパイプラインを継続できます。実際に要素をフィルタリングするのではなく、それらは単にそのようにフラグが付けられます---それがそれを不愉快なものにしています.
_auto where_unpleasant = [](auto func) {
return [=](auto... i) {
return list(std::make_pair(func(i), i)...);
};
};
_
_where_unpleasant
_は仕事を完了させますが、不愉快なことに...
たとえば、これは負の要素をフィルタリングする方法です。
_auto positive = [](auto i) { return i >= 0; };
auto pair_print = [](auto pair) {
if(pair.first)
std::cout << pair.second << " ";
return pair;
};
list(10, 20)
(flatmap(pair))
(where_unpleasant(positive))
(fmap(pair_print)); // prints 10 and 20 in some order
_
----異種のタプル----
これまでのところ、同質のタプルについての議論でした。それを真のタプルに一般化しましょう。ただし、fmap
、flatmap
、where
はコールバックラムダを1つだけ受け取ります。それぞれが1つのタイプで機能する複数のラムダを提供するために、それらをオーバーロードできます。例えば、
_template <class A, class... B>
struct overload : overload<A>, overload<B...> {
overload(A a, B... b)
: overload<A>(a), overload<B...>(b...)
{}
using overload<A>::operator ();
using overload<B...>::operator ();
};
template <class A>
struct overload<A> : A{
overload(A a)
: A(a) {}
using A::operator();
};
template <class... F>
auto make_overload(F... f) {
return overload<F...>(f...);
}
auto test =
make_overload([](int i) { std::cout << "int = " << i << std::endl; },
[](double d) { std::cout << "double = " << d << std::endl; });
test(10); // int
test(9.99); // double
_
オーバーロードされたラムダ手法を使用して、異種のタプル連続体を処理してみましょう。
_auto int_or_string =
make_overload([](int i) { return 5*i; },
[](std::string s) { return s+s; });
list(10, "20")
(fmap(int_or_string))
(fmap(print)); // prints 2020 and 50 in some order
_
最後に、実例
これは 継続渡しスタイル の形式に似ています。
CPSの大まかな考え方は次のとおりです。関数(たとえばf
)が値を返すのではなく、f
に関数である別の引数継続。次に、f
は、戻るのではなく、戻り値でこの継続を呼び出します。例を見てみましょう:
_int f (int x) { return x + 42; }
_
なる
_void f (int x, auto cont) { cont (x + 42); }
_
呼び出しは末尾呼び出しであり、ジャンプに最適化できます(これは、TCOが、Schemeなどの一部の言語で必須であり、そのセマンティクスは、CPSへの何らかの変換に依存しているためです)。
もう一つの例:
_void get_int (auto cont) { cont (10); }
void print_int (int x) { printf ("%d", x), }
_
これでget_int (std::bind (f, _1, print_int))
を実行して54を出力できます。すべての継続呼び出しはalways末尾呼び出し(printf
も継続呼び出しです)。
よく知られている例は、非同期コールバック(たとえば、JavaScriptでのAJAX呼び出し)です。並列処理を実行するルーチンに継続を渡します。
上記の例のように、継続を構成できます(そして、興味がある場合は モナドを形成 )。実際 可能 (機能的な)プログラムを完全にCPSに変換し、すべての呼び出しが末尾呼び出しになるようにします(プログラムを実行するためにスタックは必要ありません!)。