有人在使用Expected<T>的单子绑定式编程吗?

11
首先,“bind”与std::bind无关。我看了一下Expected<T>的讲解,认为其中缺失了Haskell中这种技术背后的核心思想。
在Haskell中,核心思想是“永远不要”访问Expected<T>的值。相反,你需要将一个lambda表达式传递给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> 的所有优点。 这是在 Haskell 中使用 Either monad 的标准方式。

就像我说的那样,肯定有人已经看过这个问题了。

编辑:我为 callFun{1..3} 写错了返回类型。 它们返回各种值的 Expected<T>,而不是 T。 这就是 then>> combinator 的全部意义所在。

2个回答

6
将普通函数传递给函数模板(比如你的.then)在C++中非常令人沮丧,需要为它们提供显式类型签名,如果它们是重载或模板。这很丑陋,不利于单子计算链。
此外,我们当前的lambda是单态的,必须显式地输入参数类型,这使整个情况更加糟糕。
有许多(库)尝试使C++中的函数编程更容易,但最终总归回到这两个问题。
最后但并非最不重要的是,在C++中进行函数式编程不是常见的做法,对许多人来说这个概念完全陌生,而“返回代码”类似的概念则易于理解。
(请注意,您的.then函数模板的V参数必须显式指定,但这是相对容易解决的。)

你能否举一个C++中最佳语法的例子,以便扩展回答? - user239558
@user: some_expected.then(static_cast<R(*)(Args...)>(&some_overloaded_function)),或者使用lambda表达式,因为它们是单态的,可以看作是伪装的转换。 :| - Xeo
这有点超出我的理解范围,但你们两个可能想要查看Bartosz的讲座:https://www.youtube.com/watch?v=ph7qt0pkPkc 免责声明:就像很多FP的情况一样,我有一种感觉:要么这不是那么特别,要么我错过了什么。 - NoSenseEtAl
@NoSenseEtAl:你漏了点什么。;-D - ildjarn

2

为了提供更多信息并记录我的实验,我回答自己的问题:

我修改了Expected<T>。我所做的是将get()重命名为thenReturn(),通过命名来鼓励不使用它。我将整个东西重命名为either<T>

然后我添加了then(...)函数。我认为结果还不错(除了可能有很多错误),但我必须指出,then并不是单子绑定(monadic bind)。单子绑定是函数组合的一种变体,因此您可以操作两个函数并返回一个函数。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";
}

无论是在Got_Ham城市还是在异常国度,但这只是垃圾邮件。 - v.oddou

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接