私はこの慣用句をtuple-continuator、またはより一般的にはmonadic-continuatorと呼びます。これは間違いなく継続モナドのインスタンスです。C++ プログラマー向けの継続モナドの優れた紹介は、こちらです。本質的に、list
上記のラムダは値 (可変引数のパラメーター パック) を取り、単純な「継続子」 (内部クロージャー) を返します。このコンティニュエータは、callable ( と呼ばれるaccess
) が与えられると、パラメータ パックをそれに渡し、その callable が返すものを返します。
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;
};
は typeのSimpleContinuator
値を 1 つ受け取り、が呼び出されたときにA
それを に渡します。上記のラムダは本質的に同じです。より一般的です。単一の値の代わりに、内側のクロージャーはパラメーター パックをキャプチャし、それを関数に渡します。きちんとした!access
andThen
list
access
うまくいけば、それが継続者であることの意味を説明しています。しかし、モナドであるとはどういう意味ですか? 写真を使ったわかりやすい紹介です。
list
ラムダもリストモナドであり、継続モナドとして実装されていると思います。継続モナドはすべてのモナドの母であることに注意してください。つまり、継続モナドを持つ任意のモナドを実装できます。もちろん、リストモナドは手の届かないものではありません。
parameter-pack は非常に自然に「リスト」(多くの場合異種の型) であるため、リスト/シーケンス モナドのように機能することは理にかなっています。上記のlist
ラムダは、C++ パラメーター パックをモナド構造に変換する非常に興味深い方法です。したがって、操作を次々と連鎖させることができます。
ただし、上記のlength
ラムダはモナドを壊し、内部のネストされたラムダは単純に整数を返すため、少しがっかりします。以下に示すように、長さ「ゲッター」を記述するより良い方法が間違いなくあります。
- - ファンクタ - -
リスト lambda がモナドであると言う前に、それがファンクターであることを示さなければなりません。つまり、list には fmap を記述する必要があります。
上記のラムダ リストは、パラメーター パックからのファンクターの作成者として機能しますreturn
。その作成されたファンクターはパラメーターパックをそれ自体で保持し(キャプチャー)、可変数の引数を受け入れる呼び出し可能オブジェクトを指定すると、それに「アクセス」できます。callable は 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 は a のリストを 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
では、 (aka bind
, selectmany
)を書いてみましょう。
フラットマップのタイプはflatmap: (a -> list[b]) -> list[a] -> list[b].
つまり、a を b のリストと a のリストにマップする関数を指定すると、flatmap は b のリストを返します。本質的に、それは list-of-a から各要素を取得し、それに対して func を呼び出し、(空の可能性がある) list-of-b を 1 つずつ受け取り、すべての list-of-b を連結し、最終的なリストを返します。 -of-b。
リストの flatmap の実装を次に示します。
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++ パラメーター パックに適用できるようになりました。甘い!
----モナド法則----
モナドが 3 つのモナド法則list
をすべて満たしていることを確認しましょう。
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));
すべてのアサートが満たされます。
----収集パイプライン----
上記の 'list' ラムダは確かにモナドであり、ことわざの 'list-monad' の特徴を共有していますが、これは非常に不快です。特に、(aka )などの一般的なコレクション パイプラインコンビネータの動作は、一般的な期待を満たしていないためです。filter
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
、、、コールバック ラムダは 1 つしか取りませんflatmap
。where
それぞれが 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
最後に、実際の例