在C++中表达通用的monadic接口(如Monad类)

Express general monadic interface (like Monad class) in C++

本文关键字:接口 Monad monadic C++      更新时间:2023-10-16

甚至可以表达一种monad"C++吗? 我开始写这样的东西,但卡住了:

#include <iostream>
template <typename  a, typename b> struct M;
template <typename a, typename b> struct M {
virtual M<b>& operator>>( M<b>& (*fn)(M<a> &m, const a &x) ) = 0;
};
template <typename a, typename b> 
struct MSome : public M<a> {
virtual M<b>& operator>>( M<a>& (*fn)(M<a> &m, const a &x) ) {
return fn(*this, x);
}
private:
a x;
};
M<int, int>& wtf(M<int> &m, const int &v) {
std::cout << v << std::endl;
return m;
}
int main() {
//    MSome<int> v;
//    v >> wtf >> wtf;
return 0;
}

但面临缺乏多态性的问题。实际上,这可能是我的非当前C++因为我上次在8年前使用它。也许可以使用一些新的C++功能(如类型推断)来表达通用的 monadic 接口。这只是为了好玩,也是为了向非哈斯克勒和非数学家解释monads。

C++'类型系统不够强大,无法抽象出更高种类的类型,但由于模板是鸭式的,你可以忽略这一点,只是单独实现各种Monads,然后将monadic操作表示为SFINAE模板。丑陋,但

最好的。

这个评论是狠狠的。我一次又一次地看到人们试图使模板专业化成为"协变"和/或滥用继承。无论好坏,在我看来,面向概念的泛型编程*更理智。下面是一个快速演示,它将使用 C++11 功能以简洁明了,尽管应该可以在 C++03 中实现相同的功能:

(*:对于相互竞争的意见,请参阅我引用中的"丑陋,但最好的"!

#include <utility>
#include <type_traits>
// SFINAE utility
template<typename...> struct void_ { using type = void; };
template<typename... T> using Void = typename void_<T...>::type;
/*
* In an ideal world std::result_of would just work instead of all that.
* Consider this as a write-once (until std::result_of is fixed), use-many
* situation.
*/    
template<typename Sig, typename Sfinae = void> struct result_of {};
template<typename F, typename... Args>
struct result_of<
F(Args...)
, Void<decltype(std::declval<F>()(std::declval<Args>()...))>
> {
using type = decltype(std::declval<F>()(std::declval<Args>()...));
};
template<typename Sig> using ResultOf = typename result_of<Sig>::type;
/*
* Note how both template parameters have kind *, MonadicValue would be
* m a, not m. We don't whether MonadicValue is a specialization of some M<T>
* or not (or derived from a specialization of some M<T>). Note that it is
* possible to retrieve the a in m a via typename MonadicValue::value_type
* if MonadicValue is indeed a model of the proper concept.
*
* Defer actual implementation to the operator() of MonadicValue,
* which will do the monad-specific operation
*/
template<
typename MonadicValue
, typename F
/* It is possible to put a self-documenting assertion here
that will *not* SFINAE out but truly result in a hard error
unless some conditions are not satisfied -- I leave this out
for brevity
, Requires<
MonadicValueConcept<MonadicValue>
// The two following constraints ensure that
// F has signature a -> m b
, Callable<F, ValueType<MonadicValue>>
, MonadicValueConcept<ResultOf<F(ValueType<MonadicValue>)>>
>...
*/
>
ResultOf<MonadicValue(F)>
bind(MonadicValue&& value, F&& f)
{ return std::forward<MonadicValue>(value)(std::forward<F>(f)); }
// Picking Maybe as an example monad because it's easy
template<typename T>
struct just_type {
using value_type = T;
// Encapsulation omitted for brevity
value_type value;
template<typename F>
// The use of ResultOf means that we have a soft contraint
// here, but the commented Requires clause in bind happens
// before we would end up here
ResultOf<F(value_type)>
operator()(F&& f)
{ return std::forward<F>(f)(value); }
};
template<typename T>
just_type<T> just(T&& t)
{ return { std::forward<T>(t) }; }
template<typename T>
just_type<typename std::decay<T>::type> make_just(T&& t)
{ return { std::forward<T>(t) }; }
struct nothing_type {
// Note that because nothing_type and just_type<T>
// are part of the same concept we *must* put in
// a value_type member type -- whether you need
// a value member or not however is a design
// consideration with trade-offs
struct universal { template<typename T> operator T(); };
using value_type = universal;
template<typename F>
nothing_type operator()(F const&) const
{ return {}; }
};
constexpr nothing_type nothing;

然后可以写类似bind(bind(make_just(6), [](int i) { return i - 2; }), [](int i) { return just("Hello, World!"[i]); }).请注意,这篇文章中的代码不完整,因为包装后的值没有正确转发,一旦涉及const限定和仅移动类型,就会出现错误。您可以在此处查看代码(使用 GCC 4.7),尽管这可能是用词不当,因为它所做的只是不触发断言。(在 ideone 上使用相同的代码供将来的读者使用。

解决方案的核心是just_type<T>nothing_typeMonadicValue(bind内部)都不是monad,而是总体monad的某些monadic值的类型 -just_type<int>nothing_type一起是一个monad(有点 - 我现在把这个问题放在一边,但请记住,例如可以在事后重新绑定模板专业化, 喜欢std::allocator<T>!因此,bind在接受的内容上必须宽容一些,但请注意,这并不意味着它必须接受一切。

当然,完全有可能有一个类模板M这样M<T>MonadicValue的模型,并且bind(m, f)只有类型M<U>,而m具有类型M<T>。从某种意义上说,这将使 monadM(带有* -> *),并且代码仍然可以工作。(说到Maybe,也许调整boost::optional<T>有一个一元界面将是一个很好的练习。

精明的读者会注意到,我在这里没有等价的return,一切都是通过justmake_just工厂完成的,这些工厂是Just构造函数的对应物。这是为了使答案简短 - 一个可能的解决方案是编写一个pure来完成return的工作,并返回一个可隐式转换为建模MonadicValue的任何类型的值(例如通过推迟到某些MonadicValue::pure)。

不过,有一些设计方面的考虑,因为C++的有限类型推导意味着bind(pure(4), [](int) { return pure(5); })不会开箱即用。然而,这并不是一个无法克服的问题。(解决方案的一些概述是重载bind,但是如果我们添加到MonadicValue概念的界面中,这将不方便,因为任何新操作也必须能够显式处理纯值;或者使纯值也成为MonadicValue模型。

我会这样做:

template<class T>
class IO {
public:
virtual T get() const=0;
};
template<class T, class K>
class C : public IO<K> {
public:
C(IO<T> &io1, IO<K> &io2) : io1(io1), io2(io2) { }
K get() const { 
io1.get();
return io2.get();
}
private:
IO<T> &io1;
IO<K> &io2;
};
int main() {
IO<float> *io = new YYYY;
IO<int> *io2 = new XXX;
C<float,int> c(*io, *io2);
return c.get();
}