(First of all "bind" in the question has nothing to do with std::bind
)
I have watched the Expected<T> talk and I thought the presentation on the history of this technique was missing the core idea behind this thing in Haskell.
The core idea in Haskell is that you "never" acess the value of an Expected<T>
. What you do instead is pass a lambda to the Expected<T>
that will either be applied or not depending on the state of the Expected<T>
.
I would have expected this "bind" combinator to be the main method that Expected<T>
would be used, so I have to ask if this programming style has been rejected for some reason. I'll call that combinator then
in the following:
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());
}
}
The point of this combinator is to chain a list of functions where you don't need to check for errors, and where the first function that fails will short-circuit the evaluation.
auto res = Expected<Foo>::fromCode([]() { return callFun1(...); })
.then([](Baz& val) { return callFun2(..,val,..); })
.then([](Bar& val) { return callFun3(val,...); });
Or this syntax which is starting to resemble the >>=
operator that is used in Haskell.
auto res = []() { return callFun1(...); }
>> [](Baz& val) { return callFun2(..,val,..); }
>> [](Bar& val) { return callFun3(val,...); };
callFun1
returns a Expected<Baz>
, callFun2
returns a Expected<Bar>
, and callFun3
returns a Expected<Foo>
.
As you can see, this code doesn't check for errors. Errors will stop execution, but they still have all the advantages of Expected<T>
. This is the standard way to use the Either
monad in Haskell.
As I said, surely someone must have looked at this.
Edit: I wrote wrong return types for callFun{1..3}. They return Expected<T>
, not T
for various values of T
. This is sort of the whole point of the then
or >>
combinator.
Answering my own question to give some more information and document my experiment:
I mutilated Expected<T>
. What I did was rename get()
to thenReturn()
to discourage its use through naming. I renamed the whole thing either<T>
.
And then I added the then(...)
function. I don't think the result is that bad (except for probably lots of bugs), but I must point out that then
isn't monadic bind. The monadic bind is a variant of function composition, so you operate on two functions and return a function. then
simply applies a function to an either
, if possible.
What we get is
// 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;
}
Here is a full example:
#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";
}
Passing normal functions to function templates (say, your .then
) in C++, as opposed to Haskell, is extremely frustrating. You have to provide an explicit type signature for them if they're overloaded or templates. This is ugly and doesn't lend itself to monadic computation chains.
Also, our current lambdas are monomorphic, you have to explicitly type out the parameter types, which makes this whole situation even worse.
There have been many (library) tries to make functional programming in C++ easier, but it always comes back down to those two points.
Last but not least, functional-style programming in C++ isn't the norm and there are many people to whom that concept is completely alien, while a "return code"-like concept is easy to understand.
(Note that your .then
function template's V
parameter has to be specified explicitly, but that is relatively easy fixable.)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With