Кто-нибудь использует монадическое программирование в стиле связывания с ожидаемым <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";
}