C++ 中的 Monad 接口
Monad interface in C++
我目前正在学习一些东西 haskell 并开始了解 monad 的工作原理。
由于我通常编写 C++ 代码,并且我认为 monad 模式(就我现在的理解而言)在 C++ 中使用也非常棒,例如用于期货等,
我想知道是否有一种方法可以实现接口或基类 class,以强制正确重载函数 bind
和 return
(因为另一个名称比 C++ 的 return) 对于派生类型?
为了更清楚地说明我在想什么:
考虑我们有以下非成员函数:
auto foo(const int x) const -> std::string;
还有一个成员函数bar
,它对不同的class有不同的重载:
auto bar() const -> const *Monad<int>;
如果我们现在想做这样的事情:foo(someMember.bar())
,
这根本行不通。因此,如果必须知道 return 是什么柱,例如如果它 return 是 future<int>
,我们必须调用 bar().get()
,哪些块,即使我们不需要在此屏蔽。
在haskell中我们可以做类似bar >>= foo
的事情
所以我问自己是否可以在 C++ 中实现这样的行为,因为在调用 foo(x)
时我们不关心 x 是否是一个包含 int
的对象,以及在什么样的class int
是装箱的,我们只想在装箱类型上应用函数 foo
。
很抱歉我在用英语表达我的想法时遇到了一些问题,因为我不是母语人士。
我担心 Haskell 风格的多态性和 C++ 模板对于在 C++ 中以实际可用的方式实用地定义 monad 来说太过分了。
从技术上讲,您可以将 monad M
定义为以下形式的模板 class(为了简单起见,我将按值传递所有内容)
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++ 中是否可用。这肯定会导致非惯用代码。
那么,你的问题是如何要求一个class有这样的接口。你可以使用像
这样的东西
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
。
首先请注意,作为 monad 不是 属性 类型,而是类型构造函数。
例如在 Haskell 中,您将 List a
作为类型,将 List
作为类型构造函数。在 C++ 中,我们具有与模板相同的功能:std::list
是一个可以构造类型 std::list<int>
的类型构造函数。这里 List
是一个单子,但 List Bool
不是。
为了使类型构造函数 M
成为单子的,它需要提供两个特殊函数:
- 将某种类型
T
的任意值提升到 monad 的函数,即 T -> M<T>
类型的函数。此函数在 Haskell. 中称为 return
M<T> ->(T -> M<T'>) -> M<T'>
类型的函数(在 Haskell 中称为 bind
),即采用 M<T>
类型对象和 M<T>
类型函数的函数=30=] 并将参数函数应用于包裹在参数 M<T>
. 中的 T
对象
还有一些属性是这两个函数必须完成的,但是由于语义属性不能在编译时检查(在Haskell和C++中都没有),我们真的不需要关心他们在这里。
我们可以检查这两个函数的存在和类型,一旦我们决定为它们设置syntax/names。
对于第一个,明显的选择是一个构造函数,它只接受任何给定类型 T
的一个元素。对于第二个,我决定使用 operator>>=
因为我希望它成为一个运算符以避免嵌套函数调用,它类似于 Haskell 符号(但不幸的是它是右关联的 -哦好吧)。
正在检查 monadic 接口
那么如何检查模板的属性呢?幸运的是,C++ 中有模板-模板参数和 SFINAE。
首先,我们需要一种方法来确定是否确实存在采用任意类型的构造函数。我们可以通过检查给定类型构造函数 M
的类型 M<DummyType>
对于我们定义的虚拟类型 struct DummyType{};
是否格式正确来近似。这样我们就可以确保我们正在检查的类型不会有专门化。
对于bind
我们做同样的事情:检查是否有operator>>=(M<DummyType> const&, M<DummyType2>(*)(DummyType))
并且返回的类型实际上是M<DummyType2>
.
可以使用 C++17 来检查函数是否存在 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
= 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。
使用类型特征实现基于monadic接口的泛型函数
monad 的最大优势在于仅使用 monadic 接口就可以做很多事情。例如,我们知道每个 monad 也是一个 applicative,所以我们可以编写 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 的 monad 的大部分优点都无法使用,因为它们严重依赖柯里化。例如。在此实现中,您只能将函数映射到只接受一个参数的 monad 上。在我的 github 上你可以找到一个也支持柯里化的版本,但语法更糟糕。
如果有兴趣,这里有一个 coliru。
编辑:我刚刚注意到我在提供 std::vector<int>
类型的参数时编译器无法推断出 Monad = std::vector
和 T = 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>>;
};
这允许我们做的另一件巧妙的事情是清理上面通用 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)}; });
}
我认为这种 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;
}
我目前正在学习一些东西 haskell 并开始了解 monad 的工作原理。 由于我通常编写 C++ 代码,并且我认为 monad 模式(就我现在的理解而言)在 C++ 中使用也非常棒,例如用于期货等,
我想知道是否有一种方法可以实现接口或基类 class,以强制正确重载函数 bind
和 return
(因为另一个名称比 C++ 的 return) 对于派生类型?
为了更清楚地说明我在想什么:
考虑我们有以下非成员函数:
auto foo(const int x) const -> std::string;
还有一个成员函数bar
,它对不同的class有不同的重载:
auto bar() const -> const *Monad<int>;
如果我们现在想做这样的事情:foo(someMember.bar())
,
这根本行不通。因此,如果必须知道 return 是什么柱,例如如果它 return 是 future<int>
,我们必须调用 bar().get()
,哪些块,即使我们不需要在此屏蔽。
在haskell中我们可以做类似bar >>= foo
所以我问自己是否可以在 C++ 中实现这样的行为,因为在调用 foo(x)
时我们不关心 x 是否是一个包含 int
的对象,以及在什么样的class int
是装箱的,我们只想在装箱类型上应用函数 foo
。
很抱歉我在用英语表达我的想法时遇到了一些问题,因为我不是母语人士。
我担心 Haskell 风格的多态性和 C++ 模板对于在 C++ 中以实际可用的方式实用地定义 monad 来说太过分了。
从技术上讲,您可以将 monad M
定义为以下形式的模板 class(为了简单起见,我将按值传递所有内容)
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++ 中是否可用。这肯定会导致非惯用代码。
那么,你的问题是如何要求一个class有这样的接口。你可以使用像
这样的东西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
。
首先请注意,作为 monad 不是 属性 类型,而是类型构造函数。
例如在 Haskell 中,您将 List a
作为类型,将 List
作为类型构造函数。在 C++ 中,我们具有与模板相同的功能:std::list
是一个可以构造类型 std::list<int>
的类型构造函数。这里 List
是一个单子,但 List Bool
不是。
为了使类型构造函数 M
成为单子的,它需要提供两个特殊函数:
- 将某种类型
T
的任意值提升到 monad 的函数,即T -> M<T>
类型的函数。此函数在 Haskell. 中称为 M<T> ->(T -> M<T'>) -> M<T'>
类型的函数(在 Haskell 中称为bind
),即采用M<T>
类型对象和M<T>
类型函数的函数=30=] 并将参数函数应用于包裹在参数M<T>
. 中的
return
T
对象
还有一些属性是这两个函数必须完成的,但是由于语义属性不能在编译时检查(在Haskell和C++中都没有),我们真的不需要关心他们在这里。
我们可以检查这两个函数的存在和类型,一旦我们决定为它们设置syntax/names。
对于第一个,明显的选择是一个构造函数,它只接受任何给定类型 T
的一个元素。对于第二个,我决定使用 operator>>=
因为我希望它成为一个运算符以避免嵌套函数调用,它类似于 Haskell 符号(但不幸的是它是右关联的 -哦好吧)。
正在检查 monadic 接口
那么如何检查模板的属性呢?幸运的是,C++ 中有模板-模板参数和 SFINAE。
首先,我们需要一种方法来确定是否确实存在采用任意类型的构造函数。我们可以通过检查给定类型构造函数 M
的类型 M<DummyType>
对于我们定义的虚拟类型 struct DummyType{};
是否格式正确来近似。这样我们就可以确保我们正在检查的类型不会有专门化。
对于bind
我们做同样的事情:检查是否有operator>>=(M<DummyType> const&, M<DummyType2>(*)(DummyType))
并且返回的类型实际上是M<DummyType2>
.
可以使用 C++17 来检查函数是否存在 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
= 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。
使用类型特征实现基于monadic接口的泛型函数
monad 的最大优势在于仅使用 monadic 接口就可以做很多事情。例如,我们知道每个 monad 也是一个 applicative,所以我们可以编写 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 的 monad 的大部分优点都无法使用,因为它们严重依赖柯里化。例如。在此实现中,您只能将函数映射到只接受一个参数的 monad 上。在我的 github 上你可以找到一个也支持柯里化的版本,但语法更糟糕。
如果有兴趣,这里有一个 coliru。
编辑:我刚刚注意到我在提供 std::vector<int>
类型的参数时编译器无法推断出 Monad = std::vector
和 T = 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>>;
};
这允许我们做的另一件巧妙的事情是清理上面通用 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)}; });
}
我认为这种 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;
}