From 171993808fbbc18f50c815d8b3ebb91d834dfaad Mon Sep 17 00:00:00 2001 From: Ben Deane Date: Fri, 15 Aug 2025 10:40:13 -0600 Subject: [PATCH] :sparkles: Add unpacking and piping for optional `and_then` Problem: - It's awkward to continually unpack tuples when calling `optional` ... `transform` ... `and_then` chains. - It's wordy to express `and_then` chains, when the pipe operator is a conventional way to express monadic bind. Solution: - Add automatic tuple unpacking. - Add pipe syntax for `and_then` chains. --- docs/optional.adoc | 23 ++++++++ include/stdx/optional.hpp | 118 +++++++++++++++++++++++++++----------- test/optional.cpp | 39 +++++++++++++ 3 files changed, 148 insertions(+), 32 deletions(-) diff --git a/docs/optional.adoc b/docs/optional.adoc index 5d38233..5f9670d 100644 --- a/docs/optional.adoc +++ b/docs/optional.adoc @@ -90,6 +90,15 @@ tombstone value. At first thought, https://en.cppreference.com/w/cpp/numeric/math/isnan[NaN] is the obvious tombstone, but NaNs never compare equal to anything, not even themselves. +For tuple-like types, if all of their component parts provide tombstone values, +a tombstone value for them is synthesized: + +[source,cpp] +---- +// S has a tombstone value, therefore so does std::tuple +auto o = stdx::optional{std::tuple{S{42}, S{17}}}; +---- + === Multi-argument `transform` `stdx::optional` provides one extra feature over `std::optional`: the ability to @@ -112,3 +121,17 @@ auto opt_sum = transform( This flavor of `transform` returns the result of the function only if all of its `stdx::optional` arguments are engaged. If any one is not, a disengaged `stdx::optional` is returned. + +=== Unpacking `transform` and `and_then` + +When the contained `value_type` supports the tuple protocol with `apply`, +`transform` (or `and_then`) can unpack it to pass arguments to a function: + +[source,cpp] +---- +auto opt1 = stdx::optional{std::tuple{S{42}, S{17}}}; +auto opt_sum = transform( + [](S const &x, S const &y) { return S{x.value + y.value}; }, + opt1); +// result is stdx::optional{S{59}} +---- diff --git a/include/stdx/optional.hpp b/include/stdx/optional.hpp index f631420..21034e3 100644 --- a/include/stdx/optional.hpp +++ b/include/stdx/optional.hpp @@ -61,6 +61,37 @@ template struct tombstone_value { } }; +namespace optional_detail { +template struct unwrap_invoker { + template + constexpr static auto invoke(F &&f, A &&a) { + return [&] { return std::forward(f)(std::forward(a)); }; + } +}; + +template typename L, typename... Ts> +struct unwrap_invoker, + std::void_t(), + std::declval>()))>> { + template + constexpr static auto invoke(F &&f, A &&a) { + return [&] { return apply(std::forward(f), std::forward(a)); }; + } +}; + +template +constexpr auto unwrap_invoke(F &&f, Arg &&arg) { + return unwrap_invoker, + stdx::remove_cvref_t>::invoke(std::forward(f), + std::forward( + arg)); +} + +template +using unwrap_invoke_result_t = + decltype(unwrap_invoke(std::declval(), std::declval())()); +} // namespace optional_detail + template > class optional { static_assert(not std::is_integral_v or not stdx::is_specialization_of_v, @@ -169,32 +200,34 @@ template > class optional { } template constexpr auto transform(F &&f) & { - using func_t = stdx::remove_cvref_t; - using U = std::invoke_result_t; - return *this ? optional{with_result_of{ - [&] { return std::forward(f)(val); }}} - : optional{}; + using U = optional_detail::unwrap_invoke_result_t; + return *this + ? optional{with_result_of{optional_detail::unwrap_invoke( + std::forward(f), val)}} + : optional{}; } template constexpr auto transform(F &&f) const & { - using func_t = stdx::remove_cvref_t; - using U = std::invoke_result_t; - return *this ? optional{with_result_of{ - [&] { return std::forward(f)(val); }}} - : optional{}; + using U = + optional_detail::unwrap_invoke_result_t; + return *this + ? optional{with_result_of{optional_detail::unwrap_invoke( + std::forward(f), val)}} + : optional{}; } template constexpr auto transform(F &&f) && { - using func_t = stdx::remove_cvref_t; - using U = std::invoke_result_t; - return *this ? optional{with_result_of{ - [&] { return std::forward(f)(std::move(val)); }}} - : optional{}; + using U = optional_detail::unwrap_invoke_result_t; + return *this + ? optional{with_result_of{optional_detail::unwrap_invoke( + std::forward(f), std::move(val))}} + : optional{}; } template constexpr auto transform(F &&f) const && { - using func_t = stdx::remove_cvref_t; - using U = std::invoke_result_t; - return *this ? optional{with_result_of{ - [&] { return std::forward(f)(std::move(val)); }}} - : optional{}; + using U = + optional_detail::unwrap_invoke_result_t; + return *this + ? optional{with_result_of{optional_detail::unwrap_invoke( + std::forward(f), std::move(val))}} + : optional{}; } template constexpr auto or_else(F &&f) const & -> optional { @@ -205,24 +238,28 @@ template > class optional { } template constexpr auto and_then(F &&f) & { - using func_t = stdx::remove_cvref_t; - using U = std::invoke_result_t; - return *this ? std::forward(f)(val) : U{}; + using U = optional_detail::unwrap_invoke_result_t; + return *this ? optional_detail::unwrap_invoke(std::forward(f), val)() + : U{}; } template constexpr auto and_then(F &&f) const & { - using func_t = stdx::remove_cvref_t; - using U = std::invoke_result_t; - return *this ? std::forward(f)(val) : U{}; + using U = + optional_detail::unwrap_invoke_result_t; + return *this ? optional_detail::unwrap_invoke(std::forward(f), val)() + : U{}; } template constexpr auto and_then(F &&f) && { - using func_t = stdx::remove_cvref_t; - using U = std::invoke_result_t; - return *this ? std::forward(f)(std::move(val)) : U{}; + using U = optional_detail::unwrap_invoke_result_t; + return *this ? optional_detail::unwrap_invoke(std::forward(f), + std::move(val))() + : U{}; } template constexpr auto and_then(F &&f) const && { - using func_t = stdx::remove_cvref_t; - using U = std::invoke_result_t; - return *this ? std::forward(f)(std::move(val)) : U{}; + using U = + optional_detail::unwrap_invoke_result_t; + return *this ? optional_detail::unwrap_invoke(std::forward(f), + std::move(val))() + : U{}; } private: @@ -260,6 +297,23 @@ template > class optional { -> bool { return not(lhs < rhs); } + + template + [[nodiscard]] friend constexpr auto operator|(optional const &lhs, F &&f) { + return lhs.and_then(std::forward(f)); + } + template + [[nodiscard]] friend constexpr auto operator|(optional &lhs, F &&f) { + return lhs.and_then(std::forward(f)); + } + template + [[nodiscard]] friend constexpr auto operator|(optional &&lhs, F &&f) { + return std::move(lhs).and_then(std::forward(f)); + } + template + [[nodiscard]] friend constexpr auto operator|(optional const &&lhs, F &&f) { + return std::move(lhs).and_then(std::forward(f)); + } }; template optional(T) -> optional; diff --git a/test/optional.cpp b/test/optional.cpp index 0a5cb9d..d9d768a 100644 --- a/test/optional.cpp +++ b/test/optional.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -464,3 +465,41 @@ TEST_CASE("tombstone traits for product types come from components", STATIC_REQUIRE(*o == std::tuple{E{0xffu}, S{-1}, std::numeric_limits::infinity()}); } + +TEST_CASE("transform unpacks tuples if necessary", "[optional]") { + constexpr auto o1 = stdx::optional{std::tuple{S{42}, S{17}}}; + constexpr auto o2 = + o1.transform([](auto s1, auto s2) { return S{s1.value + s2.value}; }); + STATIC_REQUIRE(o2->value == 59); +} + +TEST_CASE("transform unpacks tuple-protocol types if necessary", "[optional]") { + constexpr auto o1 = stdx::optional{std::pair{S{42}, S{17}}}; + constexpr auto o2 = + o1.transform([](auto s1, auto s2) { return S{s1.value + s2.value}; }); + STATIC_REQUIRE(o2->value == 59); +} + +TEST_CASE("and_then unpacks tuples if necessary", "[optional]") { + constexpr auto o1 = stdx::optional{std::tuple{S{42}, S{17}}}; + constexpr auto o2 = o1.and_then([](auto s1, auto s2) { + return stdx::optional{S{s1.value + s2.value}}; + }); + STATIC_REQUIRE(o2->value == 59); +} + +TEST_CASE("and_then unpacks tuple-protocol types if necessary", "[optional]") { + constexpr auto o1 = stdx::optional{std::pair{S{42}, S{17}}}; + constexpr auto o2 = o1.and_then([](auto s1, auto s2) { + return stdx::optional{S{s1.value + s2.value}}; + }); + STATIC_REQUIRE(o2->value == 59); +} + +TEST_CASE("and_then is pipeable", "[optional]") { + constexpr auto const o1 = stdx::optional{std::tuple{S{42}, S{17}}}; + constexpr auto const o2 = o1 | [](auto s1, auto s2) { + return stdx::optional{S{s1.value + s2.value}}; + }; + STATIC_REQUIRE(o2->value == 59); +}