Skip to content

Commit 1719938

Browse files
committed
✨ 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.
1 parent 4a768f6 commit 1719938

File tree

3 files changed

+148
-32
lines changed

3 files changed

+148
-32
lines changed

docs/optional.adoc

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,15 @@ tombstone value. At first thought,
9090
https://en.cppreference.com/w/cpp/numeric/math/isnan[NaN] is the obvious
9191
tombstone, but NaNs never compare equal to anything, not even themselves.
9292

93+
For tuple-like types, if all of their component parts provide tombstone values,
94+
a tombstone value for them is synthesized:
95+
96+
[source,cpp]
97+
----
98+
// S has a tombstone value, therefore so does std::tuple<S, S>
99+
auto o = stdx::optional{std::tuple{S{42}, S{17}}};
100+
----
101+
93102
=== Multi-argument `transform`
94103

95104
`stdx::optional` provides one extra feature over `std::optional`: the ability to
@@ -112,3 +121,17 @@ auto opt_sum = transform(
112121
This flavor of `transform` returns the result of the function only if all of its
113122
`stdx::optional` arguments are engaged. If any one is not, a disengaged
114123
`stdx::optional` is returned.
124+
125+
=== Unpacking `transform` and `and_then`
126+
127+
When the contained `value_type` supports the tuple protocol with `apply`,
128+
`transform` (or `and_then`) can unpack it to pass arguments to a function:
129+
130+
[source,cpp]
131+
----
132+
auto opt1 = stdx::optional{std::tuple{S{42}, S{17}}};
133+
auto opt_sum = transform(
134+
[](S const &x, S const &y) { return S{x.value + y.value}; },
135+
opt1);
136+
// result is stdx::optional{S{59}}
137+
----

include/stdx/optional.hpp

Lines changed: 86 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,37 @@ template <auto V> struct tombstone_value {
6161
}
6262
};
6363

