Fixing a bug with C++'s >>= operator
This is very important and I don't know why it has been broken for so long
As someone who has done a bit of functional programming, I have to say that C++ has really strange design choices, like being eagerly evaluated and allowing the user to cause all sorts of weird memory safety issues. But that’s excusable – not all languages can be as good as Haskell.
However, there is one thing that cannot be excused – C++ has this weird bug where the >>= operator represents right-bitshift-and-assign, and not what it should be, which is the monadic bind operator like in Haskell.
Well, thanks to C++’s amazing operator overload system, I was able to write some code that fixes the problem!
Here’s some Haskell code:
module Main where
main :: IO ()
main =
let obj1 = Just 10
obj2 = Nothing :: Maybe Int
action x = Just (x + 10)
newthing1 = obj1 >>= action
newthing2 = obj2 >>= action
composeTwice = obj1 >>= action >>= action
in do
putStrLn "Hello World!"
putStrLn $ show obj1 ++ " becomes " ++ show newthing1
putStrLn $ show obj2 ++ " becomes " ++ show newthing2
putStrLn $ "Composed twice: " ++ show composeTwice
And here’s the equivalent C++ code with my custom Maybe
type:
int main() {
Maybe<int> obj1 = Maybe<int>::Just(10);
Maybe<int> obj2 = Maybe<int>::Nothing();
std::function<Maybe<int>(const int &)> action =
([](const int &a) { return Maybe<int>::Just(a + 10); });
Maybe<int> newthing1 = (obj1 >>= action);
Maybe<int> newthing2 = (obj2 >>= action);
Maybe<int> compose_twice = ((obj1 >>= action) >>= action);
std::cout << "Hello World!\n"
<< obj1 << " becomes " << newthing1 << "\n"
<< obj2 << " becomes " << newthing2 << "\n"
<< "Composed twice: " << compose_twice;
}
Here’s the output:
Hello World!
Just(10) becomes Just(20)
Nothing becomes Nothing
Composed twice: Just(30)
This kind of thing is extremely useful, and I would like C++ to fix this bug as soon as possible.
Full source
The full source of the mockup is available as a gist, but also mirrored here:
#include <functional>
#include <iostream>
#include <memory>
#include <optional>
#include <sstream>
#include <string>
template <class A> class Maybe {
std::unique_ptr<A> contents;
Maybe(std::unique_ptr<A> contents) : contents(std::move(contents)) {}
Maybe() {}
public:
static Maybe<A> Just(A a) { return Maybe(std::make_unique<A>(a)); }
static Maybe<A> Nothing() { return Maybe(); }
bool is_just() const { return this->contents != nullptr; }
const A &unwrap() const {
if (this->contents) {
return *this->contents;
}
throw "failed to unwrap nothing";
}
template <class B> auto operator>>=(std::function<Maybe<B>(const A &)> f) {
if (this->is_just()) {
return f(this->unwrap());
}
return Maybe<B>::Nothing();
}
};
template <class A>
std::ostream &operator<<(std::ostream &os, const Maybe<A> &obj) {
if (obj.is_just()) {
return os << "Just(" << obj.unwrap() << ")";
} else {
return os << "Nothing";
}
}
int main() {
Maybe<int> obj1 = Maybe<int>::Just(10);
Maybe<int> obj2 = Maybe<int>::Nothing();
std::function<Maybe<int>(const int &)> action =
([](const int &a) { return Maybe<int>::Just(a + 10); });
Maybe<int> newthing1 = (obj1 >>= action);
Maybe<int> newthing2 = (obj2 >>= action);
Maybe<int> compose_twice = ((obj1 >>= action) >>= action);
std::cout << "Hello World!\n"
<< obj1 << " becomes " << newthing1 << "\n"
<< obj2 << " becomes " << newthing2 << "\n"
<< "Composed twice: " << compose_twice;
}
#undef SHITPOST
Okay, you must be wondering how the hell this works. Well, the meat of the code is here.
template <class B> auto operator>>=(std::function<Maybe<B>(const A &)> f) {
if (this->is_just()) {
return f(this->unwrap());
}
return Maybe<B>::Nothing();
}
Let’s break this down.
Assignments are expressions
Assignments in C++ are expressions. Even =. Usually, they return the newly
assigned value. For example, if you write something like a = b = c
that
assigns b to the value of c, and then a to the value.
How do add-and-assign operators work?
#include <iostream>
int main() {
int a = 2;
int b = 3;
int c = 7;
int d = a += b += c;
std::cout << a << " " << b << " " << c << " " << d;
}
has output 12 10 7 12
. What’s happening here is:
b += c
makesc
stay the same andb = b + c = 3 + 7 = 10
a += b
makesb
stay the same anda = a + b = 2 + 10 = 12
d = a
makesd = a = 12
.
Other operate-and-assign operators have basically the same rules.
Notice that it’s right-associative (i.e. this is a += (b += c)
) whereas
Haskell’s >>=
is left-associative (as in, a >>= b >>= c
is
(a >>= b) >>= c
). That’s why I have to write it like this:
Maybe<int> compose_twice = ((obj1 >>= action) >>= action);
auto
return type
I tried a type signature like this:
template <class B> Maybe<B> operator>>=(std::function<Maybe<B>(const A &)> f);
This will error at the very first time it’s used, even if you explicitly specify the return type like so:
Maybe<int> newthing1 = (obj1 >>= action);
This is because even though we did constrain B
in the arguments, C++ seems too
stupid to guess what the return will be.
C++23
I know std::optional
got a .and_then()
method added to it that’s basically
this but less cursed. I have not tried a C++23 compiler, but I suspect you might
be able to generalize to anything that has a .and_then()
method, although I
haven’t tried that yet.