(Прежде всего «связать» в вопросе не имеет ничего общего с std::bind
)
Я смотрел ожидаемый<T> говорить и я думал, что в презентации по истории этой техники не хватало основной идеи, стоящей за этим в Хаскеле.
Основная идея в 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
возвращает Expected<Baz>
, callFun2
возвращает Expected<Bar>
, а также callFun3
возвращает Expected<Foo>
,
Как видите, этот код не проверяет наличие ошибок. Ошибки остановят выполнение, но они все еще имеют все преимущества Expected<T>
, Это стандартный способ использования Either
Монада в Хаскеле.
Как я уже сказал, наверняка кто-то должен был смотреть на это.
Изменить: я написал неправильные типы возврата для callFun {1..3}. Они вернулись Expected<T>
не T
для различных значений T
, В этом вся суть then
или же >>
комбинатор.
Передача нормальных функций в шаблоны функций (скажем, ваш .then
) в C ++, в отличие от Haskell, крайне разочаровывает. Вы должны предоставить явную сигнатуру типа для них, если они перегружены или шаблоны. Это уродливо и не поддается цепочкам монадических вычислений.
Кроме того, наши текущие лямбды мономорфный, Вы должны явно указать типы параметров, что еще больше ухудшит ситуацию.
Было много (библиотечных) попыток упростить функциональное программирование на C ++, но оно всегда сводится к этим двум пунктам.
И последнее, но не менее важное: программирование в функциональном стиле на C ++ не является нормой, и есть много людей, которым эта концепция совершенно чужда, в то время как концепция, подобная «возвратному коду», легко понять.
(Обратите внимание, что ваш .then
шаблон функции V
параметр должен быть указан явно, но это относительно легко исправить.)
Отвечая на мой собственный вопрос, чтобы дать больше информации и задокументировать мой эксперимент:
Я изуродовал 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";
}