C++中的Monad接口



我目前正在学习一点haskell,并开始了解monad是如何工作的。由于我通常编码C++,我认为monad模式(正如我现在所理解的那样)在C++中使用也非常棒,例如用于期货等,

我想知道是否有一种方法可以实现接口或基类,从而为派生类型强制正确重载函数bindreturn(原因是使用了除C++返回之外的其他名称)?

为了更清楚地说明我的想法:

考虑我们有以下非成员函数:

auto foo(const int x) const -> std::string;

和一个成员函数bar,它对不同的类有不同的重载:

auto bar() const -> const *Monad<int>;

如果我们现在想做这样的事情:CCD_ 4,这根本不起作用。因此,如果必须知道bar返回了什么,例如,如果它返回了future<int>,我们必须调用bar().get(),它会阻塞,即使我们不需要在这里阻塞。

在haskell中,我们可以做一些类似bar >>= foo的事情

所以我在问自己,我们是否可以在C++中实现这样的行为,因为当调用foo(x)时,我们不在乎x是否是一个装箱int的对象,以及int在什么样的类中装箱,我们只想在装箱类型上应用函数foo

很抱歉,我在用英语表达我的想法时遇到了一些问题,因为我不是母语为英语的人。

首先要注意,monad不是类型的属性,而是类型构造函数的属性。

例如,在Haskell中,List a作为类型,List作为类型构造函数。在C++中,我们对模板具有相同的功能:std::list是一个类型构造函数,可以构造类型std::list<int>。这里List是monad,但List Bool不是。

为了使类型构造函数M是一元的,它需要提供两个特殊的函数:

  1. 一个函数,将某些类型的T的任意值提升到monad,即T -> M<T>类型的函数。这个函数在Haskell中被称为return
  2. M<T> ->(T -> M<T'>) -> M<T'>类型的函数(在Haskell中称为bind),即接受M<T>类型的对象和T -> M<T'>类型的函数,并将变元函数应用于封装在变元M<T>中的T对象的函数

这两个函数还必须实现一些属性,但由于语义属性不能在编译时检查(无论是在Haskell中还是在C++中),我们在这里都不需要关心它们。

然而,一旦我们决定了这两个函数的语法/名称,我们就可以检查它们的存在性和类型。对于第一个,显而易见的选择是一个构造函数,它只接受任何给定类型T的一个元素。对于第二个,我决定使用operator>>=,因为我希望它是一个运算符,以避免嵌套的函数调用,而且它类似于Haskell表示法(但不幸的是,它是正确的关联-哦,好吧)。

检查一元接口

那么如何检查模板的属性呢?幸运的是,C++中有模板-模板参数和SFINAE。

首先,我们需要一种方法来确定是否真的有一个采用任意类型的构造函数。我们可以通过检查给定类型构造函数M的类型M<DummyType>是我们定义的伪类型struct DummyType{};的良好形式来近似这一点。通过这种方式,我们可以确保我们检查的类型不会有专门化。

对于bind,我们做同样的事情:检查是否有operator>>=(M<DummyType> const&, M<DummyType2>(*)(DummyType)),并且返回的类型实际上是M<DummyType2>

可以使用C++17sstd::void_t来检查函数是否存在(我强烈建议Walter Browns在2014年CppCon上介绍该技术)。可以使用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成为上述概念意义上的monad。

使用类型特性实现基于一元接口的泛型函数

单元的最大优点是,只有一个单元接口可以做很多事情。例如,我们知道每个monad也是一个可应用的,所以我们可以编写Haskell的ap函数,并用它来实现foo(someMember.bar())0,它允许将任何普通函数应用于monadic值。

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

让我们看看如何使用它,假设您已经为std::vectoroptional实现了operator>>=

// 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
}

限制

虽然这允许用一种通用的方式来定义monad的概念,并允许直接实现monadic类型构造函数,但也有一些缺点。

首先也是最重要的一点,我不知道有一种方法可以让编译器推断出哪个类型构造函数被用来创建模板化类型,也就是说,据我所知,没有一种方法必须编译器计算出std::vector模板被用来创建类型std::vector<int>。因此,您必须手动将调用中类型构造函数的名称添加到例如fmap的实现中

其次,编写在泛型monad上工作的函数是非常难看的,正如您在apliftM中看到的那样。另一方面,这些只能写一次。最重要的是,一旦我们获得概念(希望在C++2x中),整个方法将变得更容易编写和使用。

最后但同样重要的是,在我写在这里的形式中,Haskell的monad的大多数优点都是不可用的,因为它们严重依赖于currying。例如,在这个实现中,您只能将函数映射到只接受一个参数的monad上。在我的github上,你可以找到一个同样支持currying的版本,但语法更糟糕。

对于感兴趣的人,这里有一个coliru。

编辑:我刚刚注意到,当提供类型为std::vector<int>的参数时,编译器无法推导出Monad = std::vectorT = int,这一事实我错了。这意味着,您确实可以使用fmap在任意容器上映射函数的统一语法,即

auto v3 = fmap(square<>{}, v2);
auto o3 = fmap(square<>{}, o2);

编译并做正确的事情。

我把这个例子添加到coliru中。

编辑:使用概念

由于C++20的概念就在眼前,而且语法几乎是最终的,因此用使用概念的等效代码更新此回复是有意义的。

要使用概念来实现这一点,最简单的方法就是编写一个概念来包装is_mad-type特性。

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

这允许我们做的另一件整洁的事情是清理上面通用monad函数的签名,比如:

// 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++中实际定义monad,因为它实际上是可用的。

从技术上讲,您可以将monadM定义为以下形式的模板类(为了保持简单,我将按值传递所有内容)

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++中可用。这肯定会导致非惯用代码。

然后,您的问题是如何要求类具有这样的接口。你可以使用类似的东西

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

但是,从M类型中删除map不会触发类型错误。事实上,错误只会在实例化时生成。模板不是foralls。

我认为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;
}

最新更新