Maybe you can implement Maybe in this way in C++
I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn't resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years. —— Tony Hoare, QCon London 2009
This post demonstrates a naive idea of how to implement Maybe trait (like std::option::Option
in Rust or Maybe
in Haskell) in C++17 and above.
What drives us to use Maybe
is to avoid ambitious result. For example, if you're going to write a simple function that handle division.
int div_v1(int a, int b) { return a / b; }
Of course we know that if b
is 0
, a floating point exception will be raised and your program will crash.
Well, technically we can just let the program crash, but most of the time we want to handle this illegal input more elegantly. So a if
statement should be put in this function to see whether b
is 0
.
int div_v2(int a, int b) { if (b == 0) { fprintf(stderr, "[ERROR] %s: b is 0", __PRETTY_FUNCTION__); // but what should we return? return 0; } else { return a / b; } }
However, it seems that there is no appropriate value to return if b
is 0
. Think about div_v2(0, 233)
, the result should exactly 0
, so 0
cannot be used as an identification of illegal input.
Any other number? Then think about div_v2(a, 1)
, since variable a
can be any number, and b
is 1
, so there is no such number we can use as identification of illegal input.
Do we have any workaround? Let's see. Try to return NULL
if b
is 0
. But NULL
is just an alias of 0
before C++11 standard.
If we use nullptr
, which introduced since C++11 so that we can distinguish 0
and nullptr
, the code will be
int * div_v3(int a, int b) { if (b == 0) { fprintf(stderr, "[ERROR] %s: b is 0", __PRETTY_FUNCTION__); return nullptr; } else { // since we cannot return a temporary variable on stack // we have to explicitly allocate memory int * res = new int; *res = a / b; return res; } } int main(int argc, char *argv[]) { int * res = div_v3(0, 3); if (res != nullptr) { printf("%d\n", *res); // which introduced extra memory management delete res; } }
As you can see, this requires extra memory management. Maybe you will argue that we can do this
int * div_v4(int a, int b, int &result) { if (b == 0) { fprintf(stderr, "[ERROR] %s: b is 0", __PRETTY_FUNCTION__); return nullptr; } else { result = a / b; return &result; } } int main(int argc, char *argv[]) { int result; int * ret = div_v4(0, 3, result); if (ret != nullptr) { printf("%d\n", result); } }
And if you're using C++17 standard, you can write the code of main
part more compactly.
// compile with `-std=c++17` if (int result; div_v4(0, 3, result) != nullptr) { printf("%d\n", result); }
Well, this is where a "but" comes in. You cannot transform the math expression (100 / 10) / (200 / 50)
in one line. So instead of writing something we could do with div_v1
int result = div_v1(div_v1(100, 10), div_v1(200, 50));
we can only write
// compile with `-std=c++17` if (int result_a; div_v4(100, 10, result_a) != nullptr) { if (int result_b; div_v4(200, 50, result_b) != nullptr) { if (int result_c; div_v4(result_a, result_b, result_c) != nullptr) { // what a hell } } }
In order to be safe and easy, we can write a maybe.hpp
that wraps all functionalities of Maybe