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

#ifndef __MAYBE_HPP
#define __MAYBE_HPP

#include <functional>
#include <optional>
#include <variant>

// https://schneide.blog/2018/01/11/c17-the-two-line-visitor-explained/
template<class... Ts> struct Maybe : Ts... { using Ts::operator()...; };
template<class... Ts> Maybe(Ts...) -> Maybe<Ts...>;

// Just<T>
template<typename T>
struct Just {
    T value;
    Just(T t) : value(t){}
};
// Nothing
struct Nothing {};

// Using macro to simplify code
#define MAYBE(t) std::variant<Just<t>, Nothing>

/// Extract value from MAYBE(T)
///
/// @param t The variable that saves the extracted value
/// @param maybe Some value maybe has type `T`
///
/// @return true if Just<T>
///         false if Nothing{}
template<typename T>
bool some(T &t, const MAYBE(T) &maybe) {
    bool has_value;
    std::visit(Maybe {
        [&](Just<T> a) {
            t = a.value;
            has_value = true;
        },
        [&](Nothing) {
            has_value = false;
        }
    }, maybe);
    return has_value;
}

/// Try to extract value from `maybe`,
/// if `maybe` has a value, then invoke the callback `cb` with that value
/// otherwise return Nothing{}
///
/// @param maybe Some value maybe has type `T`
/// @param cb Callback function
///
/// @return MAYBE(T) from the callback function
template <typename T>
MAYBE(T) with_some(const MAYBE(T) &maybe, const std::function<MAYBE(T)(T &value)> &&cb) {
    if (int value; some(value, maybe)) {
        return cb(value);
    } else {
        return Nothing{};
    }
}

/// Try to extract value from `maybe`,
/// if `maybe` has a value, then invoke the callback `cb` with that value
/// otherwise return Nothing{}
///
/// @param maybe Some value maybe has type `T`
/// @param cb Callback function
///
/// @return Result from the callback function, the callback function has return type `R`
template <typename T, typename R>
R with_some(const MAYBE(T) &maybe, const std::function<R(T &value)> &&cb) {
    if (T value; some(value, maybe)) {
        return cb(value);
    } else {
        return Nothing{};
    }
}

/// Try to extract value from `maybe`,
/// if `maybe` has a value, then invoke the callback `cb` with that value
/// otherwise return Nothing{}
///
/// @param maybe Some value maybe has type `T`
/// @param cb Callback function
template <typename T, typename R>
void with_some(const MAYBE(T) &maybe, const std::function<void(T &value)> &&cb) {
    if (T value; some(value, maybe)) {
        cb(value);
    } else {
        return;
    }
}

#endif  // __MAYBE_HPP

And now we can write our division function safe and simple.

#include <iostream>
#include "maybe.hpp"

MAYBE(int) div_v5(MAYBE(int) a, MAYBE(int) b) {
    return with_some<int>(a, [=](int &just_a) -> MAYBE(int) {
        return with_some<int>(b, [=](int &just_b) -> MAYBE(int) {
            // return Nothing{} if divisor is 0
            if (just_b == 0) return Nothing{};
            
            // otherwise return just_a / just_b
            return Just<int>{just_a / just_b};
        });
    });
}

int main(int argc, char *argv[]) {
    // we can easily transform the math expression ((100 / 10) / (200 / 50))
    if (int result; some(result, div_v5(div_v5(100, 10), div_v5(200, 50)))) {
        std::cout << result << '\n';
    }
}

The code above actually compiles and runs, and it is also something like a proof-of-concept. C++ 17 standard added std::variant, so we can write

#include <iostream>
#include <optional>

std::optional<int> div_v6(std::optional<int> a, std::optional<int> b) {
    if (a.has_value() && b.has_value() && b.value() != 0) {
        return a.value() / b.value();
    } else {
        return std::nullopt;
    }
}

int main(int argc, char *argv[]) {
    std::optional<int> p = div_v6(div_v6(100, 0), div_v6(200, 50));
    if (p.has_value()) {
        std::cout << p.value() << '\n';
    }
}
声明: 本文为 Cocoa 原创, 转载注明出处喵~

Leave a Reply

Your email address will not be published. Required fields are marked *