私は現在少しハスケルを学んでいて、モナドがどのように機能するかを理解し始めました。私は通常C++をコーディングしていて、モナドパターンは(今理解しているように)C++でも、たとえば先物などに使用すると本当に素晴らしいと思います。
関数bind
およびreturn
(C++の戻り値以外の名前の原因)の正しいオーバーロードを強制するためのインターフェイスまたは基本クラスを実装する方法があるのだろうか?派生型?
私が考えていることをより明確にするために:
次の非メンバー関数があると考えてください。
_auto foo(const int x) const -> std::string;
_
そして、メンバー関数bar
は、クラスごとに異なるオーバーロードがあります。
_auto bar() const -> const *Monad<int>;
_
foo(someMember.bar())
のようなことをしたい場合、これは単に機能しません。したがって、どのバーが返されるかを知る必要がある場合、たとえば_future<int>
_を返す場合は、ここでブロックする必要がない場合でも、ブロックするbar().get()
を呼び出す必要があります。
Haskellでは_bar >>= foo
_のようなことをすることができます
foo(x)
を呼び出すときに、xがint
をボックス化するオブジェクトであるかどうか、およびどのようなクラスであるかは気にしないため、C++でこのような動作を実現できるかどうかを自問しています。 int
はボックス化されているので、ボックス化されたタイプに関数foo
を適用したいだけです。
私はネイティブスピーカーではないので、英語で考えをまとめるのに問題があります。
モナドであることは型のプロパティではなく、型コンストラクターのプロパティであることに最初に注意してください。
例えば。 Haskellでは、型として_List a
_があり、型コンストラクターとしてList
があります。 C++では、テンプレートと同じ機能があります。_std::list
_は、型_std::list<int>
_を作成できる型コンストラクターです。ここで、List
はモナドですが、_List Bool
_はそうではありません。
型コンストラクターM
をモナドにするためには、2つの特別な関数を提供する必要があります。
T
の任意の値をモナドに持ち上げる関数、つまりタイプ_T -> M<T>
_の関数。この関数はHaskellではreturn
と呼ばれています。M<T> ->(T -> M<T'>) -> M<T'>
型の関数(Haskellではbind
と呼ばれる)、つまり_M<T>
_型のオブジェクトと_T -> M<T'>
_型の関数を取り、適用する関数引数_M<T>
_内にラップされたT
オブジェクトへの引数関数。これら2つの関数が満たさなければならないプロパティもいくつかありますが、セマンティックプロパティはコンパイル時にチェックできないため(HaskellでもC++でも)、ここでは実際に気にする必要はありません。
ただし、canで確認できるのは、これら2つの関数の構文/名前を決定した後、それらの存在とタイプです。最初のものの場合、明らかな選択は、任意のタイプT
の要素を1つだけ取るコンストラクターです。 2つ目は、ネストされた関数呼び出しを回避するために演算子にしたいので、_operator>>=
_を使用することにしました。これは、Haskell表記に似ています(ただし、残念ながら、右結合です-まあ)。
では、テンプレートのプロパティをどのようにチェックするのでしょうか。幸い、C++にはテンプレートとテンプレートの引数とSFINAEがあります。
まず、任意の型をとるコンストラクターが実際に存在するかどうかを把握する方法が必要です。与えられた型コンストラクターM
について、型_M<DummyType>
_が、定義したダミー型_struct DummyType{};
_に対して整形式であることを確認することで概算できます。このようにして、チェック対象のタイプに特殊化がないことを確認できます。
bind
についても、同じことを行います。operator>>=(M<DummyType> const&, M<DummyType2>(*)(DummyType))
があり、返される型が実際には_M<DummyType2>
_であることを確認します。
関数が存在することの確認は、C++ 17s _std::void_t
_を使用して実行できます(WalterBrownsがCppCon2014でテクニックを紹介することを強くお勧めします)。タイプが正しいことを確認するには、std :: is_sameを使用します。
全体として、これは次のようになります。
_// declare the two dummy types we need for detecting constructor and bind
struct DummyType{};
struct DummyType2{};
// returns the return type of the constructor call with a single
// object of type T if such a constructor exists and nothing
// otherwise. Here `Monad` is a fixed type constructor.
template <template<typename, typename...> class Monad, typename T>
using constructor_return_t
= decltype(Monad<T>{std::declval<T>()});
// returns the return type of operator>>=(const Monad<T>&, Monad<T'>(*)(T))
// if such an operator is defined and nothing otherwise. Here Monad
// is a fixed type constructor and T and funcType are arbitrary types.
template <template <typename, typename...> class Monad, typename T, typename T'>
using monadic_bind_t
= decltype(std::declval<Monad<T> const&>() >>= std::declval<Monad<T'>(*)(T)>());
// logical 'and' for std::true_type and it's children
template <typename, typename, typename = void>
struct type_and : std::false_type{};
template<typename T, typename T2>
struct type_and<T, T2, std::enable_if_t<std::is_base_of<std::true_type, T>::value && std::is_base_of<std::true_type, T2>::value>>
: std::true_type{};
// the actual check that our type constructor indeed satisfies our concept
template <template <typename, typename...> class, typename = void>
struct is_monad : std::false_type {};
template <template <typename, typename...> class Monad>
struct is_monad<Monad,
void_t<constructor_return_t<Monad, DummyType>,
monadic_bind_t<Monad, DummyType, DummyType2>>>
: type_and<std::is_same<monadic_bind_t<Monad, DummyType, DummyType2>,
Monad<DummyType2>>,
std::is_same<constructor_return_t<Monad, DummyType>,
Monad<DummyType>>> {};
_
通常、型コンストラクターは単一の型T
を引数として取ると予想されますが、STLコンテナーで通常使用されるデフォルトのアロケーターを説明するために可変個引数テンプレートテンプレートパラメーターを使用していることに注意してください。それがなければ、上記で定義した概念の意味で_std::vector
_をモナドにすることはできません。
モナドの大きな利点は、モナドインターフェイスだけでできることがたくさんあることです。たとえば、すべてのモナドも適用可能であることがわかっているので、Haskellのap
関数を記述し、それを使用して、通常の関数をモナド値に適用できるliftM
を実装できます。
_// ap
template <template <typename, typename...> class Monad, typename T, typename funcType>
auto ap(const Monad<funcType>& wrappedFn, const Monad<T>& x) {
static_assert(is_monad<Monad>{}(), "");
return wrappedFn >>= [x] (auto&& x1) { return x >>= [x1 = std::forward<decltype(x1)>(x1)] (auto&& x2) {
return Monad<decltype(std::declval<funcType>()(std::declval<T>()))> { x1 (std::forward<decltype(x2)>(x2)) }; }; };
}
// convenience function to lift arbitrary values into the monad, i.e.
// just a wrapper for the constructor that takes a single argument.
template <template <typename, typename...> class Monad, typename T>
Monad<std::remove_const_t<std::remove_reference_t<T>>> pure(T&& val) {
static_assert(is_monad<Monad>{}(), "");
return Monad<std::remove_const_t<std::remove_reference_t<T>>> { std::forward<decltype(val)>(val) };
}
// liftM
template <template <typename, typename...> class Monad, typename funcType>
auto liftM(funcType&& f) {
static_assert(is_monad<Monad>{}(), "");
return [_f = std::forward<decltype(f)>(f)] (auto x) {
return ap(pure<Monad>(_f), x);
};
}
// fmap
template <template <typename, typename...> class Monad, typename T, typename funcType>
auto fmap(funcType&& f, Monad<T> const& x) {
static_assert(is_monad<Monad>{}(), "");
return x >>= ( [_f = std::forward<funcType>(f)] (const T& val) {
return Monad<decltype(_f(std::declval<T>()))> {_f(val)}; });
}
_
_operator>>=
_とoptional
に_std::vector
_をすでに実装していると仮定して、どのように使用できるか見てみましょう。
_// functor similar to std::plus<>, etc.
template <typename T = void>
struct square {
auto operator()(T&& x) {
return x * std::forward<decltype(x)>(x);
}
};
template <>
struct square<void> {
template <typename T>
auto operator()(T&& x) const {
return x * std::forward<decltype(x)>(x);
}
};
int main(int, char**) {
auto vector_empty = std::vector<double>{};
auto vector_with_values = std::vector<int>{2, 3, 31};
auto optional_with_value = optional<double>{42.0};
auto optional_empty = optional<int>{};
auto v1 = liftM<std::vector>(square<>{})(vector_empty); // still an empty vector
auto v2 = liftM<std::vector>(square<>{})(vector_with_values); // == vector<int>{4, 9, 961};
auto o1 = liftM<optional>(square<>{})(optional_empty); // still an empty optional
auto o2 = liftM<optional>(square<>{})(optional_with_value); // == optional<int>{1764.0};
std::cout << std::boolalpha << is_monad<std::vector>::value << std::endl; // prints true
std::cout << std::boolalpha << is_monad<std::list>::value << std::endl; // prints false
}
_
これにより、モナドの概念を定義する一般的な方法が可能になり、モナド型コンストラクターの簡単な実装が可能になりますが、いくつかの欠点があります。
何よりもまず、テンプレート化された型を作成するために使用された型コンストラクターをコンパイラーに推測させる方法があることを私は知りません。つまり、コンパイラーが_std::vector
_テンプレートは、タイプ_std::vector<int>
_の作成に使用されています。したがって、たとえばの実装への呼び出しで型コンストラクタの名前を手動で追加する必要があります。 fmap
。
第二に、ap
とliftM
でわかるように、ジェネリックモナドで動作する関数を書くのはかなり醜いです。一方、これらは一度だけ書く必要があります。それに加えて、概念を理解すると(できればC++ 2xで)、アプローチ全体の記述と使用が非常に簡単になります。
最後になりましたが、ここに書き留めた形式では、ハスケルのモナドの利点のほとんどは、カリー化に大きく依存しているため、使用できません。例えば。この実装では、引数を1つだけ取るモナドにのみ関数をマップできます。私の github には、カリー化もサポートされているバージョンがありますが、構文はさらに悪いです。
そして興味のある人のために、ここに colir があります。
編集:タイプ_Monad = std::vector
_の引数が指定された場合、コンパイラが_T = int
_および_std::vector<int>
_を推測できないという事実に関して私が間違っていることに気づきました。これは、fmap
を使用して、任意のコンテナに関数をマッピングするための統一された構文を実際に持つことができることを意味します。
_auto v3 = fmap(square<>{}, v2);
auto o3 = fmap(square<>{}, o2);
_
コンパイルして正しいことをします。
例をcoliruに追加しました。
C++ 20の概念はすぐそこにあり、構文はほぼ最終的なものであるため、概念を使用する同等のコードでこの応答を更新することは理にかなっています。
これを概念で機能させるためにできる最も簡単なことは、is_monad型の特性をラップする概念を書くことです。
_template<template<typename, typename...> typename T>
concept monad = is_monad<T>::value;
_
ただし、それ自体を概念として記述することもできるため、少し明確になります。
_template<template<typename, typename...> typename Monad>
concept monad = requires {
std::is_same_v<monadic_bind_t<Monad, DummyType, DummyType2>, Monad<DummyType2>>;
std::is_same_v<constructor_return_t<Monad, DummyType>, Monad<DummyType>>;
};
_
これにより、次のように、上記の一般的なモナド関数のシグネチャをクリーンアップすることができます。
_// fmap
template <monad Monad, typename T, typename funcType>
auto fmap(funcType&& f, Monad<T> const& x) {
return x >>= ( [_f = std::forward<funcType>(f)] (const T& val) {
return Monad<decltype(_f(std::declval<T>()))> {_f(val)}; });
}
_
HaskellスタイルのポリモーフィズムとC++テンプレートは、実際に使用できる方法でC++でモナドを実用的に定義するには遠すぎるのではないかと心配しています。
技術的には、モナドM
を次の形式のテンプレートクラスとして定義できます(単純にするために、すべてを値で渡します)。
template <typename A>
struct M {
// ...
// this provides return :: a -> M a
M(A a) { .... }
// this provides (>>=) :: M a -> (a -> M b) -> M b
template <typename B>
M<B> bind(std::function< M<B> (A) > f) { ... }
// this provides flip fmap :: M a -> (a -> b) -> M b
template <typename B>
M<B> map(std::function< B (A) > f) { ... }
};
これかもしれない動作します(私はC++の専門家ではありません)が、C++で使用できるかどうかはわかりません。確かにそれは非慣用的なコードにつながるでしょう。
次に、あなたの質問は、クラスがそのようなインターフェースを持っていることをrequireする方法についてです。あなたは次のようなものを使うことができます
template <typename A>
struct M : public Monad<M, A> {
...
};
どこ
template <template <typename T> M, typename A>
class Monad {
// this provides return :: a -> M a
Monad(A a) = 0;
// this provides (>>=) :: M a -> (a -> M b) -> M b
template <typename B>
M<B> bind(std::function< M<B> (A) > f) = 0;
// this provides flip fmap :: M a -> (a -> b) -> M b
template <typename B>
M<B> map(std::function< B (A) > f) = 0;
};
しかし悲しいかな、
monads.cpp:31:44: error: templates may not be ‘virtual’
M<B> bind(std::function< M<B> (A) > f) = 0;
テンプレートはポリモーフィック関数に似ていますが、異なるものです。
新しいアプローチ。これは機能しているように見えますが、機能しません。
template <template <typename T> typename M, typename A>
class Monad {
// this provides return :: a -> M a
Monad(A a) = 0;
// this provides (>>=) :: M a -> (a -> M b) -> M b
template <typename B>
M<B> bind(std::function< M<B> (A) > f);
// this provides flip fmap :: M a -> (a -> b) -> M b
template <typename B>
M<B> map(std::function< B (A) > f);
};
// The identity monad, as a basic case
template <typename A>
struct M : public Monad<M, A> {
A x;
// ...
// this provides return :: a -> M a
M(A a) : x(a) { }
// this provides (>>=) :: M a -> (a -> M b) -> M b
template <typename B>
M<B> bind(std::function< M<B> (A) > f) {
return f(x);
}
// this provides flip fmap :: M a -> (a -> b) -> M b
template <typename B>
M<B> map(std::function< B (A) > f) {
return M(f(x));
}
};
ただし、たとえばmap
をM
型から削除しても、型エラーは発生しません。実際、エラーはインスタンス化時にのみ生成されます。繰り返しになりますが、テンプレートはforall
sではありません。
C++でのこのスタイルのプログラミングの最も基本的な形式は、次のようなものだと思います。
#include <functional>
#include <cassert>
#include <boost/optional.hpp>
template<typename A>
struct Monad
{
public:
explicit Monad(boost::optional<A> a) : m(a) {}
inline bool valid() const { return static_cast<bool>(m); }
inline const A& data() const { assert(valid()); return *m; }
private:
const boost::optional<A> m;
};
Monad<double> Div(const Monad<double>& ma, const Monad<double>& mb)
{
if (!ma.valid() || !mb.valid() || mb.data() == 0.0)
{
return Monad<double>(boost::optional<double>{});
}
return Monad<double>(ma.data() / mb.data());
};
int main()
{
Monad<double> M1(3);
Monad<double> M2(2);
Monad<double> M0(0);
auto MR1 = Div(M1, M2);
if (MR1.valid())
std::cout << "3/2 = " << MR1.data() << '\n';
auto MR2 = Div(M1, M0);
if (MR2.valid())
std::cout << "3/0 = " << MR2.data() << '\n';
return 0;
}