64+
namespace optional_detail {
65+
template <typename Func, typename Arg, typename = void> struct unwrap_invoker {
66+
template <typename F, typename A>
67+
constexpr static auto invoke(F &&f, A &&a) {
68+
return [&] { return std::forward<F>(f)(std::forward<A>(a)); };
69+
}
70+
};
71+
72+
template <typename Func, template <typename...> typename L, typename... Ts>
73+
struct unwrap_invoker<Func, L<Ts...>,
74+
std::void_t<decltype(apply(std::declval<Func>(),
75+
std::declval<L<Ts...>>()))>> {
76+
template <typename F, typename A>
77+
constexpr static auto invoke(F &&f, A &&a) {
78+
return [&] { return apply(std::forward<F>(f), std::forward<A>(a)); };
79+
}
80+
};
81+
82+
template <typename F, typename Arg>
83+
constexpr auto unwrap_invoke(F &&f, Arg &&arg) {
84+
return unwrap_invoker<stdx::remove_cvref_t<F>,
85+
stdx::remove_cvref_t<Arg>>::invoke(std::forward<F>(f),
86+
std::forward<Arg>(
87+
arg));
88+
}
89+
90+
template <typename F, typename Arg>
91+
using unwrap_invoke_result_t =
92+
decltype(unwrap_invoke(std::declval<F>(), std::declval<Arg>())());
93+
} // namespace optional_detail
94+
6495
template <typename T, typename TS = tombstone_traits<T>> class optional {
6596
static_assert(not std::is_integral_v<T> or
6697
not stdx::is_specialization_of_v<TS, tombstone_traits>,
@@ -169,32 +200,34 @@ template <typename T, typename TS = tombstone_traits<T>> class optional {
169200
}
170201

171202
template <typename F> constexpr auto transform(F &&f) & {
172-
using func_t = stdx::remove_cvref_t<F>;
173-
using U = std::invoke_result_t<func_t, value_type &>;
174-
return *this ? optional<U>{with_result_of{
175-
[&] { return std::forward<F>(f)(val); }}}
176-
: optional<U>{};
203+
using U = optional_detail::unwrap_invoke_result_t<F, value_type &>;
204+
return *this
205+
? optional<U>{with_result_of{optional_detail::unwrap_invoke(
206+
std::forward<F>(f), val)}}
207+
: optional<U>{};
177208
}
178209
template <typename F> constexpr auto transform(F &&f) const & {
179-
using func_t = stdx::remove_cvref_t<F>;
180-
using U = std::invoke_result_t<func_t, value_type const &>;
181-
return *this ? optional<U>{with_result_of{
182-
[&] { return std::forward<F>(f)(val); }}}
183-
: optional<U>{};
210+
using U =
211+
optional_detail::unwrap_invoke_result_t<F, value_type const &>;
212+
return *this
213+
? optional<U>{with_result_of{optional_detail::unwrap_invoke(
214+
std::forward<F>(f), val)}}
215+
: optional<U>{};
184216
}
185217
template <typename F> constexpr auto transform(F &&f) && {
186-
using func_t = stdx::remove_cvref_t<F>;
187-
using U = std::invoke_result_t<func_t, value_type &&>;
188-
return *this ? optional<U>{with_result_of{
189-
[&] { return std::forward<F>(f)(std::move(val)); }}}
190-
: optional<U>{};
218+
using U = optional_detail::unwrap_invoke_result_t<F, value_type &&>;
219+
return *this
220+
? optional<U>{with_result_of{optional_detail::unwrap_invoke(
221+
std::forward<F>(f), std::move(val))}}
222+
: optional<U>{};
191223
}
192224
template <typename F> constexpr auto transform(F &&f) const && {
193-
using func_t = stdx::remove_cvref_t<F>;
194-
using U = std::invoke_result_t<func_t, value_type const &&>;
195-
return *this ? optional<U>{with_result_of{
196-
[&] { return std::forward<F>(f)(std::move(val)); }}}
197-
: optional<U>{};
225+
using U =
226+
optional_detail::unwrap_invoke_result_t<F, value_type const &&>;
227+
return *this
228+
? optional<U>{with_result_of{optional_detail::unwrap_invoke(
229+
std::forward<F>(f), std::move(val))}}
230+
: optional<U>{};
198231
}
199232

200233
template <typename F> constexpr auto or_else(F &&f) const & -> optional {
@@ -205,24 +238,28 @@ template <typename T, typename TS = tombstone_traits<T>> class optional {
205238
}
206239

207240
template <typename F> constexpr auto and_then(F &&f) & {
208-
using func_t = stdx::remove_cvref_t<F>;
209-
using U = std::invoke_result_t<func_t, value_type &>;
210-
return *this ? std::forward<F>(f)(val) : U{};
241+
using U = optional_detail::unwrap_invoke_result_t<F, value_type &>;
242+
return *this ? optional_detail::unwrap_invoke(std::forward<F>(f), val)()
243+
: U{};
211244
}
212245
template <typename F> constexpr auto and_then(F &&f) const & {
213-
using func_t = stdx::remove_cvref_t<F>;
214-
using U = std::invoke_result_t<func_t, value_type const &>;
215-
return *this ? std::forward<F>(f)(val) : U{};
246+
using U =
247+
optional_detail::unwrap_invoke_result_t<F, value_type const &>;
248+
return *this ? optional_detail::unwrap_invoke(std::forward<F>(f), val)()
249+
: U{};
216250
}
217251
template <typename F> constexpr auto and_then(F &&f) && {
218-
using func_t = stdx::remove_cvref_t<F>;
219-
using U = std::invoke_result_t<func_t, value_type &&>;
220-
return *this ? std::forward<F>(f)(std::move(val)) : U{};
252+
using U = optional_detail::unwrap_invoke_result_t<F, value_type &&>;
253+
return *this ? optional_detail::unwrap_invoke(std::forward<F>(f),
254+
std::move(val))()
255+
: U{};
221256
}
222257
template <typename F> constexpr auto and_then(F &&f) const && {
223-
using func_t = stdx::remove_cvref_t<F>;
224-
using U = std::invoke_result_t<func_t, value_type &&>;
225-
return *this ? std::forward<F>(f)(std::move(val)) : U{};
258+
using U =
259+
optional_detail::unwrap_invoke_result_t<F, value_type const &&>;
260+
return *this ? optional_detail::unwrap_invoke(std::forward<F>(f),
261+
std::move(val))()
262+
: U{};
226263
}
227264

228265
private:
@@ -260,6 +297,23 @@ template <typename T, typename TS = tombstone_traits<T>> class optional {
260297
-> bool {
261298
return not(lhs < rhs);
262299
}
300+
301+
template <typename F>
302+
[[nodiscard]] friend constexpr auto operator|(optional const &lhs, F &&f) {
303+
return lhs.and_then(std::forward<F>(f));
304+
}
305+
template <typename F>
306+
[[nodiscard]] friend constexpr auto operator|(optional &lhs, F &&f) {
307+
return lhs.and_then(std::forward<F>(f));
308+
}
309+
template <typename F>
310+
[[nodiscard]] friend constexpr auto operator|(optional &&lhs, F &&f) {
311+
return std::move(lhs).and_then(std::forward<F>(f));
312+
}
313+
template <typename F>
314+
[[nodiscard]] friend constexpr auto operator|(optional const &&lhs, F &&f) {
315+
return std::move(lhs).and_then(std::forward<F>(f));
316+
}
263317
};
264318

265319
template <typename T> optional(T) -> optional<T>;

test/optional.cpp

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include <cstdint>
77
#include <optional>
88
#include <string_view>
9+
#include <tuple>
910
#include <type_traits>
1011
#include <utility>
1112

@@ -464,3 +465,41 @@ TEST_CASE("tombstone traits for product types come from components",
464465
STATIC_REQUIRE(*o == std::tuple{E{0xffu}, S{-1},
465466
std::numeric_limits<float>::infinity()});
466467
}
468+
469+
TEST_CASE("transform unpacks tuples if necessary", "[optional]") {
470+
constexpr auto o1 = stdx::optional{std::tuple{S{42}, S{17}}};
471+
constexpr auto o2 =
472+
o1.transform([](auto s1, auto s2) { return S{s1.value + s2.value}; });
473+
STATIC_REQUIRE(o2->value == 59);
474+
}
475+
476+
TEST_CASE("transform unpacks tuple-protocol types if necessary", "[optional]") {
477+
constexpr auto o1 = stdx::optional{std::pair{S{42}, S{17}}};
478+
constexpr auto o2 =
479+
o1.transform([](auto s1, auto s2) { return S{s1.value + s2.value}; });
480+
STATIC_REQUIRE(o2->value == 59);
481+
}
482+
483+
TEST_CASE("and_then unpacks tuples if necessary", "[optional]") {
484+
constexpr auto o1 = stdx::optional{std::tuple{S{42}, S{17}}};
485+
constexpr auto o2 = o1.and_then([](auto s1, auto s2) {
486+
return stdx::optional{S{s1.value + s2.value}};
487+
});
488+
STATIC_REQUIRE(o2->value == 59);
489+
}
490+
491+
TEST_CASE("and_then unpacks tuple-protocol types if necessary", "[optional]") {
492+
constexpr auto o1 = stdx::optional{std::pair{S{42}, S{17}}};
493+
constexpr auto o2 = o1.and_then([](auto s1, auto s2) {
494+
return stdx::optional{S{s1.value + s2.value}};
495+
});
496+
STATIC_REQUIRE(o2->value == 59);
497+
}
498+
499+
TEST_CASE("and_then is pipeable", "[optional]") {
500+
constexpr auto const o1 = stdx::optional{std::tuple{S{42}, S{17}}};
501+
constexpr auto const o2 = o1 | [](auto s1, auto s2) {
502+
return stdx::optional{S{s1.value + s2.value}};
503+
};
504+
STATIC_REQUIRE(o2->value == 59);
505+
}

0 commit comments

Comments
 (0)