10

(首先问题中的“绑定”与无关std::bind

我看过Expected<T>的演讲,我认为关于这种技术历史的介绍缺少 Haskell 中这个东西背后的核心思想。

Haskell 的核心思想是你“从不”访问Expected<T>. 相反,您所做的是将 lambda 传递给Expected<T>将根据Expected<T>.

我本来希望这个“绑定”组合器是主要的方法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中使用 monad 的标准方式。

正如我所说,肯定有人看过这个。

编辑:我为 callFun{1..3} 编写了错误的返回类型。他们返回Expected<T>,而不是TT. 这是thenor>>组合子的全部意义。

4

2 回答 2

6

.then与 Haskell 相比,将普通函数传递给 C++ 中的函数模板(例如,您的 . 如果它们被重载或模板,则必须为它们提供显式类型签名。这很丑陋,不适合单子计算链。

此外,我们当前的 lambda 是单态的,您必须显式输入参数类型,这使得整个情况变得更糟。

已经有许多(库)尝试使 C++ 中的函数式编程更容易,但它总是回到这两点。

最后但并非最不重要的一点是,C++ 中的函数式编程不是常态,有很多人对这个概念完全陌生,而类似“返回代码”的概念很容易理解。

(请注意,您的.then函数模板的V参数必须明确指定,但这相对容易修复。)

于 2013-03-14T15:20:58.787 回答
2

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

我残缺不全Expected<T>。我所做的是重命名get()thenReturn()以阻止通过命名来使用它。我重命名了整个东西either<T>

然后我添加了这个then(...)功能。我不认为结果那么糟糕(除了可能有很多错误),但我必须指出这then 不是一元绑定。一元绑定是函数组合的变体,因此您对两个函数进行操作并返回一个函数。 如果可能,then只需将函数应用于 a 。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";
}
于 2013-03-14T23:23:36.207 回答