Кто-нибудь использует монадическое программирование в стиле связывания с ожидаемым <T>
(Прежде всего "bind" в вопросе не имеет ничего общего с std::bind
)
Я просмотрел Ожидаемый <T> говорить, и я подумал, что в презентации этой истории отсутствует основная идея этой вещи в Haskell.
Основная идея в Haskell заключается в том, что вы "никогда" не используете значение Expected<T>
. Вместо этого вы принимаете лямбда для Expected<T>
, который будет применяться или не зависит от состояния Expected<T>
.
Я бы ожидал, что этот комбинатор "bind" станет основным методом, который будет использоваться Expected<T>
, поэтому я должен спросить, был ли этот стиль программирования отклонен по какой-либо причине. Я назову этот комбинатор then
в следующем:
template <class T> class Expected<T> {
....
template <class V, class F> Expected<V> then(F fun_) {
if (!valid()) {
return Expected<V>::fromException(this(??)); // something like that
}
return fun_(get());
}
}
Точка этого комбинатора - это цепочка списка функций, в которых вам не нужно проверять наличие ошибок, а первая неудавшаяся функция может коротко завершить оценку.
auto res = Expected<Foo>::fromCode([]() { return callFun1(...); })
.then([](Baz& val) { return callFun2(..,val,..); })
.then([](Bar& val) { return callFun3(val,...); });
Или этот синтаксис, который начинает напоминать оператор >>=
, который используется в Haskell.
auto res = []() { return callFun1(...); }
>> [](Baz& val) { return callFun2(..,val,..); }
>> [](Bar& val) { return callFun3(val,...); };
callFun1
возвращает a Expected<Baz>
, callFun2
возвращает a Expected<Bar>
, а callFun3
возвращает a Expected<Foo>
.
Как вы можете видеть, этот код не проверяет наличие ошибок. Ошибки прекратят выполнение, но они все еще имеют все преимущества Expected<T>
. Это стандартный способ использования монады Either
в Haskell.
Как я уже сказал, наверняка кто-то, должно быть, посмотрел на это.
Изменить: я написал неправильные типы возврата для callFun {1..3}. Они возвращают Expected<T>
, а не T
для различных значений T
. Это своего рода целая точка комбинатора then
или >>
.
Ответы
Ответ 1
Передача нормальных функций функциональным шаблонам (например, ваш .then
) в С++, в отличие от Haskell, крайне расстраивает. Вы должны предоставить явную подпись типа для них, если они перегружены или шаблоны. Это уродливо и не поддается монадическим целям вычислений.
Кроме того, наши текущие lambdas мономорфны, вы должны явно вводить типы параметров, что делает эту ситуацию еще хуже.
Было много (библиотеки) пытается сделать функциональное программирование на С++ проще, но он всегда возвращается к тем двум точкам.
И последнее, но не менее важное: программирование функционального стиля на С++ не является нормой, и есть много людей, которым эта концепция совершенно чужда, а понятие "код возврата" легко понять.
(Обратите внимание, что ваш шаблон .then
template V
должен быть указан явно, но это относительно легко устранить.)
Ответ 2
Отвечая на мой собственный вопрос, чтобы дать дополнительную информацию и документировать свой эксперимент:
Я искалечил Expected<T>
. То, что я сделал, было переименовано get()
в thenReturn()
, чтобы препятствовать его использованию посредством именования. Я переименовал все это either<T>
.
И затем я добавил функцию then(...)
. Я не думаю, что результат такой плохой (за исключением, вероятно, большого количества ошибок), но я должен указать, что then
не является монадическим связыванием. Монадическое связывание является вариантом функциональной композиции, поэтому вы работаете с двумя функциями и возвращаете функцию. then
просто применяет функцию к either
, если это возможно.
Мы получаем
// Some template function we want to run.
// Notice that all our functions return either<T>, so it
// is "discouraged" to access the wrapped return value directly.
template <class T>
auto square(T num) -> either<T>
{
std::cout << "square\n";
return num*num;
}
// Some fixed-type function we want to run.
either<double> square2(int num)
{
return num*num;
}
// Example of a style of programming.
int doit()
{
using std::cout;
using std::string;
auto fun1 = [] (int x) -> either<int> { cout << "fun1\n"; throw "Some error"; };
auto fun2 = [] (int x) -> either<string> { cout << "fun2\n"; return string("string"); };
auto fun3 = [] (string x) -> either<int> { cout << "fun3\n"; return 53; };
int r = either<int>(1)
.then([] (int x) -> either<double> { return x + 1; })
.then([] (double x) -> either<int> { return x*x; })
.then(fun2) // here we transform to string and back to int.
.then(fun3)
.then(square<int>) // need explicit disambiguation
.then(square2)
.thenReturn();
auto r2 = either<int>(1)
.then(fun1) // exception thrown here
.then(fun2) // we can apply other functions,
.then(fun3); // but they will be ignored
try {
// when we access the value, it throws an exception.
cout << "returned : " << r2.thenReturn();
} catch (...) {
cout << "ouch, exception\n";
}
return r;
}
Вот полный пример:
#include <exception>
#include <functional>
#include <iostream>
#include <stdexcept>
#include <type_traits>
#include <typeinfo>
#include <utility>
template <class T> class either {
union {
T ham;
std::exception_ptr spam;
};
bool got_ham;
either() {}
// we're all friends here
template<typename> friend class either;
public:
typedef T HamType;
//either(const T& rhs) : ham(rhs), got_ham(true) {}
either(T&& rhs) : ham(std::move(rhs)), got_ham(true) {}
either(const either& rhs) : got_ham(rhs.got_ham) {
if (got_ham) {
new(&ham) T(rhs.ham);
} else {
new(&spam) std::exception_ptr(rhs.spam);
}
}
either(either&& rhs) : got_ham(rhs.got_ham) {
if (got_ham) {
new(&ham) T(std::move(rhs.ham));
} else {
new(&spam) std::exception_ptr(std::move(rhs.spam));
}
}
~either() {
if (got_ham) {
ham.~T();
} else {
spam.~exception_ptr();
}
}
template <class E>
static either<T> fromException(const E& exception) {
if (typeid(exception) != typeid(E)) {
throw std::invalid_argument("slicing detected");
}
return fromException(std::make_exception_ptr(exception));
}
template <class V>
static either<V> fromException(std::exception_ptr p) {
either<V> result;
result.got_ham = false;
new(&result.spam) std::exception_ptr(std::move(p));
return result;
}
template <class V>
static either<V> fromException() {
return fromException<V>(std::current_exception());
}
template <class E> bool hasException() const {
try {
if (!got_ham) std::rethrow_exception(spam);
} catch (const E& object) {
return true;
} catch (...) {
}
return false;
}
template <class F>
auto then(F fun) const -> either<decltype(fun(ham).needed_for_decltype())> {
typedef decltype(fun(ham).needed_for_decltype()) ResT;
if (!got_ham) {
either<ResT> result;
result.got_ham = false;
result.spam = spam;
return result;
}
try {
return fun(ham);
} catch (...) {
return fromException<ResT>();
}
}
T& thenReturn() {
if (!got_ham) std::rethrow_exception(spam);
return ham;
}
const T& thenReturn() const {
if (!got_ham) std::rethrow_exception(spam);
return ham;
}
T needed_for_decltype();
};
template <class T>
auto square(T num) -> either<T>
{
std::cout << "square\n";
return num*num;
}
either<double> square2(int num)
{
return num*num;
}
int doit()
{
using std::cout;
using std::string;
auto fun1 = [] (int x) -> either<int> { cout << "fun1\n"; throw "Some error"; };
auto fun2 = [] (int x) -> either<string> { cout << "fun2\n"; return string("string"); };
auto fun3 = [] (string x) -> either<int> { cout << "fun3\n"; return 53; };
int r = either<int>(1)
.then([] (int x) -> either<double> { return x + 1; })
.then([] (double x) -> either<int> { return x*x; })
.then(fun2) // here we transform to string and back to int.
.then(fun3)
.then(square<int>) // need explicit disambiguation
.then(square2)
.thenReturn();
auto r2 = either<int>(1)
.then(fun1) // exception thrown here
.then(fun2) // we can apply other functions,
.then(fun3); // but they will be ignored
try {
// when we access the value, it throws an exception.
cout << "returned : " << r2.thenReturn();
} catch (...) {
cout << "ouch, exception\n";
}
return r;
}
int main() {
using std::cout;
doit();
cout << "end. ok";
}