Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Sum types in C++

At work, I ran into a situation where the best type to describe the result returned from a function would be std::variant<uint64_t, uint64_t> - of course, this isn't valid C++, because you can't have two variants of the same type. I could represent this as a std::pair<bool, uint64_t>, or where the first element of the pair is an enum, but this is a special case; a std::variant<uint64_t, uint64_t, bool> isn't so neatly representable, and my functional programming background really made me want Either - so I went to try to implement it, using the Visitor pattern as I have been able to do in other languages without native support for sum types:

template <typename A, typename B, typename C>
class EitherVisitor {
    virtual C onLeft(const A& left) = 0;
    virtual C onRight(const B& right) = 0;
};

template <typename A, typename B>
class Either {
    template <typename C>
    virtual C Accept(EitherVisitor<A, B, C> visitor) = 0;
};

template <typename A, typename B>
class Left: Either<A, B> {
private:
    A value;
public:
    Left(const A& valueIn): value(valueIn) {}

    template <typename C>
    virtual C Accept(EitherVisitor<A, B, C> visitor) {
        return visitor.onLeft(value);
    }
};

template <typename A, typename B>
class Right: Either<A, B> {
private:
    B value;
public:
    Right(const B& valueIn): value(valueIn) {}
    
    template <typename C>
    virtual C Accept(EitherVisitor<A, B, C> visitor) {
        return visitor.onRight(value);
    }
};

C++ rejects this, because the template method Accept cannot be virtual. Is there a workaround to this limitation, that would allow me to correctly represent the fundamental sum type in terms of its f-algebra and catamorphism?

like image 367
Kris Nuttycombe Avatar asked Jan 25 '23 15:01

Kris Nuttycombe


1 Answers

Perhaps the simplest solution is a lightweight wrapper around T for Right and Left? Basically a strong type alias (could also use Boost's strong typedef)

template<class T>
struct Left
{
    T val;
};

template<class T>
struct Right
{
    T val;
};

And then we can distinguish between them for visitation:

template<class T, class U>
using Either = std::variant<Left<T>, Right<U>>;

Either<int, int> TrySomething()
{
    if (rand() % 2 == 0) // get off my case about rand(), I know it's bad
        return Left<int>{0};
    else
        return Right<int>{0};
}

struct visitor
{
    template<class T>
    void operator()(const Left<T>& val_wrapper)
    {
        std::cout << "Success! Value is: " << val_wrapper.val << std::endl;
    }
    
    template<class T>
    void operator()(const Right<T>& val_wrapper)
    {
        std::cout << "Failure! Value is: " << val_wrapper.val << std::endl;
    }
};

int main()
{
    visitor v;
    for (size_t i = 0; i < 10; ++i)
    {
        auto res = TrySomething();
        std::visit(v, res);
    }
}

Demo

like image 194
AndyG Avatar answered Feb 01 '23 19:02

AndyG