Monad界面在C ++中
我现在正在学习一点Haskell,并开始弄清楚monad是如何工作的。 由于我正常编写C ++代码,因此我认为monad模式(就像我现在所理解的那样)是非常棒的,可以在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
事情
于是我问自己,如果我们能在C ++中实现这样的行为++,因为打电话时foo(x)
我们不关心,如果x是一个对象,该对象框的int
,以及在什么样的类的int
是盒装,我们只是想在盒装类型上应用函数foo
。
对不起,我有一些问题用英文形成我的想法,因为我不是母语的人。
首先请注意,作为monad不是类型的属性,而是类型构造函数。
例如在Haskell中,您将List a
列为类型,将List
列为类型构造函数。 在C ++中,我们对模板具有相同的功能: std::list
是一个可以构造std::list<int>
类型的类型构造函数。 这里List
是monad,但List Bool
不是。
为了使类型构造函数M
是一元的,它需要提供两个特殊的函数:
T
任意值提升到monad的函数,即类型T -> M<T>
的函数。 这个函数在Haskell中被称为return
。 bind
)类型为M<T> ->(T -> M<T'>) -> M<T'>
,即一个函数,它接受一个M<T>
类型的对象和一个T -> M<T'>
函数,并将参数函数应用于包含在参数M<T>
内的T
对象。 这两个函数还有一些属性需要完成,但是由于语义属性在编译时无法检查(无论是在Haskell还是C ++中),我们并不需要关心它们。
然而,我们可以检查的是,一旦我们确定了它们的语法/名称,这两个函数的存在和类型。 对于第一个,显而易见的选择是一个构造函数,该构造函数只接受任何给定类型T
一个元素。 对于第二个,我决定去operator>>=
因为我希望它是一个运算符,以避免嵌套的函数调用,它与Haskell符号类似(但不幸的是它是右联合的 - 哦)。
检查monadic接口
那么如何检查模板的属性? 幸运的是在C ++中有模板模板参数和SFINAE。
首先,我们需要一种方法来确定实际上是否有构造函数采用任意类型。 我们可以通过检查一个给定类型构造器M
的类型M<DummyType>
是否适合虚拟类型struct DummyType{};
我们定义。 这样我们就可以确保我们正在检查的类型不存在专门化。
对于bind
我们做同样的事情:检查是否有一个operator>>=(M<DummyType> const&, M<DummyType2>(*)(DummyType))
,返回的类型实际上是M<DummyType2>
。
检查一个函数是否存在可以使用C ++ 17s std::void_t
来完成(我强烈建议Walter Browns在CppCon 2014上介绍他的技术)。 检查类型是否正确可以用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
= declval(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。
使用类型特征来实现基于monadic接口的泛型函数
Monad的巨大优势在于,只有单一接口才能做很多事情。 例如,我们知道每个monad也是一个应用程序,所以我们可以编写Haskell的ap
函数并使用它来实现liftM
,它允许将任何普通函数应用于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::vector
和optional
实现了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的函数是非常难看的,正如你可以用ap
和liftM
看到的那样。 另一方面,这些必须只写一次。 除此之外,一旦我们获得了概念(希望用C ++ 2x),整个方法将变得更容易编写和使用。
最后但并非最不重要的一点,以我在这里写下的形式,Haskell单子的大多数优点都不可用,因为它们严重依赖于currying。 例如,在这个实现中,你只能映射monads上的函数,它们只需要一个参数。 在我的github上,你可以找到一个版本,也有咖啡支持,但语法更糟糕。
对于感兴趣的,这里是一个coliru。
编辑:我只注意到,我错误的事实是,编译器不能推导出Monad = std::vector
和T = int
时,提供了一个类型为std::vector<int>
。 这意味着你真的可以有一个统一的语法来映射一个函数通过任意容器与fmap
,即
auto v3 = fmap(square<>{}, v2);
auto o3 = fmap(square<>{}, o2);
编译并做正确的事情。
我将这个例子添加到了coliru中。
我担心Haskell风格的多态和C ++模板实际上可用的方式实际上很难在C ++中实际定义monad。
从技术上讲,你可能将monad 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 ++。 肯定会导致非惯用代码。
那么,你的问题是关于如何要求一个类有这样的接口。 你可以使用类似的东西
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
不会触发类型错误。 事实上,错误只会在实例化时产生。 模板是不是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;
}
链接地址: http://www.djcxy.com/p/47685.html