web-dev-qa-db-ja.com

C ++のモナドインターフェース

私は現在少しハスケルを学んでいて、モナドがどのように機能するかを理解し始めました。私は通常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を適用したいだけです。

私はネイティブスピーカーではないので、英語で考えをまとめるのに問題があります。

14
Exagon

モナドであることは型のプロパティではなく、型コンストラクターのプロパティであることに最初に注意してください。

例えば。 Haskellでは、型として_List a_があり、型コンストラクターとしてListがあります。 C++では、テンプレートと同じ機能があります。_std::list_は、型_std::list<int>_を作成できる型コンストラクターです。ここで、Listはモナドですが、_List Bool_はそうではありません。

型コンストラクターMをモナドにするためには、2つの特別な関数を提供する必要があります。

  1. あるタイプTの任意の値をモナドに持ち上げる関数、つまりタイプ_T -> M<T>_の関数。この関数はHaskellではreturnと呼ばれています。
  2. 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

第二に、apliftMでわかるように、ジェネリックモナドで動作する関数を書くのはかなり醜いです。一方、これらは一度だけ書く必要があります。それに加えて、概念を理解すると(できれば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)}; });
}
_
23
Corristo

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));
  }
};

ただし、たとえばmapM型から削除しても、型エラーは発生しません。実際、エラーはインスタンス化時にのみ生成されます。繰り返しになりますが、テンプレートはforallsではありません。

6
chi

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;
}
1
schorsch_76