How to make std::optional a run-time monad with Hana?

Viewed 163

The type hana::optional represents an

Optional value whose optional-ness is known at compile-time

and it models, among others, the concept of Monad. Since it is a compile-time optional, the question of what happens at run-time if I try to hana::flatten a non-empty hana::optional which doesn't wrap another hana::optional makes no sense, because because the error would be caught at compile-time.

But what about a run-time optional?

Here's my attempt on making std::optional (and only it, I'm not making a general concept of a maybe, which also boost::optional would satisfy) a hana::Monad, and I'd like to know if this is the correct way of doing it:

#include <assert.h>
#include <boost/hana/chain.hpp>
#include <optional>
#include <stdexcept>
#include <type_traits>
namespace hana = boost::hana;

template<typename T, typename = void>
struct has_value_type : std::false_type {};
template<typename T>
struct has_value_type<T, std::void_t<typename T::value_type>> : std::true_type {};
template<typename T>
bool constexpr has_value_type_t = has_value_type<T>::value;

namespace boost::hana {

    template<typename T>
    struct transform_impl<std::optional<T>> {
        template <typename Opt, typename F>
        static constexpr auto apply(Opt&& o, F&& f) {
            return o.has_value() ? std::make_optional(f(o.value())) : std::nullopt;
        }
    };

    template<typename T>
    struct flatten_impl<std::optional<T>> {
        template <typename X>
        static constexpr auto apply(X&& x) {
            if constexpr (has_value_type_t<typename X::value_type>)
                return (x.has_value() && x.value().has_value()) ? x.value() : std::nullopt;
            else {
                if (!x.has_value())
                    return std::nullopt;
                else
                    throw std::runtime_error("std::optional<non_stdoptional_type> can't be flattened!");
            }
        }
    };

    template<>
    struct flatten_impl<std::nullopt_t> {
        template <typename X>
        static constexpr auto apply(X&&) {
            return std::nullopt;
        }
    };
}


int main() {
    assert(hana::flatten(std::optional<std::optional<int>>{3}) == std::optional<int>{3});
    assert(hana::flatten(std::optional<int>{}) == std::optional<int>{});
    assert(hana::flatten(std::nullopt) == std::optional<int>{});
    //assert(hana::flatten(std::optional<int>{2}) == std::optional<int>{}); // throws
}
1 Answers

There are a few improvements that can be made.

Boost.Hana uses a tag dispatch system for specializing the impl structs. This simplifies specialization, but also prevents a cartesian product of instantations of the outer impl struct with the inner function template.

Also your use of value_type to detect a nested optional is subject to error given the broad set of types that would satisfy this constraint. The tag system also helps with this if you use hana::is_a<optional_tag, X>.

This more specific constraint also eliminates your need to perform a run-time check for a nested optional since the information is in the type itself. optional<optional<...>>

Specializing for flatten_impl<nullopt_t> is incorrect since you can't run it through transform. I don't think it is necessary.

Here is a simplified version that also demonstrates you can transform the types. It's also constexpr!

#include <assert.h>
#include <boost/hana/core/is_a.hpp>
#include <boost/hana/core/tag_of.hpp>
#include <boost/hana/flatten.hpp>
#include <optional>
#include <string>
namespace hana = boost::hana;

struct my_optional_tag { };

namespace boost::hana {

    template <typename T>
    struct tag_of<std::optional<T>> {
        using type = my_optional_tag;
    };

    template <>
    struct transform_impl<my_optional_tag> {
        template <typename Opt, typename F>
        static constexpr auto apply(Opt&& o, F&& f) {
            using ResultValue = decltype(std::forward<F>(f)(std::forward<Opt>(o).value()));
            using Result = std::optional<std::decay_t<ResultValue>>;
            return o.has_value() ? Result(std::forward<F>(f)(std::forward<Opt>(o).value())) :
                                   Result(std::nullopt);
        }
    };

    template <>
    struct flatten_impl<my_optional_tag> {
        template <typename X>
        static constexpr auto apply(X&& x) {
            using Nested = decltype(std::forward<X>(x).value());
            static_assert(is_a<my_optional_tag, Nested>, "must have nested optional");
            return std::forward<X>(x).value_or(Nested(std::nullopt));
        }
    };
}


int main() {
    // Functor
    static_assert(hana::transform(std::optional<int>{41}, [](int x) { return x + 1.0f; }) ==
            std::optional<float>{42});

    // Monad
    static_assert(hana::flatten(std::optional<std::optional<int>>{3}) == std::optional<int>{3});

    constexpr auto inc = [](int x) { return std::optional{x + 1.0f}; };
    static_assert(hana::chain(std::optional<int>{5}, inc) == std::optional{6.0f});

    static_assert(hana::flatten(std::optional<std::optional<int>>{std::nullopt}) == std::nullopt);

    // hana::flatten(std::optional<int>{3}); // compile-time error
}

https://godbolt.org/z/7KWGn6

Please note that I left out the implementations for hana::lift and hana::ap which are required for hana::Applicative which is required for hana::Monad (in the documented minimal complete definition). I will leave this to the reader.

Related