haskell — выражает общий монадический интерфейс (например, класс 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 ++, такие как вывод типа. Это просто для удовольствия и для объяснения монад нехакеллерам и нематематикам.

7

Решение

Система типов C ++ не является достаточно мощной, чтобы абстрагироваться над типами с более высоким родом, но поскольку шаблоны типизированы по типу утки, вы можете игнорировать это и просто отдельно реализовывать различные монады, а затем выражать монадические операции как шаблоны 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_type или же MonadicValue (внутри bind) являются монадами, но являются типами для некоторых монадических значений всеобъемлющей монады — just_type<int> а также nothing_type все вместе являются монадой (вроде — я сейчас откладываю вопрос рода, но имейте в виду, что, например, можно перепривязать специализации шаблона, например, для std::allocator<T>!). В качестве таких bind должно быть несколько снисходительно к тому, что он принимает, но обратите внимание, что это не значит, что он должен принимать все.

Конечно, вполне возможно иметь шаблон класса M такой, что M<T> это модель MonadicValue а также bind(m, f) только когда-либо имеет тип M<U> где m имеет тип M<T>, Это в некотором смысле сделало бы M Монада (с добрым * -> *), и код все равно будет работать. (И говоря о Maybe, возможно, адаптируясь boost::optional<T> иметь монадический интерфейс было бы хорошим упражнением.)

Проницательный читатель заметил бы, что у меня нет эквивалента return здесь все сделано с just а также make_just заводы, которые являются аналогами Just конструктор. Это должно быть кратким ответом — возможное решение было бы написать pure это делает работу returnи это возвращает значение, которое неявно преобразуется в любой тип, который моделирует MonadicValue (откладывая, например, некоторые MonadicValue::pure).

Тем не менее, существуют конструктивные соображения, заключающиеся в том, что ограниченный вывод типа C ++ означает, что bind(pure(4), [](int) { return pure(5); }) не будет работать из коробки. Это, однако, не является непреодолимой проблемой. (Некоторые контуры решения должны быть перегружены bind, но это неудобно, если мы добавим к интерфейсу нашего MonadicValue концепция, поскольку любая новая операция также должна иметь возможность явно иметь дело с чистыми значениями; или сделать чистую ценность моделью MonadicValue также.)

3

Другие решения

Я бы сделал это так:

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

По вопросам рекламы [email protected]